Merge branch 'master' into profile_feed

This commit is contained in:
Matthieu 2021-03-14 22:54:39 +01:00
commit 10bf68978d
98 changed files with 2946 additions and 2173 deletions

View File

@ -1,8 +1,8 @@
image: reactivecircus/android-emulator-26:latest
image: reactivecircus/android-emulator-23:latest
variables:
API_LEVEL: "26"
API_LEVEL: "23"
ARCH: "x86"
TARGET: "default"
@ -34,25 +34,24 @@ debugTests:
- ./gradlew -Pci --console=plain :app:testDebug
#emulatorTest:
# interruptible: true
# stage: test
# script:
# - sdkmanager --sdk_root=${ANDROID_HOME} "system-images;android-${API_LEVEL};${TARGET};${ARCH}"
# - echo no | avdmanager create avd --force --name "api-${API_LEVEL}" --abi "${TARGET}/${ARCH}" --package "system-images;android-${API_LEVEL};${TARGET};${ARCH}"
# - $ANDROID_HOME/emulator/emulator -avd "api-${API_LEVEL}" -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none &
# - chmod +x android-wait-for-emulator.sh
# - ./gradlew build
# - ./android-wait-for-emulator.sh
# - adb shell settings put global window_animation_scale 0.0
# - adb shell settings put global transition_animation_scale 0.0
# - adb shell settings put global animator_duration_scale 0.0
emulatorTest:
interruptible: true
stage: test
script:
- echo no | avdmanager create avd --force --name "api-${API_LEVEL}" --abi "${TARGET}/${ARCH}" --package "system-images;android-${API_LEVEL};${TARGET};${ARCH}"
- $ANDROID_SDK_ROOT/emulator/emulator -avd "api-${API_LEVEL}" -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none &
- chmod +x android-wait-for-emulator.sh
- ./gradlew build
- ./android-wait-for-emulator.sh
- adb shell settings put global window_animation_scale 0.0
- adb shell settings put global transition_animation_scale 0.0
- adb shell settings put global animator_duration_scale 0.0
# - ./gradlew build connectedCheck connectedDebugAndroidTest jacocoTestReport
- ./gradlew build connectedCheck connectedStagingAndroidTest jacocoTestReport
# - cat app/build/reports/jacoco/jacocoTestReport/html/index.html | grep -o 'Total[^%]*%'
- cat app/build/reports/jacoco/jacocoTestReport/html/index.html | grep -o 'Total[^%]*%'
# artifacts:
# paths:
# - ./app/build/reports/jacoco/jacocoTestReport/
# expire_in: 1 week
artifacts:
paths:
- ./app/build/reports/jacoco/jacocoTestReport/
expire_in: 1 week

View File

@ -24,8 +24,8 @@ android {
applicationId "com.h.pixeldroid"
minSdkVersion 23
targetSdkVersion 30
versionCode 9
versionName "1.0.alpha8"
versionCode 10
versionName "1.0.alpha9"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
@ -38,16 +38,40 @@ android {
test.java.srcDirs += 'src/test/java'
androidTest.java.srcDirs += 'src/androidTest/java'
}
testBuildType "staging"
buildTypes {
debug {
}
staging {
initWith debug
testCoverageEnabled true
// These values are first looked for in the env variables, and, if not found there,
// in the local.properties file (which is not checked into version control of course!).
// If you are not running the integration tests, you can just set them to dummy values
// in local.properties or comment the buildConfigField lines
def localProperties = new Properties()
if (rootProject.file("local.properties").exists()) {
localProperties.load(new FileInputStream(rootProject.file("local.properties")))
}
buildConfigField "String", "USER_ID", System.getenv("USER_ID") ?: localProperties['USER_ID']
buildConfigField "String", "INSTANCE_URI", System.getenv("INSTANCE_URI") ?: localProperties['INSTANCE_URI']
buildConfigField "String", "ACCESS_TOKEN", System.getenv("ACCESS_TOKEN") ?: localProperties['ACCESS_TOKEN']
buildConfigField "String", "REFRESH_TOKEN", System.getenv("REFRESH_TOKEN") ?: localProperties['REFRESH_TOKEN']
buildConfigField "String", "CLIENT_ID", System.getenv("CLIENT_ID") ?: localProperties['CLIENT_ID']
buildConfigField "String", "CLIENT_SECRET", System.getenv("CLIENT_SECRET") ?: localProperties['CLIENT_SECRET']
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
minifyEnabled true
shrinkResources true
proguardFiles 'proguard-rules.pro'
}
}
testOptions {
@ -64,7 +88,6 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
/**
* AndroidX dependencies:
@ -73,32 +96,32 @@ dependencies {
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.2'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.4'
implementation "androidx.browser:browser:1.3.0"
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.2'
implementation 'androidx.paging:paging-runtime-ktx:3.0.0-alpha12'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.4'
implementation 'androidx.paging:paging-runtime-ktx:3.0.0-beta02'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.0'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0"
implementation "androidx.annotation:annotation:1.1.0"
implementation 'androidx.gridlayout:gridlayout:1.0.0'
// Use the most recent version of CameraX
def cameraX_version = '1.0.0-rc01'
def cameraX_version = '1.0.0-rc03'
implementation "androidx.camera:camera-core:${cameraX_version}"
implementation "androidx.camera:camera-camera2:${cameraX_version}"
// CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:$cameraX_version"
// CameraX View class
implementation 'androidx.camera:camera-view:1.0.0-alpha20'
implementation 'androidx.camera:camera-view:1.0.0-alpha22'
def room_version = "2.3.0-alpha04"
def room_version = "2.3.0-beta03"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
@ -109,7 +132,7 @@ dependencies {
*/
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.google.android.material:material:1.3.0'
//Dagger (dependency injection)
implementation 'com.google.dagger:dagger-android:2.30.1'
@ -124,7 +147,7 @@ dependencies {
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.20'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'com.github.connyduck:sparkbutton:4.0.0'
implementation 'com.github.connyduck:sparkbutton:4.1.0'
implementation 'info.androidhive:imagefilters:1.0.7'
@ -166,7 +189,8 @@ dependencies {
*/
// debugImplementation required vs testImplementation: https://issuetracker.google.com/issues/128612536
debugImplementation("androidx.fragment:fragment-testing:1.2.5") {
//noinspection FragmentGradleConfiguration
stagingImplementation("androidx.fragment:fragment-testing:1.3.1") {
exclude group:'androidx.test', module:'monitor'
}
@ -195,7 +219,7 @@ tasks.withType(Test) {
}
task jacocoTestReport(type: JacocoReport, dependsOn: ['connectedDebugAndroidTest', 'testDebugUnitTest', 'createDebugCoverageReport']) {
task jacocoTestReport(type: JacocoReport, dependsOn: ['connectedStagingAndroidTest', 'testStagingUnitTest', 'createStagingCoverageReport']) {
reports {
xml.enabled = true
@ -209,9 +233,9 @@ task jacocoTestReport(type: JacocoReport, dependsOn: ['connectedDebugAndroidTest
getClassDirectories().from(files([kotlinDebugTree]))
getExecutionData().from(fileTree(dir: project.buildDir, includes: [
'outputs/code_coverage/debugAndroidTest/connected/*coverage.ec',
'outputs/code_coverage/stagingAndroidTest/connected/*coverage.ec',
'jacoco/testDebugUnitTest.exec'
'jacoco/testStagingUnitTest.exec'
]))
}

View File

@ -765,3 +765,10 @@
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/ongakuer/CircleIndicator
- artifact: androidx.dynamicanimation:dynamicanimation:+
name: dynamicanimation
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html

View File

@ -1,21 +1,68 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# proguard file largely copied from Tusky's
# GENERAL OPTIONS (inspired from AOSP's proguard-android-optimize.txt)
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# turn on all optimizations except those that are known to cause problems on Android
-optimizations !code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 6
-allowaccessmodification
-dontpreverify
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-keepattributes *Annotation*
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
native <methods>;
}
# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
}
# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# APP SPECIFIC OPTIONS
# keep members of our model classes, they are used in json de/serialization
-keepclassmembers class com.h.pixeldroid.utils.api.objects.* { *; }
-keep public enum com.h.pixeldroid.utils.api.objects.*$** {
**[] $VALUES;
public *;
}
# preserve line numbers for crash reporting
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
# 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 checkParameterIsNotNull(java.lang.Object, java.lang.String);
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
static void throwUninitializedPropertyAccessException(java.lang.String);
}

View File

@ -4,6 +4,7 @@ import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_CHOOSER
import android.widget.ImageButton
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.intent.Intents
@ -14,7 +15,6 @@ import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.postCreation.camera.CameraFragment
import com.h.pixeldroid.testUtility.clearData
import com.h.pixeldroid.testUtility.initDB
import kotlinx.android.synthetic.main.camera_ui_container.*
import org.hamcrest.CoreMatchers
import org.hamcrest.Matcher
import org.junit.After
@ -50,9 +50,9 @@ class CameraTest {
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
refreshToken = "refreshToken",
clientId = "clientId",
clientSecret = "clientSecret"
)
)
db.close()
@ -88,23 +88,23 @@ class CameraTest {
val scenario = launchFragmentInContainer<CameraFragment>()
scenario.onFragment { fragment ->
fragment.photo_view_button.performClick()
fragment.view?.findViewById<ImageButton>(R.id.photo_view_button)?.performClick()
}
Thread.sleep(1000)
Intents.intended(expectedIntent)
}
@Test
fun switchButton() {
val scenario = launchFragmentInContainer<CameraFragment>()
scenario.onFragment { fragment ->
fragment.camera_switch_button.performClick()
fragment.view?.findViewById<ImageButton>(R.id.camera_switch_button)?.performClick()
}
Thread.sleep(1000)
//FIXME this assert doesn't actually do anything...
// All this test really does is make sure it doesn't crash
scenario.onFragment { fragment ->
assert(!fragment.isHidden)
}

View File

@ -12,12 +12,8 @@ import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.UiDevice
import com.h.pixeldroid.testUtility.*
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.testUtility.clearData
import com.h.pixeldroid.testUtility.initDB
import org.junit.After
import org.junit.Before
import org.junit.Rule
@ -28,7 +24,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DrawerMenuTest {
private lateinit var mockServer: MockServer
private lateinit var db: AppDatabase
private lateinit var context: Context
@ -38,33 +33,15 @@ class DrawerMenuTest {
@Before
fun before(){
mockServer = MockServer()
mockServer.start()
val baseUrl = mockServer.getUrl()
context = ApplicationProvider.getApplicationContext()
db = initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
testiTestoInstance
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
testiTesto
)
db.close()
@ -78,7 +55,6 @@ class DrawerMenuTest {
@After
fun after() {
clearData()
mockServer.stop()
}
@Test
@ -132,10 +108,11 @@ class DrawerMenuTest {
onView(withText(R.string.menu_account)).perform(click())
// Check that profile activity was opened.
onView(withId(R.id.editButton)).check(matches(isDisplayed()))
val followersText = context.getString(R.string.nb_followers)
.format(68)
val followersText = context.resources.getQuantityString(R.plurals.nb_followers, 2, 2)
onView(withText(followersText)).perform(click())
onView(withText("Dobios")).check(matches(isDisplayed()))
waitForView(R.id.account_entry_avatar)
onView(withText("PixelDroid Developer")).check(matches(isDisplayed()))
}
@Test
@ -144,10 +121,11 @@ class DrawerMenuTest {
onView(withText(R.string.menu_account)).perform(click())
// Check that profile activity was opened.
onView(withId(R.id.editButton)).check(matches(isDisplayed()))
val followingText = context.getString(R.string.nb_following)
.format(27)
val followingText = context.resources.getQuantityString(R.plurals.nb_following, 3, 3)
onView(withText(followingText)).perform(click())
onView(withText("Dobios")).check(matches(isDisplayed()))
waitForView(R.id.account_entry_avatar)
onView(withText("@User 1")).check(matches(isDisplayed()))
}
/*@Test
@ -162,31 +140,38 @@ class DrawerMenuTest {
fun clickFollowers() {
// Open My Profile from drawer
onView(withText(R.string.menu_account)).perform(click())
Thread.sleep(1000)
waitForView(R.id.nbFollowersTextView)
// Open followers list
onView(withId(R.id.nbFollowersTextView)).perform(click())
Thread.sleep(1000)
// Open follower's profile
onView(withText("ete2")).perform(click())
Thread.sleep(1000)
onView(withId(R.id.accountNameTextView)).check(matches(withText("Christian")))
waitForView(R.id.account_entry_avatar)
// Open follower's profile
onView(withText("@pixeldroid")).perform(click())
waitForView(R.id.profilePictureImageView)
onView(withId(R.id.accountNameTextView)).check(matches(withText("PixelDroid Developer")))
}
@Test
fun clickFollowing() {
// Open My Profile from drawer
onView(withText(R.string.menu_account)).perform(click())
Thread.sleep(1000)
waitForView(R.id.nbFollowersTextView)
// Open followers list
onView(withId(R.id.nbFollowingTextView)).perform(click())
Thread.sleep(1000)
// Open following's profile
onView(withText("Dobios")).perform(click())
Thread.sleep(1000)
onView(withId(R.id.accountNameTextView)).check(matches(withText("Andrew Dobis")))
waitForView(R.id.account_entry_avatar)
// Open following's profile
onView(withText("@user2")).perform(click())
waitForView(R.id.profilePictureImageView)
onView(withId(R.id.accountNameTextView)).check(matches(withText("User 2")))
}
@Test

View File

@ -22,14 +22,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.adapters.ThumbnailAdapter
import com.h.pixeldroid.postCreation.photoEdit.PhotoEditActivity
import com.h.pixeldroid.postCreation.photoEdit.ThumbnailAdapter
import com.h.pixeldroid.settings.AboutActivity
import com.h.pixeldroid.testUtility.CustomMatchers
import com.h.pixeldroid.testUtility.clearData
import junit.framework.Assert.assertTrue
import kotlinx.android.synthetic.main.fragment_edit_image.*
import com.h.pixeldroid.testUtility.clickChildViewWithId
import com.h.pixeldroid.testUtility.slowSwipeLeft
import com.h.pixeldroid.testUtility.waitForView
import org.hamcrest.CoreMatchers.allOf
import org.junit.*
import org.junit.Assert.assertTrue
import org.junit.rules.Timeout
import org.junit.runner.RunWith
import java.io.File
@ -72,7 +74,7 @@ class EditPhotoTest {
activityScenario = ActivityScenario.launch<PhotoEditActivity>(intent).onActivity{a -> activity = a}
Thread.sleep(1000)
waitForView(R.id.coordinator_edit)
}
@After
@ -119,14 +121,15 @@ class EditPhotoTest {
@Test
fun FiltersIsSwipeableAndClickeable() {
waitForView(R.id.thumbnail)
Espresso.onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(1, CustomMatchers.clickChildViewWithId(R.id.thumbnail)))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(1, clickChildViewWithId(R.id.thumbnail)))
Thread.sleep(1000)
Espresso.onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(1, CustomMatchers.slowSwipeLeft(false)))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(1, slowSwipeLeft(false)))
Thread.sleep(1000)
Espresso.onView(withId(R.id.recycler_view))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(5, CustomMatchers.clickChildViewWithId(R.id.thumbnail)))
.perform(actionOnItemAtPosition<ThumbnailAdapter.MyViewHolder>(5, clickChildViewWithId(R.id.thumbnail)))
Espresso.onView(withId(R.id.image_preview)).check(matches(isDisplayed()))
}
@ -134,16 +137,16 @@ class EditPhotoTest {
fun BrightnessSaturationContrastTest() {
Espresso.onView(withId(R.id.tabs)).perform(selectTabAtPosition(1))
Thread.sleep(1000)
waitForView(R.id.seekbar_brightness)
var change = 5
Espresso.onView(withId(R.id.seekbar_brightness)).perform(setProgress(change))
Espresso.onView(withId(R.id.seekbar_contrast)).perform(setProgress(change))
Espresso.onView(withId(R.id.seekbar_saturation)).perform(setProgress(change))
Assert.assertEquals(change, activity.seekbar_brightness.progress)
Assert.assertEquals(change, activity.seekbar_contrast.progress)
Assert.assertEquals(change, activity.seekbar_saturation.progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_brightness).progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_contrast).progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_saturation).progress)
Thread.sleep(1000)
@ -152,28 +155,32 @@ class EditPhotoTest {
Espresso.onView(withId(R.id.seekbar_contrast)).perform(setProgress(change))
Espresso.onView(withId(R.id.seekbar_saturation)).perform(setProgress(change))
Assert.assertEquals(change, activity.seekbar_brightness.progress)
Assert.assertEquals(change, activity.seekbar_contrast.progress)
Assert.assertEquals(change, activity.seekbar_saturation.progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_brightness).progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_contrast).progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_saturation).progress)
}
@Test
fun saveButton() {
// The save button saves the edits and goes back to the post creation activity.
Espresso.onView(withId(R.id.action_save)).perform(click())
Espresso.onView(withId(com.google.android.material.R.id.snackbar_text))
.check(matches(withText(R.string.save_image_success)))
Thread.sleep(1000)
assertTrue(activityScenario.state == Lifecycle.State.DESTROYED)
}
@Test
fun backButton() {
Espresso.onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click())
Thread.sleep(1000)
assertTrue(activityScenario.state == Lifecycle.State.DESTROYED)
}
@Test
fun croppingIsPossible() {
Espresso.onView(withId(R.id.cropImageButton)).perform(click())
Thread.sleep(1000)
waitForView(R.id.menu_crop)
Espresso.onView(withId(R.id.menu_crop)).perform(click())
Espresso.onView(withId(R.id.image_preview)).check(matches(isDisplayed()))
}
@ -181,7 +188,7 @@ class EditPhotoTest {
@Test
fun alreadyUploadingDialog() {
activityScenario.onActivity { a -> a.saving = true }
Espresso.onView(withId(R.id.action_upload)).perform(click())
Espresso.onView(withId(R.id.action_save)).perform(click())
Thread.sleep(1000)
Espresso.onView(withText(R.string.busy_dialog_text)).check(matches(isDisplayed()))
}

View File

@ -10,21 +10,11 @@ import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.posts.StatusViewHolder
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.atPosition
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.clickChildViewWithId
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.first
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.getText
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.second
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.slowSwipeUp
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.typeTextInViewWithId
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.testUtility.clearData
import com.h.pixeldroid.testUtility.initDB
import com.h.pixeldroid.testUtility.*
import org.hamcrest.CoreMatchers.not
import org.junit.After
import org.junit.Before
@ -37,7 +27,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class HomeFeedTest {
private lateinit var mockServer: MockServer
private lateinit var activityScenario: ActivityScenario<MainActivity>
private lateinit var db: AppDatabase
private lateinit var context: Context
@ -47,31 +36,14 @@ class HomeFeedTest {
@Before
fun before(){
mockServer = MockServer()
mockServer.start()
val baseUrl = mockServer.getUrl()
context = ApplicationProvider.getApplicationContext()
db = initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
testiTestoInstance
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
testiTesto
)
db.close()
activityScenario = ActivityScenario.launch(MainActivity::class.java)
@ -79,21 +51,59 @@ class HomeFeedTest {
@After
fun after() {
clearData()
mockServer.stop()
}
@Test
fun clickingTabOnAlbumShowsNextPhoto() {
//Wait for the feed to load
Thread.sleep(1000)
waitForView(R.id.postPager)
activityScenario.onActivity {
a -> run {
//Pick the second photo
a.findViewById<TabLayout>(R.id.postTabs).getTabAt(1)?.select()
a.findViewById<ViewPager2>(R.id.postPager).currentItem = 2
}
}
onView(first(withId(R.id.postTabs))).check(matches(isDisplayed()))
onView(first(withId(R.id.postPager))).check(matches(isDisplayed()))
}
/*
@Test
fun clickingReblogButtonWorks() {
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(0, clickChildViewWithId(R.id.reblogger)))
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(0, clickChildViewWithId(R.id.reblogger)))
onView(first(withId(R.id.nshares)))
.check(matches(withText(getText(first(withId(R.id.nshares))))))
}
@Test
fun doubleTapLikerWorks() {
Thread.sleep(1000)
//Get initial like count
val likes = getText(first(withId(R.id.nlikes)))
val nLikes = likes!!.split(" ")[0].toInt()
//Remove sensitive media warning
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(0, clickChildViewWithId(R.id.sensitiveWarning)))
Thread.sleep(100)
//Like the post
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(0, clickChildViewWithId(R.id.postPicture)))
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder >
(0, clickChildViewWithId(R.id.postPicture)))
//...
Thread.sleep(100)
//Profit
onView(first(withId(R.id.nlikes))).check(matches((withText("${nLikes + 1} Likes"))))
}
@Test
@ -114,10 +124,12 @@ class HomeFeedTest {
actionOnItemAtPosition<StatusViewHolder>(2, clickChildViewWithId(R.id.liker))
)
onView((withId(R.id.list))).check(matches(isDisplayed()))
}
}*/
@Test
fun clickingUsernameOpensProfile() {
waitForView(R.id.username)
onView(withId(R.id.list)).perform(
actionOnItemAtPosition<StatusViewHolder>(0, clickChildViewWithId(R.id.username))
)
@ -126,26 +138,18 @@ class HomeFeedTest {
@Test
fun clickingProfilePicOpensProfile() {
waitForView(R.id.profilePic)
onView(withId(R.id.list)).perform(
actionOnItemAtPosition<StatusViewHolder>(0, clickChildViewWithId(R.id.profilePic))
)
onView(withId(R.id.accountNameTextView)).check(matches(isDisplayed()))
}
@Test
fun clickingReblogButtonWorks() {
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(0, clickChildViewWithId(R.id.reblogger)))
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(0, clickChildViewWithId(R.id.reblogger)))
onView(first(withId(R.id.nshares)))
.check(matches(withText(getText(first(withId(R.id.nshares))))))
}
@Test
fun clickingMentionOpensProfile() {
waitForView(R.id.description)
onView(withId(R.id.list)).perform(
actionOnItemAtPosition<StatusViewHolder>(0, clickChildViewWithId(R.id.description))
)
@ -159,7 +163,7 @@ class HomeFeedTest {
)
onView(withId(R.id.list)).check(matches(isDisplayed()))
}
*/
@Test
fun clickingCommentButtonOpensCommentSection() {
@ -211,20 +215,17 @@ class HomeFeedTest {
Thread.sleep(1000)
onView(first(withId(R.id.commentContainer)))
.check(matches(hasDescendant(withId(R.id.comment))))
}
}*/
@Test
fun performClickOnSensitiveWarning() {
onView(withId(R.id.list)).perform(scrollToPosition<StatusViewHolder>(1))
Thread.sleep(1000)
waitForView(R.id.username)
onView(second(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
Thread.sleep(1000)
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(1, clickChildViewWithId(R.id.sensitiveWarning)))
Thread.sleep(1000)
onView(withId(R.id.list))
.check(matches(atPosition(1, not(withId(R.id.sensitiveWarning)))))
@ -232,45 +233,19 @@ class HomeFeedTest {
@Test
fun performClickOnSensitiveWarningTabs() {
waitForView(R.id.username)
onView(withId(R.id.list)).perform(scrollToPosition<StatusViewHolder>(0))
Thread.sleep(1000)
onView(first(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
Thread.sleep(1000)
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(0, clickChildViewWithId(R.id.sensitiveWarning)))
Thread.sleep(1000)
onView(first(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.GONE)))
}
@Test
fun doubleTapLikerWorks() {
//Get initial like count
val likes = getText(first(withId(R.id.nlikes)))
val nlikes = likes!!.split(" ")[0].toInt()
//Remove sensitive media warning
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(0, clickChildViewWithId(R.id.sensitiveWarning)))
Thread.sleep(100)
//Like the post
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(0, clickChildViewWithId(R.id.postPicture)))
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder >
(0, clickChildViewWithId(R.id.postPicture)))
//...
Thread.sleep(100)
//Profit
onView(first(withId(R.id.nlikes))).check(matches((withText("${nlikes + 1} Likes"))))
}
/*
@Test
fun goOfflineShowsPosts() {

View File

@ -30,9 +30,7 @@ import com.h.pixeldroid.posts.StatusViewHolder
import com.h.pixeldroid.utils.api.objects.Account
import com.h.pixeldroid.utils.api.objects.Account.Companion.ACCOUNT_TAG
import com.h.pixeldroid.settings.AboutActivity
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.testUtility.clearData
import com.h.pixeldroid.testUtility.initDB
import com.h.pixeldroid.testUtility.*
import org.hamcrest.CoreMatchers
import org.hamcrest.Matcher
import org.hamcrest.Matchers
@ -47,7 +45,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class IntentTest {
private lateinit var mockServer: MockServer
private lateinit var db: AppDatabase
private lateinit var context: Context
@ -62,34 +59,14 @@ class IntentTest {
@Before
fun before() {
mockServer = MockServer()
mockServer.start()
val baseUrl = mockServer.getUrl()
context = ApplicationProvider.getApplicationContext()
db = initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
testiTestoInstance
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
)
db.userDao().insertUser(testiTesto)
db.close()
Intents.init()
@ -100,81 +77,32 @@ class IntentTest {
fun clickingMentionOpensProfile() {
ActivityScenario.launch(MainActivity::class.java)
val account = Account("1450", "deerbard_photo", "deerbard_photo",
"https://pixelfed.social/deerbard_photo", "deerbard photography",
val account = Account("265626292148375552", "user2", "user2",
"https://testing2.pixeldroid.org/user2", "User 2",
"",
"https://pixelfed.social/storage/avatars/000/000/001/450/SMSep5NoabDam1W8UDMh_avatar.png?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a",
"https://pixelfed.social/storage/avatars/000/000/001/450/SMSep5NoabDam1W8UDMh_avatar.png?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a",
"https://testing2.pixeldroid.org/storage/avatars/default.jpg?v=0",
"https://testing2.pixeldroid.org/storage/avatars/default.jpg?v=0",
"", "", false, emptyList(), null,
"2018-08-01T12:58:21.000000Z", 72, 68, 27,
"2021-02-11T23:44:03.000000Z", 0, 1, 1,
null, null, false, null)
val expectedIntent: Matcher<Intent> = CoreMatchers.allOf(
IntentMatchers.hasExtra(ACCOUNT_TAG, account)
)
Thread.sleep(1000)
waitForView(R.id.description)
//Click the mention
Espresso.onView(ViewMatchers.withId(R.id.list))
.perform(RecyclerViewActions.actionOnItemAtPosition<StatusViewHolder>
(0, clickClickableSpanInDescription("@Dobios")))
(0, clickClickableSpanInDescription("@user2")))
//Wait a bit
Thread.sleep(1000)
//Check that the Profile is shown
//Check that the right intent was launched
intended(expectedIntent)
}
private fun clickClickableSpanInDescription(textToClick: CharSequence): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return Matchers.instanceOf(TextView::class.java)
}
override fun getDescription(): String {
return "clicking on a ClickableSpan"
}
override fun perform(uiController: UiController, view: View) {
val textView = view.findViewById<View>(R.id.description) as TextView
val spannableString = textView.text as SpannableString
if (spannableString.isEmpty()) {
// TextView is empty, nothing to do
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
// Get the links inside the TextView and check if we find textToClick
val spans = spannableString.getSpans(0, spannableString.length, ClickableSpan::class.java)
if (spans.isNotEmpty()) {
var spanCandidate: ClickableSpan
for (span: ClickableSpan in spans) {
spanCandidate = span
val start = spannableString.getSpanStart(spanCandidate)
val end = spannableString.getSpanEnd(spanCandidate)
val sequence = spannableString.subSequence(start, end)
if (textToClick.toString() == sequence.toString()) {
span.onClick(textView)
return
}
}
}
// textToClick not found in TextView
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
}
}
@Test
fun clickEditProfileMakesIntent() {
@ -202,6 +130,5 @@ class IntentTest {
fun after() {
Intents.release()
clearData()
mockServer.stop()
}
}

View File

@ -1,6 +1,27 @@
package com.h.pixeldroid
/*
import android.content.Context
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import com.h.pixeldroid.testUtility.clearData
import com.h.pixeldroid.testUtility.initDB
import com.h.pixeldroid.utils.db.AppDatabase
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.Timeout
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LoginActivityOfflineTest {
@ -10,7 +31,7 @@ class LoginActivityOfflineTest {
fun switchAirplaneMode() {
val device = UiDevice.getInstance(getInstrumentation())
device.openQuickSettings()
device.findObject(UiSelector().textContains("airplane")).click()
device.findObject(UiSelector().textContains("Airplane")).click()
device.pressHome()
}
}
@ -46,6 +67,4 @@ class LoginActivityOfflineTest {
db.close()
clearData()
}
}
*/
}*/

View File

@ -19,9 +19,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.testUtility.clearData
import com.h.pixeldroid.testUtility.initDB
import com.h.pixeldroid.testUtility.testiTesto
import com.h.pixeldroid.testUtility.testiTestoInstance
import org.junit.After
import org.junit.Before
import org.junit.Rule
@ -35,7 +36,6 @@ class LoginActivityOnlineTest {
private lateinit var db: AppDatabase
private lateinit var context: Context
private lateinit var pref: SharedPreferences
private lateinit var server: MockServer
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@ -43,8 +43,6 @@ class LoginActivityOnlineTest {
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
server = MockServer()
server.start()
context = ApplicationProvider.getApplicationContext()
pref = context.getSharedPreferences("com.h.pixeldroid.pref", Context.MODE_PRIVATE)
pref.edit().clear().apply()
@ -55,7 +53,6 @@ class LoginActivityOnlineTest {
@After
fun after() {
clearData()
server.stop()
}
@Test
@ -102,6 +99,7 @@ class LoginActivityOnlineTest {
))
}
/*
@Test
fun correctIntentReturnLoadsMainActivity() {
context = ApplicationProvider.getApplicationContext()
@ -109,36 +107,21 @@ class LoginActivityOnlineTest {
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = server.getUrl().toString(),
title = "PixelTest"
)
testiTestoInstance
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = server.getUrl().toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
)
db.userDao().insertUser(testiTesto)
db.close()
pref.edit()
.putString("domain", server.getUrl().toString())
.putString("clientID", "test_id")
.putString("clientSecret", "test_secret")
.putString("domain", testiTestoInstance.uri)
.putString("clientID", testiTesto.clientId)
.putString("clientSecret", testiTesto.clientSecret)
.apply()
val uri = Uri.parse("oauth2redirect://com.h.pixeldroid?code=test_code")
val uri = Uri.parse("oauth2redirect://com.h.pixeldroid?code=$testiTesto.")
val intent = Intent(ACTION_VIEW, uri, context, LoginActivity::class.java)
ActivityScenario.launch<LoginActivity>(intent)
Thread.sleep(1000)
onView(withId(R.id.main_activity_main_linear_layout)).check(matches(isDisplayed()))
}
*/
}

View File

@ -1,6 +1,5 @@
package com.h.pixeldroid
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import androidx.test.core.app.ActivityScenario
@ -8,14 +7,20 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasDataString
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiDevice
import com.h.pixeldroid.BuildConfig.INSTANCE_URI
import com.h.pixeldroid.testUtility.clearData
import com.h.pixeldroid.testUtility.waitForView
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matcher
@ -28,17 +33,16 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LoginCheckIntent {
private lateinit var context: Context
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@get:Rule
val intentsTestRule = ActivityTestRule(LoginActivity::class.java)
private lateinit var activityScenario: ActivityScenario<LoginActivity>
@Before
fun before() {
Intents.init()
activityScenario = ActivityScenario.launch(LoginActivity::class.java)
}
@Test
@ -46,11 +50,16 @@ class LoginCheckIntent {
ActivityScenario.launch(LoginActivity::class.java)
val expectedIntent: Matcher<Intent> = allOf(
hasAction(ACTION_VIEW),
hasDataString(containsString("pixelfed.de"))
hasDataString(containsString(INSTANCE_URI))
)
Thread.sleep(1000)
onView(withId(R.id.editText)).perform(scrollTo()).perform(ViewActions.replaceText("pixelfed.de"), ViewActions.closeSoftKeyboard())
waitForView(R.id.editText)
onView(withId(R.id.editText)).perform(scrollTo()).perform(
ViewActions.replaceText(
INSTANCE_URI
), ViewActions.closeSoftKeyboard()
)
onView(withId(R.id.connect_instance_button)).perform(scrollTo()).perform(click())
Thread.sleep(3000)
@ -61,22 +70,20 @@ class LoginCheckIntent {
@Test
fun launchesInstanceInfo() {
ActivityScenario.launch(LoginActivity::class.java)
val expectedIntent: Matcher<Intent> = allOf(
hasAction(ACTION_VIEW),
hasDataString(containsString("pixelfed.org/join"))
)
onView(withId(R.id.whatsAnInstanceTextView)).perform(scrollTo()).perform(click())
Thread.sleep(3000)
intended(expectedIntent)
waitForView(R.id.whats_an_instance_explanation)
onView(withText(R.string.whats_an_instance_explanation))
.inRoot(isDialog())
.check(matches(isDisplayed()));
}
@After
fun after() {
Intents.release()
clearData()
activityScenario.close()
}
}

View File

@ -1,49 +1,44 @@
package com.h.pixeldroid
/*
import android.content.Context
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.testUtility.*
import com.h.pixeldroid.utils.db.AppDatabase
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MockedServerTest {
private lateinit var mockServer: MockServer
private lateinit var activityScenario: ActivityScenario<MainActivity>
private lateinit var db: AppDatabase
private lateinit var context: Context
@Before
fun before(){
mockServer = MockServer()
mockServer.start()
val baseUrl = mockServer.getUrl()
context = ApplicationProvider.getApplicationContext()
db = initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
)
db.instanceDao().insertInstance(testiTestoInstance)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token"
)
)
db.userDao().insertUser(testiTesto)
db.close()
activityScenario = ActivityScenario.launch(MainActivity::class.java)
}
@After
fun after() {
clearData()
mockServer.stop()
}
/*
@Test
@ -59,90 +54,53 @@ class MockedServerTest {
Thread.sleep(3000)
onView(first(withId(R.id.username))).check(matches(withText("memo")))
}
*/
@Test
fun searchHashtags() {
activityScenario.onActivity{
a -> a.findViewById<TabLayout>(R.id.tabs).getTabAt(1)?.select()
}
Thread.sleep(1000)
onView(withId(R.id.searchEditText)).perform(ViewActions.replaceText("#caturday"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.search)).perform(typeSearchViewText("#randomNoise"))
onView(withId(R.id.searchButton)).perform(click())
Thread.sleep(3000)
onView(first(withId(R.id.tag_name))).check(matches(withText("#caturday")))
waitForView(R.id.tag_name)
onView(first(withId(R.id.tag_name))).check(matches(withText("#randomNoise")))
}
*/
@Test
fun openDiscoverPost(){
activityScenario.onActivity{
a -> a.findViewById<TabLayout>(R.id.tabs).getTabAt(1)?.select()
}
Thread.sleep(1000)
onView(first(withId(R.id.postPreview))).perform(click())
Thread.sleep(1000)
onView(withId(R.id.username)).check(matches(withText("Arthur")))
waitForView(R.id.postPreview)
onView(first(withId(R.id.postPreview))).perform(click())
waitForView(R.id.username)
onView(withId(R.id.username)).check(matches(withSubstring("User ")))
}
/*
@Test
fun searchAccounts() {
activityScenario.onActivity{
a -> a.findViewById<TabLayout>(R.id.tabs).getTabAt(1)?.select()
}
Thread.sleep(1000)
onView(withId(R.id.searchEditText)).perform(ViewActions.replaceText("@dansup"), ViewActions.closeSoftKeyboard())
waitForView(R.id.search)
onView(withId(R.id.searchButton)).perform(click())
Thread.sleep(3000)
onView(first(withId(R.id.account_entry_username))).check(matches(withText("dansup")))
onView(withId(R.id.search)).perform(typeSearchViewText("@user3"))
waitForView(R.id.account_entry_username)
onView(first(withId(R.id.account_entry_username))).check(matches(withText("User 3")))
}
*/
@Test
fun clickFollowButton() {
//Get initial like count
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.username)))
Thread.sleep(1000)
// Unfollow
onView(withId(R.id.followButton)).perform((click()))
Thread.sleep(1000)
onView(withId(R.id.followButton)).check(matches(withText("Follow")))
// Follow
onView(withId(R.id.followButton)).perform((click()))
Thread.sleep(1000)
onView(withId(R.id.followButton)).check(matches(withText("Unfollow")))
}
@Test
fun clickOtherUserFollowers() {
//Get initial like count
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.username)))
Thread.sleep(1000)
// Open followers list
onView(withId(R.id.nbFollowersTextView)).perform((click()))
Thread.sleep(1000)
// Open follower's profile
onView(withText("ete2")).perform((click()))
Thread.sleep(1000)
onView(withId(R.id.accountNameTextView)).check(matches(withText("Christian")))
}
/*TODO test notifications (harder since they disappear after 6 months...
@Test
fun testNotificationsList() {
@ -156,7 +114,7 @@ class MockedServerTest {
onView(withId(R.id.view_pager)).perform(ViewActions.swipeDown())
Thread.sleep(1000)
onView(withText("Dobios followed you")).check(matches(withId(R.id.notification_type)))
onView(withText("user2 followed you")).check(matches(withId(R.id.notification_type)))
}
@Test
@ -222,7 +180,7 @@ class MockedServerTest {
Thread.sleep(1000)
onView(first(withText("Andrea"))).check(matches(withId(R.id.username)))
}
}*/
@Test
fun swipingRightStopsAtHomepage() {
@ -230,7 +188,8 @@ class MockedServerTest {
a -> a.findViewById<TabLayout>(R.id.tabs).getTabAt(4)?.select()
} // go to the last tab
Thread.sleep(1000)
waitForView(R.id.main_activity_main_linear_layout)
onView(withId(R.id.main_activity_main_linear_layout))
.perform(ViewActions.swipeRight()) // notifications
.perform(ViewActions.swipeRight()) // camera
@ -246,7 +205,8 @@ class MockedServerTest {
a -> a.findViewById<TabLayout>(R.id.tabs).getTabAt(0)?.select()
}
Thread.sleep(1000)
waitForView(R.id.main_activity_main_linear_layout)
onView(withId(R.id.main_activity_main_linear_layout))
.perform(ViewActions.swipeLeft()) // notifications
.perform(ViewActions.swipeLeft()) // camera
@ -262,7 +222,8 @@ class MockedServerTest {
a -> a.findViewById<TabLayout>(R.id.tabs).getTabAt(4)?.select()
} // go to the last tab
Thread.sleep(1000)
waitForView(R.id.view_pager)
onView(withId(R.id.view_pager))
.perform(ViewActions.swipeRight()) // notifications
.perform(ViewActions.swipeRight()) // camera
@ -276,7 +237,7 @@ class MockedServerTest {
a -> assert(a.findViewById<TabLayout>(R.id.tabs).getTabAt(0)?.isSelected ?: false)
}
}
/*
@Test
fun censorMatrices() {
val array: FloatArray = floatArrayOf(
@ -287,7 +248,5 @@ class MockedServerTest {
assert(censorColorMatrix().equals(ColorMatrix(array)))
assert(uncensorColorMatrix().equals(ColorMatrix()))
}
}
*/
}*/
}

View File

@ -1,11 +1,41 @@
package com.h.pixeldroid
/*
import android.Manifest
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.util.Log
import android.view.View.VISIBLE
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.h.pixeldroid.postCreation.PostCreationActivity
import com.h.pixeldroid.postCreation.photoEdit.ThumbnailAdapter
import com.h.pixeldroid.settings.AboutActivity
import com.h.pixeldroid.testUtility.*
import com.h.pixeldroid.utils.db.AppDatabase
import org.hamcrest.CoreMatchers.not
import org.junit.*
import org.junit.rules.Timeout
import org.junit.runner.RunWith
import java.io.File
@RunWith(AndroidJUnit4::class)
class PostCreationActivityTest {
private var testScenario: ActivityScenario<PostCreationActivity>? = null
private lateinit var mockServer: MockServer
private lateinit var db: AppDatabase
private lateinit var context: Context
@ -26,33 +56,19 @@ class PostCreationActivityTest {
@Before
fun setup() {
context = InstrumentationRegistry.getInstrumentation().targetContext
mockServer = MockServer()
mockServer.start()
val baseUrl = mockServer.getUrl()
db = initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
testiTestoInstance
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token"
)
testiTesto
)
db.close()
var uri1: String = ""
var uri2: String = ""
var uri1: Uri? = null
var uri2: Uri? = null
val scenario = ActivityScenario.launch(AboutActivity::class.java)
scenario.onActivity {
val image1 = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888)
@ -63,28 +79,32 @@ class PostCreationActivityTest {
val file1 = File.createTempFile("temp_img1", ".png")
val file2 = File.createTempFile("temp_img2", ".png")
file1.writeBitmap(image1)
uri1 = file1.toUri().toString()
uri1 = file1.toUri()
file2.writeBitmap(image2)
uri2 = file2.toUri().toString()
Log.d("test", uri1+"\n"+uri2)
uri2 = file2.toUri()
}
val intent = Intent(context, PostCreationActivity::class.java).putExtra("pictures_uri", arrayListOf(uri1, uri2))
val intent = Intent(context, PostCreationActivity::class.java)
intent.clipData = ClipData("", emptyArray(), ClipData.Item(uri1))
intent.clipData!!.addItem(ClipData.Item(uri2))
testScenario = ActivityScenario.launch(intent)
}
@After
fun after() {
clearData()
mockServer.stop()
}
@Test
@Ignore("Annoying to deal with and also sometimes the intent is not working as it should")
fun createPost() {
onView(withId(R.id.post_creation_send_button)).perform(click())
// should send on main activity
onView(withId(R.id.main_activity_main_linear_layout)).check(matches(isDisplayed()))
Thread.sleep(3000)
onView(withId(R.id.list)).check(matches(isDisplayed()))
}
/*
@Test
fun errorShown() {
testScenario!!.onActivity { a -> a.upload_error.visibility = VISIBLE }
@ -113,9 +133,9 @@ class PostCreationActivityTest {
)
)
Thread.sleep(1000)
onView(withId(R.id.action_upload)).perform(click())
onView(withId(R.id.action_save)).perform(click())
Thread.sleep(1000)
onView(withId(R.id.image_grid)).check(matches(isDisplayed()))
onView(withId(R.id.carousel)).check(matches(isDisplayed()))
}
@Test
@ -127,5 +147,5 @@ class PostCreationActivityTest {
)
)
Thread.sleep(1000)
}
}*/
}*/
}

View File

@ -16,13 +16,9 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.testUtility.*
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.testUtility.clearData
import com.h.pixeldroid.testUtility.initDB
import kotlinx.android.synthetic.main.activity_main.*
import org.hamcrest.Matcher
import org.junit.After
import org.junit.Before
@ -48,12 +44,12 @@ class PostCreationFragmentTest {
onView(withId(R.id.drawer_layout))
.perform(swipeLeft())
.perform(swipeLeft())
Thread.sleep(300)
waitForView(R.id.photo_view_button)
}
// upload intent
// image choosing intent
@Test
fun uploadButtonLaunchesGalleryIntent() {
fun galleryButtonLaunchesGalleryIntent() {
val expectedIntent: Matcher<Intent> = hasAction(Intent.ACTION_CHOOSER)
intending(expectedIntent)
onView(withId(R.id.photo_view_button)).perform(click())
@ -64,7 +60,6 @@ class PostCreationFragmentTest {
@RunWith(AndroidJUnit4::class)
class PostFragmentUITests {
private lateinit var mockServer: MockServer
private lateinit var context: Context
@get:Rule
@ -75,48 +70,31 @@ class PostFragmentUITests {
@Before
fun setup() {
context = InstrumentationRegistry.getInstrumentation().targetContext
mockServer = MockServer()
mockServer.start()
val baseUrl = mockServer.getUrl()
db = initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
testiTestoInstance
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
testiTesto
)
db.close()
Thread.sleep(300)
}
@After
fun after() {
clearData()
mockServer.stop()
}
@Test
fun newPostUiTest() {
ActivityScenario.launch(MainActivity::class.java).onActivity {
a -> a.tabs.getTabAt(2)!!.select()
it.findViewById<TabLayout>(R.id.tabs).getTabAt(2)!!.select()
}
Thread.sleep(1500)
waitForView(R.id.photo_view_button)
onView(withId(R.id.photo_view_button)).check(matches(isDisplayed()))
onView(withId(R.id.camera_capture_button)).check(matches(isDisplayed()))
}

View File

@ -13,14 +13,16 @@ import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.h.pixeldroid.BuildConfig.INSTANCE_URI
import com.h.pixeldroid.posts.PostActivity
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.utils.api.objects.*
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.testUtility.clearData
import com.h.pixeldroid.testUtility.initDB
import com.h.pixeldroid.testUtility.testiTesto
import com.h.pixeldroid.testUtility.testiTestoInstance
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.Matcher
import org.junit.*
@ -34,7 +36,6 @@ class PostTest {
private lateinit var context: Context
private lateinit var db: AppDatabase
private lateinit var mockServer: MockServer
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)
@ -42,32 +43,13 @@ class PostTest {
@Before
fun before(){
context = InstrumentationRegistry.getInstrumentation().targetContext
mockServer = MockServer()
mockServer.start()
val baseUrl = mockServer.getUrl()
db = initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(
InstanceDatabaseEntity(
uri = baseUrl.toString(),
title = "PixelTest"
)
testiTestoInstance
)
db.userDao().insertUser(
UserDatabaseEntity(
user_id = "123",
instance_uri = baseUrl.toString(),
username = "Testi",
display_name = "Testi Testo",
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
)
db.userDao().insertUser(testiTesto)
db.close()
Intents.init()
}
@ -76,14 +58,15 @@ class PostTest {
fun saveToGalleryTestSimplePost() {
val attachment = Attachment(
id = "12",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0",
meta = null
)
val post = Status(
id = "12",
account = Account(
id = "12",
username = "douze",
url = "https://pixelfed.de/douze"
username = "SQDFSQDF",
url = "$INSTANCE_URI/pixeldroid",
),
media_attachments = listOf(attachment)
)
@ -106,18 +89,20 @@ class PostTest {
fun saveToGalleryTestAlbum() {
val attachment1 = Attachment(
id = "12",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0",
meta = null
)
val attachment2 = Attachment(
id = "13",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0",
meta = null
)
val post = Status(
id = "12",
account = Account(
id = "12",
username = "douze",
url = "https://pixelfed.de/douze"
url = "$INSTANCE_URI/pixeldroid",
),
media_attachments = listOf(attachment1, attachment2)
)
@ -140,17 +125,18 @@ class PostTest {
fun shareTestSimplePost() {
val expectedIntent: Matcher<Intent> = IntentMatchers.hasAction(Intent.ACTION_CHOOSER)
val attachment = Attachment(
id = "12",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
id = "12",
url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0",
meta = null
)
val post = Status(
id = "12",
account = Account(
id = "12",
username = "douze",
url = "https://pixelfed.de/douze"
),
media_attachments = listOf(attachment)
account = Account(
id = "12",
username = "douze",
url = "$INSTANCE_URI/pixeldroid",
),
media_attachments = listOf(attachment)
)
val intent = Intent(context, PostActivity::class.java)
intent.putExtra(Status.POST_TAG, post)
@ -166,20 +152,22 @@ class PostTest {
val expectedIntent: Matcher<Intent> = IntentMatchers.hasAction(Intent.ACTION_CHOOSER)
val attachment1 = Attachment(
id = "12",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0",
meta = null
)
val attachment2 = Attachment(
id = "13",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
id = "13",
url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0",
meta = null
)
val post = Status(
id = "12",
account = Account(
id = "12",
username = "douze",
url = "https://pixelfed.de/douze"
),
media_attachments = listOf(attachment1, attachment2)
account = Account(
id = "12",
username = "douze",
url = "$INSTANCE_URI/pixeldroid",
),
media_attachments = listOf(attachment1, attachment2)
)
val intent = Intent(context, PostActivity::class.java)
intent.putExtra(Status.POST_TAG, post)
@ -208,7 +196,7 @@ class PostTest {
media_attachments= listOf(
Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg",
preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg",
remote_url=null, text_url=null, description=null, blurhash=null)
remote_url=null, text_url=null, description=null, blurhash=null, meta = null)
),
application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(),
tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)),
@ -235,7 +223,7 @@ class PostTest {
media_attachments= listOf(
Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg",
preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg",
remote_url=null, text_url=null, description=null, blurhash=null)
remote_url=null, text_url=null, description=null, blurhash=null, meta = null)
),
application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(),
tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)),
@ -243,14 +231,13 @@ class PostTest {
in_reply_to_id=null, in_reply_to_account=null, reblog=null, poll=null, card=null, language=null, text=null, favourited=false, reblogged=false, muted=false, bookmarked=false, pinned=false)
Assert.assertEquals("${status.reblogs_count} Shares",
status.getNShares(getInstrumentation().targetContext))
status.getNShares(context))
}
@After
fun after() {
Intents.release()
clearData()
mockServer.stop()
}
}

View File

@ -0,0 +1,102 @@
package com.h.pixeldroid
import android.content.Context
import android.content.Intent
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.h.pixeldroid.postCreation.PostCreationActivity
import com.h.pixeldroid.profile.ProfileActivity
import com.h.pixeldroid.testUtility.*
import com.h.pixeldroid.utils.api.objects.Account
import com.h.pixeldroid.utils.db.AppDatabase
import okhttp3.internal.wait
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.Serializable
@RunWith(AndroidJUnit4::class)
class ProfileTest {
private lateinit var activityScenario: ActivityScenario<ProfileActivity>
private lateinit var db: AppDatabase
private lateinit var context: Context
@Before
fun before(){
context = ApplicationProvider.getApplicationContext()
db = initDB(context)
db.clearAllTables()
db.instanceDao().insertInstance(testiTestoInstance)
db.userDao().insertUser(testiTesto)
db.close()
val intent = Intent(context, ProfileActivity::class.java)
val account = Account(id = "265472486651596800", username = "pixeldroid", acct = "pixeldroid", url = "https://testing2.pixeldroid.org/pixeldroid", display_name = "PixelDroid Developer", avatar = "https://testing2.pixeldroid.org/storage/avatars/default.jpg?v=0", avatar_static = "https://testing2.pixeldroid.org/storage/avatars/default.jpg?v=0", locked = false, emojis = arrayListOf(), discoverable = null, created_at = "2021-02-11T13:32:53.000000Z", statuses_count = 1, followers_count = 1, following_count = 1, moved = null, fields = null, bot = false, source = null)
intent.putExtra(Account.ACCOUNT_TAG, account)
activityScenario = ActivityScenario.launch(intent)
onView(withId(R.id.profileRefreshLayout)).perform(swipeDown())
Thread.sleep(2000)
}
@After
fun after() {
clearData()
}
@Test
fun clickFollowButton() {
if (onView(ViewMatchers.withText("Unfollow")).isDisplayed()) {
//Currently following
// Unfollow
follow("Follow")
// Follow
follow("Unfollow")
} else {
//Currently not following
// Follow
follow("Unfollow")
// Unfollow
follow("Follow")
}
}
private fun follow(follow_or_unfollow: String){
onView(withId(R.id.followButton)).perform((ViewActions.click()))
Thread.sleep(1000)
onView(withId(R.id.followButton)).check(ViewAssertions.matches(ViewMatchers.withText(follow_or_unfollow)))
}
@Test
fun clickOtherUserFollowers() {
// Open followers list
onView(withId(R.id.nbFollowersTextView)).perform((ViewActions.click()))
waitForView(R.id.account_entry_username)
// Open follower's profile
onView(ViewMatchers.withText("testi testo")).perform((ViewActions.click()))
waitForView(R.id.editButton)
//Check that our own profile opened
onView(withId(R.id.editButton)).check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}
}

View File

@ -1,153 +1,284 @@
package com.h.pixeldroid.testUtility
import android.text.SpannableString
import android.text.style.ClickableSpan
import android.view.View
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.*
import androidx.test.espresso.action.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.util.HumanReadables
import androidx.test.espresso.util.TreeIterables
import com.h.pixeldroid.R
import org.hamcrest.BaseMatcher
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import java.util.concurrent.TimeoutException
abstract class CustomMatchers {
companion object {
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
return object : BaseMatcher<T>() {
var isFirst = true
override fun describeTo(description: org.hamcrest.Description?) {
description?.appendText("first matching item")
}
fun ViewInteraction.isDisplayed(): Boolean {
return try {
check(matches(ViewMatchers.isDisplayed()))
true
} catch (e: NoMatchingViewException) {
false
}
}
override fun matches(item: Any?): Boolean {
if (isFirst && matcher.matches(item)) {
isFirst = false
return true
/**
* Waits for a view to appear in the hierarchy
* Doesn't work if the root changes (since it operates on the root!)
* @param viewId The id of the view to wait for.
*/
fun waitForView(viewId: Int) {
Espresso.onView(isRoot()).perform(waitForViewViewAction(viewId))
}
/**
* This ViewAction tells espresso to wait till a certain view is found in the view hierarchy.
* @param viewId The id of the view to wait for.
*/
private fun waitForViewViewAction(viewId: Int): ViewAction {
// The maximum time which espresso will wait for the view to show up (in milliseconds)
val timeOut = 5000
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return isRoot()
}
override fun getDescription(): String {
return "wait for a specific view with id $viewId; during $timeOut millis."
}
override fun perform(uiController: UiController, rootView: View) {
uiController.loopMainThreadUntilIdle()
val startTime = System.currentTimeMillis()
val endTime = startTime + timeOut
val viewMatcher = withId(viewId)
do {
// Iterate through all views on the screen and see if the view we are looking for is there already
for (child in TreeIterables.breadthFirstViewTraversal(rootView)) {
// found view with required ID
if (viewMatcher.matches(child)) {
return
}
}
// Loops the main thread for a specified period of time.
// Control may not return immediately, instead it'll return after the provided delay has passed and the queue is in an idle state again.
uiController.loopMainThreadForAtLeast(100)
} while (System.currentTimeMillis() < endTime) // in case of a timeout we throw an exception -&gt; test fails
throw PerformException.Builder()
.withCause(TimeoutException())
.withActionDescription(this.description)
.withViewDescription(HumanReadables.describe(rootView))
.build()
}
}
}
fun clickClickableSpanInDescription(textToClick: CharSequence): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return Matchers.instanceOf(TextView::class.java)
}
override fun getDescription(): String {
return "clicking on a ClickableSpan"
}
override fun perform(uiController: UiController, view: View) {
val textView = view.findViewById<View>(R.id.description) as TextView
val spannableString = SpannableString(textView.text)
if (spannableString.isEmpty()) {
// TextView is empty, nothing to do
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
// Get the links inside the TextView and check if we find textToClick
val spans = spannableString.getSpans(0, spannableString.length, ClickableSpan::class.java)
if (spans.isNotEmpty()) {
var spanCandidate: ClickableSpan
for (span: ClickableSpan in spans) {
spanCandidate = span
val start = spannableString.getSpanStart(spanCandidate)
val end = spannableString.getSpanEnd(spanCandidate)
val sequence = spannableString.subSequence(start, end)
if (textToClick.toString() == sequence.toString()) {
span.onClick(textView)
return
}
}
}
// textToClick not found in TextView
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
}
}
fun typeSearchViewText(text: String): ViewAction {
return object : ViewAction {
override fun getDescription(): String {
return "Change view text"
}
override fun getConstraints(): Matcher<View> {
return allOf(isDisplayed(), isAssignableFrom(SearchView::class.java))
}
override fun perform(uiController: UiController?, view: View?) {
(view as SearchView).setQuery(text, true)
}
}
}
fun <T> first(matcher: Matcher<T>): Matcher<T> {
return object : BaseMatcher<T>() {
var isFirst = true
override fun describeTo(description: org.hamcrest.Description?) {
description?.appendText("first matching item")
}
override fun matches(item: Any?): Boolean {
if (isFirst && matcher.matches(item)) {
isFirst = false
return true
}
return false
}
}
}
fun <T> second(matcher: Matcher<T>): Matcher<T> {
return object : BaseMatcher<T>() {
var isFirst = true
override fun describeTo(description: org.hamcrest.Description?) {
description?.appendText("second matching item")
}
override fun matches(item: Any?): Boolean {
if (isFirst && matcher.matches(item)) {
isFirst = false
return false
} else if (!isFirst && matcher.matches(item))
return true
return false
}
}
}
fun atPosition(position: Int, itemMatcher: Matcher<View?>): Matcher<View?> {
return object : BoundedMatcher<View?, RecyclerView>(RecyclerView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has item at position $position: ")
itemMatcher.describeTo(description)
}
override fun matchesSafely(view: RecyclerView): Boolean {
val viewHolder = view.findViewHolderForAdapterPosition(position)
?: // has no item on such position
return false
}
}
}
fun <T> second(matcher: Matcher<T>): Matcher<T>? {
return object : BaseMatcher<T>() {
var isFirst = true
override fun describeTo(description: org.hamcrest.Description?) {
description?.appendText("second matching item")
}
override fun matches(item: Any?): Boolean {
if (isFirst && matcher.matches(item)) {
isFirst = false
return false
} else if (!isFirst && matcher.matches(item))
return true
return false
}
}
return itemMatcher.matches(viewHolder.itemView)
}
}
}
fun atPosition(position: Int, itemMatcher: Matcher<View?>): Matcher<View?>? {
return object : BoundedMatcher<View?, RecyclerView>(RecyclerView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has item at position $position: ")
itemMatcher.describeTo(description)
}
override fun matchesSafely(view: RecyclerView): Boolean {
val viewHolder = view.findViewHolderForAdapterPosition(position)
?: // has no item on such position
return false
return itemMatcher.matches(viewHolder.itemView)
}
}
}
/**
* @param percent can be 1 or 0
* 1: swipes all the way up
* 0: swipes half way up
*/
fun slowSwipeUp(percent: Boolean) : ViewAction {
return ViewActions.actionWithAssertions(
GeneralSwipeAction(
/**
* @param percent can be 1 or 0
* 1: swipes all the way up
* 0: swipes half way up
*/
fun slowSwipeUp(percent: Boolean): ViewAction {
return ViewActions.actionWithAssertions(
GeneralSwipeAction(
Swipe.SLOW,
GeneralLocation.BOTTOM_CENTER,
if (percent) GeneralLocation.TOP_CENTER else GeneralLocation.CENTER,
Press.FINGER
)
)
}
)
}
/**
* @param percent can be 1 or 0
* 1: swipes all the way left
* 0: swipes half way left
*/
fun slowSwipeLeft(percent: Boolean) : ViewAction {
return ViewActions.actionWithAssertions(
GeneralSwipeAction(
/**
* @param percent can be 1 or 0
* 1: swipes all the way left
* 0: swipes half way left
*/
fun slowSwipeLeft(percent: Boolean): ViewAction {
return ViewActions.actionWithAssertions(
GeneralSwipeAction(
Swipe.SLOW,
GeneralLocation.CENTER_RIGHT,
if (percent) GeneralLocation.CENTER_LEFT else GeneralLocation.CENTER,
Press.FINGER
)
)
)
}
fun getText(matcher: Matcher<View?>?): String? {
val stringHolder = arrayOf<String?>(null)
Espresso.onView(matcher).perform(object : ViewAction {
override fun getConstraints(): Matcher<View> {
return ViewMatchers.isAssignableFrom(TextView::class.java)
}
fun getText(matcher: Matcher<View?>?): String? {
val stringHolder = arrayOf<String?>(null)
Espresso.onView(matcher).perform(object : ViewAction {
override fun getConstraints(): Matcher<View> {
return ViewMatchers.isAssignableFrom(TextView::class.java)
}
override fun getDescription(): String {
return "getting text from a TextView"
}
override fun perform(
uiController: UiController,
view: View
) {
val tv = view as TextView //Save, because of check in getConstraints()
stringHolder[0] = tv.text.toString()
}
})
return stringHolder[0]
override fun getDescription(): String {
return "getting text from a TextView"
}
fun clickChildViewWithId(id: Int) = object : ViewAction {
override fun getConstraints() = null
override fun getDescription() = "click child view with id $id"
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id)
v.performClick()
}
override fun perform(
uiController: UiController,
view: View,
) {
val tv = view as TextView //Save, because of check in getConstraints()
stringHolder[0] = tv.text.toString()
}
})
return stringHolder[0]
}
fun typeTextInViewWithId(id: Int, text: String) = object : ViewAction {
fun clickChildViewWithId(id: Int) = object : ViewAction {
override fun getConstraints() = null
override fun getConstraints() = null
override fun getDescription() = "click child view with id $id"
override fun getDescription() = "click child view with id $id"
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<EditText>(id)
v.text.append(text)
}
}
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id)
v.performClick()
}
}
fun typeTextInViewWithId(id: Int, text: String) = object : ViewAction {
override fun getConstraints() = null
override fun getDescription() = "click child view with id $id"
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<EditText>(id)
v.text.append(text)
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,235 +0,0 @@
package com.h.pixeldroid.testUtility
import okhttp3.HttpUrl
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
class MockServer {
private val server = MockWebServer()
companion object{
private const val headerName = "Content-Type"
private const val headerValue = "application/json; charset=utf-8"
}
fun start() {
try {
server.start(45106)
server.dispatcher = getDispatcher()
} catch (e: IllegalArgumentException) {
}
}
fun stop(){
try {
server.shutdown()
} catch (e: IllegalArgumentException) {
}
}
private fun getDispatcher(): Dispatcher {
return object : Dispatcher() {
@Throws(InterruptedException::class)
override fun dispatch(request: RecordedRequest): MockResponse {
when (request.path) {
"/api/v1/accounts/verify_credentials" -> return MockResponse()
.addHeader(headerName, headerValue)
.setResponseCode(200).setBody(JsonValues.accountJson)
"/api/v1/instance" -> return MockResponse()
.addHeader(headerName, headerValue)
.setResponseCode(200).setBody(JsonValues.instanceJson.replace("REPLACEWITHDOMAIN", getUrl().toString()))
"/api/v1/media" -> return MockResponse()
.addHeader(headerName, headerValue)
.setResponseCode(200).setBody(JsonValues.mediaUploadResponseJson)
"/api/v1/timelines/home" -> return MockResponse()
.addHeader(headerName, headerValue)
.setResponseCode(200).setBody(JsonValues.feedJson)
"/oauth/token" -> return MockResponse()
.addHeader(headerName, headerValue)
.setResponseCode(200).setBody(JsonValues.tokenJson)
}
when {
request.path?.contains("/api/v1/apps") == true -> {
return MockResponse()
.addHeader("Content-Type", "application/json; charset=utf-8")
.setResponseCode(200).setBody(JsonValues.applicationJson)
}
request.path?.contains("/api/v1/notifications") == true -> {
return MockResponse()
.addHeader("Content-Type", "application/json; charset=utf-8")
.setResponseCode(200).setBody(JsonValues.notificationsJson)
}
request.path?.contains("/api/v1/timelines/home") == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.feedJson)
}
request.path?.contains("/api/v1/timelines/public") == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.feedJson)
}
request.path?.contains("/api/v1/accounts/0/statuses") == true -> {
return MockResponse().setHttp2ErrorCode(401)
}
request.path?.matches("/api/v1/accounts/[0-9]*/statuses".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.accountStatusesJson)
}
request.path?.contains("/api/v1/statuses/0/context") == true -> {
return MockResponse().setHttp2ErrorCode(401)
}
request.path?.matches("/api/v1/statuses/[0-9]*/context".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.commentStatusesJson)
}
request.path?.contains("/api/v1/statuses/0/favourite") == true -> {
return MockResponse().setHttp2ErrorCode(401)
}
request.path?.matches("/api/v1/statuses/[0-9]*/favourite".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.likedJson)
}
request.path?.contains("/api/v1/statuses/0/unfavourite") == true -> {
return MockResponse().setHttp2ErrorCode(401)
}
request.path?.matches("/api/v1/statuses/[0-9]*/unfavourite".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.unlikeJson)
}
request.path?.contains("/api/v1/statuses") == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.unlikeJson)
}
request.path?.contains("/api/v1/accounts/0") == true -> {
return MockResponse().setHttp2ErrorCode(401)
}
request.path?.matches("/api/v1/accounts/[0-9]*".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.accountJson)
}
request.path?.contains("/api/v1/statuses/0/reblog") == true -> {
return MockResponse().setHttp2ErrorCode(401)
}
request.path?.matches("/api/v1/statuses/[0-9]*/reblog".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.reblogJson)
}
request.path?.contains("/api/v1/statuses/0/unreblog") == true -> {
return MockResponse().setHttp2ErrorCode(401)
}
request.path?.matches("/api/v1/statuses/[0-9]*/unreblog".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.unlikeJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/follow".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.followRelationshipJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/unfollow".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.unfollowRelationshipJson)
}
request.path?.contains("/api/v1/accounts/relationships") == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.relationshipJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/followers\\?limit=[0-9]*".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.followersJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/followers\\?since_id=[0-9]*&limit=[0-9]*".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.followersAfterJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/following\\?limit=[0-9]*".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.followersJson)
}
request.path?.matches("/api/v1/accounts/[0-9]*/following\\?since_id=[0-9]*&limit=[0-9]*".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.followersAfterJson)
}
request.path?.matches("/api/v2/search\\?type=hashtags&q=caturday&limit=[0-9]*&offset=[0-9]*".toRegex()) == true -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.searchEmpty)
}
request.path?.contains("/api/v2/search?type=hashtags&q=caturday")!!-> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.searchCaturdayHashtags)
}
request.path?.contains("/api/v2/search?type=statuses&q=caturday")!! -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.searchCaturday)
}
request.path?.contains("/api/v2/search?type=accounts&q=dansup")!! -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.searchDansupAccounts)
}
request.path?.matches("""/api/v2/search\?(max_id=[0-9]*&)?type=(accounts|statuses)&q=dansup(&limit=[0-9]*)?""".toRegex())!! -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.searchEmpty)
}
request.path?.contains("/api/v2/discover/posts")!! -> {
return MockResponse().addHeader(
"Content-Type",
"application/json; charset=utf-8"
).setResponseCode(200).setBody(JsonValues.discover)
}
else -> return MockResponse().setResponseCode(404)
}
}
}
}
fun getUrl(): HttpUrl {
return server.url("")
}
}

View File

@ -0,0 +1,28 @@
package com.h.pixeldroid.testUtility
import com.h.pixeldroid.BuildConfig.*
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
val testiTestoInstance = InstanceDatabaseEntity(
uri = INSTANCE_URI,
title = "PixelDroid CI instance",
maxStatusChars = 150,
maxPhotoSize = 64000,
maxVideoSize = 64000,
albumLimit = 4
)
val testiTesto = UserDatabaseEntity(
user_id = USER_ID,
instance_uri = INSTANCE_URI,
username = "testitesto",
display_name = "testi testo",
avatar_static = "$INSTANCE_URI/storage/avatars/default.jpg?v=0",
isActive = true,
accessToken = ACCESS_TOKEN,
refreshToken = REFRESH_TOKEN,
clientId = CLIENT_ID,
clientSecret = CLIENT_SECRET
)

View File

@ -19,7 +19,6 @@
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"

View File

@ -30883,6 +30883,253 @@
<pre>Copyright [yyyy] [name of copyright owner]&#x000A;&#x000A;Licensed under the Apache License, Version 2.0 (the "License");&#x000A;you may not use this file except in compliance with the License.&#x000A;You may obtain a copy of the License at&#x000A;&#x000A; http://www.apache.org/licenses/LICENSE-2.0&#x000A;&#x000A;Unless required by applicable law or agreed to in writing, software&#x000A;distributed under the License is distributed on an "AS IS" BASIS,&#x000A;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#x000A;See the License for the specific language governing permissions and&#x000A;limitations under the License.</pre>
</div>
</div>
<div class="library">
<!-- https://opensource.org/licenses/Apache-2.0 -->
<h1 class="title">dynamicanimation</h1>
<p class="notice">Copyright &copy; Google Inc. All rights reserved.</p>
<p><a href="http://developer.android.com/tools/extras/support-library.html">http://developer.android.com/tools/extras/support-library.html</a></p>
<input type="checkbox"><label></label>
<div class="license">
<h2>
Apache License
<br/>
Version 2.0, January 2004
<br/>
http://www.apache.org/licenses/
</h2>
<p>TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION</p>
<h2>1. Definitions.</h2>
<p>
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
</p>
<p>
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
</p>
<p>
"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.
</p>
<p>
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
</p>
<p>
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
</p>
<p>
"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.
</p>
<p>
"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).
</p>
<p>
"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.
</p>
<p>
"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."
</p>
<p>
"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.
</p>
<div class="block">
<h2 class="inline">2. Grant of Copyright License.</h2>
<p class="inline">
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.
</p>
</div>
<div class="block">
<h2 class="inline">3. Grant of Patent License.</h2>
<p class="inline">
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.
</p>
</div>
<div class="block">
<h2 class="inline">4. Redistribution.</h2>
<p class="inline">
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:
</p>
</div>
<ul class="low-alpha">
<li>
You must give any other recipients of the Work or
Derivative Works a copy of this License; and
</li>
<li>
You must cause any modified files to carry prominent notices
stating that You changed the files; and
</li>
<li>
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
</li>
<li>
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.
</li>
</ul>
<p>
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.
</p>
<div class="block">
<h2 class="inline">5. Submission of Contributions.</h2>
<p class="inline">
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.
</p>
</div>
<div class="block">
<h2 class="inline">6. Trademarks.</h2>
<p class="inline">
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.
</p>
</div>
<div class="block">
<h2 class="inline">7. Disclaimer of Warranty.</h2>
<p class="inline">
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.
</p>
</div>
<div class="block">
<h2 class="inline">8. Limitation of Liability.</h2>
<p class="inline">
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.
</p>
</div>
<div class="block">
<h2 class="inline">9. Accepting Warranty or Additional Liability.</h2>
<p class="inline">
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.
</p>
</div>
<p>END OF TERMS AND CONDITIONS</p>
<h1>APPENDIX: How to apply the Apache License to your work.</h1>
<p>
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
</p>
<pre>Copyright [yyyy] [name of copyright owner]&#x000A;&#x000A;Licensed under the Apache License, Version 2.0 (the "License");&#x000A;you may not use this file except in compliance with the License.&#x000A;You may obtain a copy of the License at&#x000A;&#x000A; http://www.apache.org/licenses/LICENSE-2.0&#x000A;&#x000A;Unless required by applicable law or agreed to in writing, software&#x000A;distributed under the License is distributed on an "AS IS" BASIS,&#x000A;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#x000A;See the License for the specific language governing permissions and&#x000A;limitations under the License.</pre>
</div>
</div>
</body>

View File

@ -10,22 +10,15 @@ import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.databinding.ActivityLoginBinding
import com.h.pixeldroid.databinding.ActivityPostCreationBinding
import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.utils.*
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.*
import com.h.pixeldroid.utils.db.addUser
import com.h.pixeldroid.utils.db.storeInstance
import com.h.pixeldroid.utils.hasInternet
import com.h.pixeldroid.utils.normalizeDomain
import com.h.pixeldroid.utils.openUrl
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import okhttp3.HttpUrl
import kotlinx.coroutines.*
import retrofit2.HttpException
import java.io.IOException
import java.lang.IllegalArgumentException
/**
Overview of the flow of the login process: (boxes are requests done in parallel,
@ -104,9 +97,13 @@ class LoginActivity : BaseActivity() {
private fun whatsAnInstance() {
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse("https://pixelfed.org/join")
startActivity(i)
val builder = AlertDialog.Builder(this)
builder.apply {
setView(layoutInflater.inflate(R.layout.whats_an_instance_explanation, null))
setPositiveButton(android.R.string.ok) { _, _ -> }
}
// Create the AlertDialog
builder.show()
}
private fun hideKeyboard() {
@ -121,11 +118,7 @@ class LoginActivity : BaseActivity() {
private fun registerAppToServer(normalizedDomain: String) {
try{
HttpUrl.Builder().host(normalizedDomain.replace("https://", "")).scheme("https").build()
} catch (e: IllegalArgumentException) {
return failedRegistration(getString(R.string.invalid_domain))
}
if(!validDomain(normalizedDomain)) return failedRegistration(getString(R.string.invalid_domain))
hideKeyboard()
loadingAnimation(true)
@ -134,7 +127,6 @@ class LoginActivity : BaseActivity() {
lifecycleScope.launch {
try {
supervisorScope { }
val credentialsDeferred: Deferred<Application?> = async {
try {
pixelfedAPI.registerApplication(
@ -153,7 +145,6 @@ class LoginActivity : BaseActivity() {
val clientId = credentials?.client_id ?: return@launch failedRegistration()
preferences.edit()
.putString("domain", normalizedDomain)
.putString("clientID", clientId)
.putString("clientSecret", credentials.client_secret)
.apply()
@ -177,18 +168,39 @@ class LoginActivity : BaseActivity() {
normalizedDomain: String,
clientId: String,
nodeInfoSchemaUrl: String
) {
val nodeInfo = try {
) = coroutineScope {
val nodeInfo: NodeInfo = try {
pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl)
} catch (exception: IOException) {
return failedRegistration(getString(R.string.instance_error))
return@coroutineScope failedRegistration(getString(R.string.instance_error))
} catch (exception: HttpException) {
return failedRegistration(getString(R.string.instance_error))
return@coroutineScope failedRegistration(getString(R.string.instance_error))
}
val domain: String = try {
if (nodeInfo.hasInstanceEndpointInfo()) {
storeInstance(db, nodeInfo)
nodeInfo.metadata?.config?.site?.url
} else {
val instance: Instance = try {
pixelfedAPI.instance()
} catch (exception: IOException) {
return@coroutineScope failedRegistration(getString(R.string.instance_error))
} catch (exception: HttpException) {
return@coroutineScope failedRegistration(getString(R.string.instance_error))
}
storeInstance(db, nodeInfo = null, instance = instance)
instance.uri
}
} catch (e: IllegalArgumentException){ null }
?: return@coroutineScope failedRegistration(getString(R.string.instance_error))
preferences.edit().putString("domain", normalizeDomain(domain)).apply()
if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) {
val builder = AlertDialog.Builder(this@LoginActivity)
builder.apply {
AlertDialog.Builder(this@LoginActivity).apply {
setMessage(R.string.instance_not_pixelfed_warning)
setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ ->
promptOAuth(normalizedDomain, clientId)
@ -197,13 +209,18 @@ class LoginActivity : BaseActivity() {
loadingAnimation(false)
wipeSharedSettings()
}
}
// Create the AlertDialog
builder.show()
}.show()
} else if (nodeInfo.metadata?.config?.features?.mobile_apis != true) {
AlertDialog.Builder(this@LoginActivity).apply {
setMessage(R.string.api_not_enabled_dialog)
setNegativeButton(android.R.string.ok) { _, _ ->
loadingAnimation(false)
wipeSharedSettings()
}
}.show()
} else {
promptOAuth(normalizedDomain, clientId)
}
}
@ -234,9 +251,6 @@ class LoginActivity : BaseActivity() {
lifecycleScope.launch {
try {
val instanceDeferred = async {
pixelfedAPI.instance()
}
val token = pixelfedAPI.obtainToken(
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code,
"authorization_code"
@ -244,20 +258,12 @@ class LoginActivity : BaseActivity() {
if (token.access_token == null) {
return@launch failedRegistration(getString(R.string.token_error))
}
val instance = instanceDeferred.await()
if (instance.uri == null) {
return@launch failedRegistration(getString(R.string.instance_error))
}
storeInstance(db, instance)
storeUser(
token.access_token,
token.refresh_token,
clientId,
clientSecret,
instance.uri
domain
)
wipeSharedSettings()
} catch (exception: IOException) {

View File

@ -61,15 +61,16 @@ class MainActivity : BaseActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
TraceDroidEmailSender.sendStackTraces("contact@pixeldroid.org", this)
//get the currently active user
user = db.userDao().getActiveUser()
//Check if we have logged in and gotten an access token
if (user == null) {
launchActivity(LoginActivity(), firstTime = true)
finish()
} else {
TraceDroidEmailSender.sendStackTraces("contact@pixeldroid.org", this)
setupDrawer()
val tabs: List<() -> Fragment> = listOf(

View File

@ -1,10 +1,8 @@
package com.h.pixeldroid.postCreation
import android.app.Activity
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.app.AlertDialog
import android.content.*
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
@ -16,26 +14,23 @@ import android.util.Log
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.Button
import android.widget.ImageButton
import android.widget.Toast
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.MainActivity
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.ActivityPostCreationBinding
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.postCreation.camera.CameraActivity
import com.h.pixeldroid.postCreation.carousel.CarouselItem
import com.h.pixeldroid.postCreation.carousel.ImageCarousel
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.utils.api.objects.Attachment
import com.h.pixeldroid.utils.api.objects.Instance
import com.h.pixeldroid.postCreation.photoEdit.PhotoEditActivity
import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Attachment
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
@ -47,14 +42,18 @@ import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.ceil
import kotlin.properties.Delegates
private const val TAG = "Post Creation Activity"
private const val MORE_PICTURES_REQUEST_CODE = 0xffff
data class PhotoData(
var imageUri: Uri,
var size: Long,
var uploadId: String? = null,
var progress: Int? = null
var progress: Int? = null,
var imageDescription: String? = null,
)
class PostCreationActivity : BaseActivity() {
@ -64,6 +63,7 @@ class PostCreationActivity : BaseActivity() {
private var positionResult = 0
private var user: UserDatabaseEntity? = null
private lateinit var instance: InstanceDatabaseEntity
private val photoData: ArrayList<PhotoData> = ArrayList()
@ -74,48 +74,40 @@ class PostCreationActivity : BaseActivity() {
binding = ActivityPostCreationBinding.inflate(layoutInflater)
setContentView(binding.root)
// get image URIs
if(intent.clipData != null) {
val count = intent.clipData!!.itemCount
for (i in 0 until count) {
intent.clipData!!.getItemAt(i).uri.let {
photoData.add(PhotoData(it))
}
}
}
user = db.userDao().getActiveUser()
val instances = db.instanceDao().getAll()
instance = user?.run {
db.instanceDao().getAll().first { instanceDatabaseEntity ->
instanceDatabaseEntity.uri.contains(instance_uri)
}
} ?: InstanceDatabaseEntity("", "")
binding.postTextInputLayout.counterMaxLength = if (user != null){
val thisInstances =
instances.filter { instanceDatabaseEntity ->
instanceDatabaseEntity.uri.contains(user!!.instance_uri)
}
thisInstances.first().max_toot_chars
} else {
Instance.DEFAULT_MAX_TOOT_CHARS
}
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
// get image URIs
intent.clipData?.let { addPossibleImages(it) }
accessToken = user?.accessToken.orEmpty()
pixelfedAPI = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
val carousel: ImageCarousel = binding.carousel
carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) })
carousel.addData(photoData.map { CarouselItem(it.imageUri) })
carousel.layoutCarouselCallback = {
//TODO transition instead of at once
if(it){
// Became a carousel
binding.toolbar3.visibility = VISIBLE
binding.toolbarPostCreation.visibility = VISIBLE
} else {
// Became a grid
binding.toolbar3.visibility = INVISIBLE
binding.toolbarPostCreation.visibility = INVISIBLE
}
}
carousel.maxEntries = instance.albumLimit
carousel.addPhotoButtonCallback = {
addPhoto(applicationContext)
}
carousel.updateDescriptionCallback = { position: Int, description: String ->
photoData[position].imageDescription = description
}
// get the description and send the post
binding.postCreationSendButton.setOnClickListener {
@ -152,7 +144,64 @@ class PostCreationActivity : BaseActivity() {
binding.removePhotoButton.setOnClickListener {
carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition ->
photoData.removeAt(currentPosition)
carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) })
carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription) })
binding.addPhotoButton.isEnabled = true
}
}
}
/**
* Will add as many images as possible to [photoData], from the [clipData], and if
* ([photoData].size + [clipData].itemCount) > [albumLimit] then it will only add as many images
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
*/
private fun addPossibleImages(clipData: ClipData){
var count = clipData.itemCount
if(count + photoData.size > instance.albumLimit){
AlertDialog.Builder(this).apply {
setMessage(getString(R.string.total_exceeds_album_limit).format(instance.albumLimit))
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
count = count.coerceAtMost(instance.albumLimit - photoData.size)
}
if (count + photoData.size >= instance.albumLimit) {
// Disable buttons to add more images
binding.addPhotoButton.isEnabled = false
}
for (i in 0 until count) {
clipData.getItemAt(i).uri.let {
val size: Long =
if (it.toString().startsWith("content")) {
contentResolver.query(it, null, null, null, null)
?.use { cursor ->
/* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data,
* and display it.
*/
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
cursor.getLong(sizeIndex)
} ?: 0
} else {
it.toFile().length()
}
val sizeInkBytes = ceil(size.toDouble() / 1000).toLong()
if(sizeInkBytes > instance.maxPhotoSize || sizeInkBytes > instance.maxVideoSize){
val maxSize = when {
instance.maxPhotoSize != instance.maxVideoSize -> {
val type = contentResolver.getType(it)
if(type?.startsWith("video/") == true){
instance.maxVideoSize
} else instance.maxPhotoSize
}
else -> instance.maxPhotoSize
}
AlertDialog.Builder(this).apply {
setMessage(getString(R.string.size_exceeds_instance_limit).format(photoData.size + 1, sizeInkBytes, maxSize))
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
}
photoData.add(PhotoData(imageUri = it, size = size))
}
}
}
@ -177,21 +226,21 @@ class PostCreationActivity : BaseActivity() {
if(path.startsWith("file")) {
MediaScannerConnection.scanFile(
this,
arrayOf(path.toUri().toFile().absolutePath),
null
this,
arrayOf(path.toUri().toFile().absolutePath),
null
) { path, uri ->
if (uri == null) {
Log.e(
"NEW IMAGE SCAN FAILED",
"Tried to scan $path, but it failed"
"NEW IMAGE SCAN FAILED",
"Tried to scan $path, but it failed"
)
}
}
}
Snackbar.make(
button, getString(R.string.save_image_success),
Snackbar.LENGTH_LONG
button, getString(R.string.save_image_success),
Snackbar.LENGTH_LONG
).show()
}
@ -204,8 +253,8 @@ class PostCreationActivity : BaseActivity() {
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
contentValues.put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES
)
val imageUri: Uri =
resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)!!
@ -228,7 +277,7 @@ class PostCreationActivity : BaseActivity() {
val content = editText?.length() ?: 0
if (content > counterMaxLength) {
// error, too many characters
error = getString(R.string.description_max_characters).format(counterMaxLength)
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
return false
}
}
@ -252,23 +301,7 @@ class PostCreationActivity : BaseActivity() {
val imageUri = data.imageUri
val imageInputStream = contentResolver.openInputStream(imageUri)!!
val size =
if (imageUri.toString().startsWith("content")) {
contentResolver.query(imageUri, null, null, null, null)
?.use { cursor ->
/* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data,
* and display it.
*/
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
cursor.getLong(sizeIndex)
} ?: 0
} else {
imageUri.toFile().length()
}
val imagePart = ProgressRequestBody(imageInputStream, size)
val imagePart = ProgressRequestBody(imageInputStream, data.size)
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", System.currentTimeMillis().toString(), imagePart)
@ -283,32 +316,43 @@ class PostCreationActivity : BaseActivity() {
}
var postSub: Disposable? = null
val inter = pixelfedAPI.mediaUpload("Bearer $accessToken", requestBody.parts[0])
val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) }
val inter = pixelfedAPI.mediaUpload("Bearer $accessToken", description, requestBody.parts[0])
postSub = inter
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ attachment: Attachment ->
data.progress = 0
data.uploadId = attachment.id!!
},
{ e ->
binding.uploadError.visibility = View.VISIBLE
e.printStackTrace()
postSub?.dispose()
sub.dispose()
},
{
data.progress = 100
if(photoData.all{it.progress == 100}){
binding.uploadProgressBar.visibility = View.GONE
binding.uploadCompletedTextview.visibility = View.VISIBLE
post()
{ attachment: Attachment ->
data.progress = 0
data.uploadId = attachment.id!!
},
{ e: Throwable ->
binding.uploadError.visibility = View.VISIBLE
if(e is HttpException){
binding.uploadErrorTextExplanation.text =
getString(R.string.upload_error).format(e.code())
binding.uploadErrorTextExplanation.visibility= VISIBLE
} else {
binding.uploadErrorTextExplanation.visibility= View.GONE
}
e.printStackTrace()
postSub?.dispose()
sub.dispose()
},
{
data.progress = 100
if (photoData.all { it.progress == 100 && it.uploadId != null }) {
binding.uploadProgressBar.visibility = View.GONE
binding.uploadCompletedTextview.visibility = View.VISIBLE
post()
}
postSub?.dispose()
sub.dispose()
}
postSub?.dispose()
sub.dispose()
}
)
}
}
@ -319,23 +363,23 @@ class PostCreationActivity : BaseActivity() {
lifecycleScope.launchWhenCreated {
try {
pixelfedAPI.postStatus(
authorization = "Bearer $accessToken",
statusText = description,
media_ids = photoData.mapNotNull { it.uploadId }.toList()
authorization = "Bearer $accessToken",
statusText = description,
media_ids = photoData.mapNotNull { it.uploadId }.toList()
)
Toast.makeText(applicationContext,getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
Toast.makeText(applicationContext, getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
val intent = Intent(this@PostCreationActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
} catch (exception: IOException) {
Toast.makeText(applicationContext,getString(R.string.upload_post_error),
Toast.LENGTH_SHORT).show()
Toast.makeText(applicationContext, getString(R.string.upload_post_error),
Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.toString())
enableButton(true)
} catch (exception: HttpException) {
Toast.makeText(applicationContext,getString(R.string.upload_post_failed),
Toast.LENGTH_SHORT).show()
Toast.makeText(applicationContext, getString(R.string.upload_post_failed),
Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.response().toString() + exception.message().toString())
enableButton(true)
}
@ -369,7 +413,7 @@ class PostCreationActivity : BaseActivity() {
if (resultCode == Activity.RESULT_OK && data != null) {
photoData[positionResult].imageUri = data.getStringExtra("result")!!.toUri()
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) })
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription) })
photoData[positionResult].progress = null
photoData[positionResult].uploadId = null
@ -377,15 +421,13 @@ class PostCreationActivity : BaseActivity() {
Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
}
} else if (requestCode == MORE_PICTURES_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK && data?.clipData != null) {
val count = data.clipData!!.itemCount
for (i in 0 until count) {
val imageUri: Uri = data.clipData!!.getItemAt(i).uri
photoData.add(PhotoData(imageUri))
if (resultCode == Activity.RESULT_OK && data?.clipData != null) {
data.clipData?.let {
addPossibleImages(it)
}
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) })
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription) })
} else if(resultCode != Activity.RESULT_CANCELED){
Toast.makeText(applicationContext, "Error while adding images", Toast.LENGTH_SHORT).show()
}

View File

@ -13,18 +13,17 @@ import com.h.pixeldroid.R
class CarouselAdapter(
@LayoutRes private val itemLayout: Int,
@IdRes private val imageViewId: Int,
var listener: OnItemClickListener? = null,
private val imageScaleType: ImageView.ScaleType,
private val imagePlaceholder: Drawable?,
private val carousel: Boolean
@LayoutRes private val itemLayout: Int,
@IdRes private val imageViewId: Int,
var listener: OnItemClickListener? = null,
private val imageScaleType: ImageView.ScaleType,
private val imagePlaceholder: Drawable?,
private val carousel: Boolean,
var maxEntries: Int?,
) : RecyclerView.Adapter<CarouselAdapter.MyViewHolder>() {
private val dataList: MutableList<CarouselItem> = mutableListOf()
class MyViewHolder(itemView: View, imageViewId: Int) : RecyclerView.ViewHolder(itemView) {
var img: ImageView = itemView.findViewById(imageViewId)
}
@ -51,6 +50,7 @@ class CarouselAdapter(
override fun getItemCount(): Int {
return if(carousel) dataList.size
else if (maxEntries != null && dataList.size >= maxEntries!!) maxEntries!!
else dataList.size + 1
}
@ -95,6 +95,11 @@ class CarouselAdapter(
}
}
fun updateDescription(position: Int, description: String) {
dataList[position] = dataList[position].copy(caption = description)
notifyItemChanged(position)
}
fun addAll(dataList: List<CarouselItem>) {
this.dataList.clear()

View File

@ -1,8 +1,10 @@
package com.h.pixeldroid.postCreation.carousel
import android.net.Uri
data class CarouselItem constructor(
val imageUrl: String? = null,
val caption: String? = null
val imageUrl: Uri,
val caption: String? = null
) {
constructor(imageUrl: String? = null) : this(imageUrl, null)
constructor(imageUrl: Uri) : this(imageUrl, null)
}

View File

@ -8,10 +8,7 @@ import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import android.widget.*
import androidx.annotation.Dimension
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
@ -19,6 +16,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.*
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.ImageCarouselBinding
import me.relex.circleindicator.CircleIndicator2
import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable
@ -31,6 +29,8 @@ class ImageCarousel(
private var adapter: CarouselAdapter? = null
private lateinit var binding: ImageCarouselBinding
private val scaleTypeArray = arrayOf(
ImageView.ScaleType.MATRIX,
ImageView.ScaleType.FIT_XY,
@ -42,11 +42,9 @@ class ImageCarousel(
ImageView.ScaleType.CENTER_INSIDE
)
private lateinit var carouselView: View
private lateinit var recyclerView: RecyclerView
private lateinit var tvCaption: TextView
private lateinit var previousButtonContainer: FrameLayout
private lateinit var nextButtonContainer: FrameLayout
private lateinit var editTextMediaDescription: EditText
private var snapHelper: SnapHelper = PagerSnapHelper()
var indicator: CircleIndicator2? = null
@ -151,9 +149,9 @@ class ImageCarousel(
set(value) {
field = value
previousButtonContainer.visibility =
binding.btnPrevious.visibility =
if (showNavigationButtons) View.VISIBLE else View.GONE
nextButtonContainer.visibility =
binding.btnNext.visibility =
if (showNavigationButtons) View.VISIBLE else View.GONE
}
@ -194,69 +192,19 @@ class ImageCarousel(
initAdapter()
}
@LayoutRes
var previousButtonLayout: Int = R.layout.previous_button_layout
set(value) {
field = value
btnPrevious = null
previousButtonContainer.removeAllViews()
LayoutInflater.from(context).apply {
inflate(previousButtonLayout, previousButtonContainer, true)
}
}
@IdRes
var previousButtonId: Int = R.id.btn_next
set(value) {
field = value
btnPrevious = carouselView.findViewById(previousButtonId)
btnPrevious?.setOnClickListener {
previous()
}
}
@Dimension(unit = Dimension.PX)
var previousButtonMargin: Int = 0
set(value) {
field = value
val previousButtonParams = previousButtonContainer.layoutParams as LayoutParams
val previousButtonParams = binding.btnPrevious.layoutParams as LayoutParams
previousButtonParams.setMargins(
previousButtonMargin,
0,
0,
0
)
previousButtonContainer.layoutParams = previousButtonParams
}
@LayoutRes
var nextButtonLayout: Int = R.layout.next_button_layout
set(value) {
field = value
btnNext = null
nextButtonContainer.removeAllViews()
LayoutInflater.from(context).apply {
inflate(nextButtonLayout, nextButtonContainer, true)
}
}
@IdRes
var nextButtonId: Int = R.id.btn_previous
set(value) {
field = value
btnNext = carouselView.findViewById(nextButtonId)
btnNext?.setOnClickListener {
next()
}
binding.btnPrevious.layoutParams = previousButtonParams
}
@Dimension(unit = Dimension.PX)
@ -264,22 +212,22 @@ class ImageCarousel(
set(value) {
field = value
val nextButtonParams = nextButtonContainer.layoutParams as LayoutParams
val nextButtonParams = binding.btnNext.layoutParams as LayoutParams
nextButtonParams.setMargins(
0,
0,
nextButtonMargin,
0
)
nextButtonContainer.layoutParams = nextButtonParams
binding.btnNext.layoutParams = nextButtonParams
}
var showLayoutSwitchButton: Boolean = true
set(value) {
field = value
btnGrid = findViewById<ImageButton>(R.id.switchToGridButton)
btnCarousel = findViewById<ImageButton>(R.id.switchToCarouselButton)
btnGrid = binding.switchToGridButton
btnCarousel = binding.switchToCarouselButton
btnGrid?.setOnClickListener {
layoutCarousel = false
@ -304,6 +252,9 @@ class ImageCarousel(
var layoutCarouselCallback: ((Boolean) -> Unit)? = null
var updateDescriptionCallback: ((position: Int, description: String) -> Unit)? = null
var layoutCarousel: Boolean = true
set(value) {
field = value
@ -313,10 +264,16 @@ class ImageCarousel(
btnNext?.visibility = VISIBLE
btnPrevious?.visibility = VISIBLE
binding.editMediaDescriptionLayout.visibility = if(editingMediaDescription) VISIBLE else INVISIBLE
tvCaption.visibility = if(editingMediaDescription) INVISIBLE else VISIBLE
} else {
recyclerView.layoutManager = GridLayoutManager(context, 3)
btnNext?.visibility = GONE
btnPrevious?.visibility = GONE
binding.editMediaDescriptionLayout.visibility = INVISIBLE
tvCaption.visibility = INVISIBLE
}
showIndicator = value
@ -330,6 +287,43 @@ class ImageCarousel(
var addPhotoButtonCallback: (() -> Unit)? = null
var editingMediaDescription: Boolean = false
set(value){
if(layoutCarousel){
field = value
if(value) editTextMediaDescription.setText(currentDescription)
else {
val description = editTextMediaDescription.text.toString()
currentDescription = description
adapter?.updateDescription(currentPosition, description)
updateDescriptionCallback?.invoke(currentPosition, description)
}
binding.editMediaDescriptionLayout.visibility = if(value) VISIBLE else INVISIBLE
tvCaption.visibility = if(value) INVISIBLE else VISIBLE
}
}
var currentDescription: String? = null
set(value) {
if(!value.isNullOrEmpty()) {
field = value
tvCaption.text = value
} else {
field = null
tvCaption.text = context.getText(R.string.no_media_description)
}
}
var maxEntries: Int? = null
set(value){
field = value
adapter?.maxEntries = value
}
init {
@ -341,12 +335,11 @@ class ImageCarousel(
private fun initViews() {
carouselView = LayoutInflater.from(context).inflate(R.layout.image_carousel, this)
binding = ImageCarouselBinding.inflate(LayoutInflater.from(context),this, true)
recyclerView = carouselView.findViewById(R.id.recyclerView)
tvCaption = carouselView.findViewById(R.id.tv_caption)
previousButtonContainer = carouselView.findViewById(R.id.previous_button_container)
nextButtonContainer = carouselView.findViewById(R.id.next_button_container)
recyclerView = binding.recyclerView
tvCaption = binding.tvCaption
editTextMediaDescription = binding.editTextMediaDescription
recyclerView.setHasFixedSize(true)
@ -403,16 +396,8 @@ class ImageCarousel(
R.id.img
)
previousButtonLayout = R.layout.previous_button_layout
previousButtonId = R.id.btn_previous
previousButtonMargin = 4.dpToPx(context)
nextButtonLayout = R.layout.next_button_layout
nextButtonId = R.id.btn_next
nextButtonMargin = 4.dpToPx(context)
showNavigationButtons = getBoolean(
@ -440,12 +425,13 @@ class ImageCarousel(
private fun initAdapter() {
adapter = CarouselAdapter(
itemLayout = itemLayout,
imageViewId = imageViewId,
listener = onItemClickListener,
imageScaleType = imageScaleType,
imagePlaceholder = imagePlaceholder,
carousel = layoutCarousel
itemLayout = itemLayout,
imageViewId = imageViewId,
listener = onItemClickListener,
imageScaleType = imageScaleType,
imagePlaceholder = imagePlaceholder,
carousel = layoutCarousel,
maxEntries = maxEntries
)
recyclerView.adapter = adapter
@ -474,7 +460,13 @@ class ImageCarousel(
val dataItem = adapter?.getItem(position)
dataItem?.apply {
tvCaption.text = this.caption
caption.apply {
if(layoutCarousel){
binding.editMediaDescriptionLayout.visibility = INVISIBLE
tvCaption.visibility = VISIBLE
}
currentDescription = this
}
}
}
}
@ -499,12 +491,27 @@ class ImageCarousel(
}
})
tvCaption.setOnClickListener {
editingMediaDescription = true
}
binding.btnNext.setOnClickListener {
next()
}
binding.btnPrevious.setOnClickListener {
previous()
}
binding.imageDescriptionButton.setOnClickListener {
editingMediaDescription = false
}
}
private fun initIndicator() {
// If no custom indicator added, then default indicator will be shown.
if (indicator == null) {
indicator = carouselView.findViewById(R.id.indicator)
indicator = binding.indicator
isBuiltInIndicator = true
}

View File

@ -49,7 +49,7 @@ private val REQUIRED_PERMISSIONS = arrayOf(
class PhotoEditActivity : BaseActivity() {
private var saving: Boolean = false
var saving: Boolean = false
private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
private val BRIGHTNESS_START = 0
private val SATURATION_START = 1.0f
@ -319,6 +319,7 @@ class PhotoEditActivity : BaseActivity() {
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(grantResults.size > 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {

View File

@ -0,0 +1,113 @@
/*
* Copyright 2019 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.
*/
package com.h.pixeldroid.posts
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
import kotlin.math.absoluteValue
import kotlin.math.sign
/**
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
*
* This solution has limitations when using multiple levels of nested scrollable elements
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
*/
class NestedScrollableHost : ConstraintLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() {
var v: View? = parent as? View
while (v != null && v !is ViewPager2) {
v = v.parent as? View
}
return v as? ViewPager2
}
private val child: View? get() = if (childCount > 0) getChildAt(0) else null
init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
}

View File

@ -1,27 +1,36 @@
package com.h.pixeldroid.posts
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
import android.widget.LinearLayout
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.ActivityPostBinding
import com.h.pixeldroid.utils.api.objects.DiscoverPost
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.api.objects.Status.Companion.DISCOVER_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.DOMAIN_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.databinding.CommentBinding
import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Mention
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_COMMENT_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG
import com.h.pixeldroid.utils.displayDimensionsInPx
import retrofit2.HttpException
import java.io.IOException
class PostActivity : BaseActivity() {
private lateinit var postFragment : PostFragment
lateinit var domain : String
private lateinit var accessToken : String
private lateinit var binding: ActivityPostBinding
private lateinit var status: Status
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPostBinding.inflate(layoutInflater)
@ -29,23 +38,40 @@ class PostActivity : BaseActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val status = intent.getSerializableExtra(POST_TAG) as Status?
val discoverPost: DiscoverPost? = intent.getSerializableExtra(DISCOVER_TAG) as DiscoverPost?
status = intent.getSerializableExtra(POST_TAG) as Status
val viewComments: Boolean = (intent.getSerializableExtra(VIEW_COMMENTS_TAG) ?: false) as Boolean
val postComment: Boolean = (intent.getSerializableExtra(POST_COMMENT_TAG) ?: false) as Boolean
val user = db.userDao().getActiveUser()
domain = user?.instance_uri.orEmpty()
accessToken = user?.accessToken.orEmpty()
postFragment = PostFragment()
val arguments = Bundle()
arguments.putString(DOMAIN_TAG, domain)
if (discoverPost != null) {
binding.postProgressBar.visibility = View.VISIBLE
getDiscoverPost(arguments, discoverPost)
} else {
initializeFragment(arguments, status)
supportActionBar?.title = getString(R.string.post_title).format(status.account?.getDisplayName())
val holder = StatusViewHolder(binding.postFragmentSingle)
holder.bind(status, apiHolder.api!!, db, lifecycleScope, displayDimensionsInPx(), isActivity = true)
val credential = "Bearer $accessToken"
activateCommenter(credential)
if(viewComments || postComment){
//Scroll already down as much as possible (since comments are not loaded yet)
binding.scrollview.requestChildFocus(binding.editComment, binding.editComment)
//Open keyboard if we want to post a comment
if(postComment && binding.editComment.requestFocus()) {
window.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
binding.editComment.requestFocus()
}
// also retrieve comments if we're not posting the comment
if(!postComment) retrieveComments(apiHolder.api!!, credential)
}
binding.postFragmentSingle.viewComments.setOnClickListener {
retrieveComments(apiHolder.api!!, credential)
}
}
@ -54,32 +80,109 @@ class PostActivity : BaseActivity() {
return true
}
private fun getDiscoverPost(
arguments: Bundle,
discoverPost: DiscoverPost
) {
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
val id = discoverPost.url?.substringAfterLast('/') ?: ""
lifecycleScope.launchWhenCreated {
try {
val status = api.getStatus("Bearer $accessToken", id)
binding.postProgressBar.visibility = View.GONE
initializeFragment(arguments, status)
} catch (exception: IOException) {
//TODO show error message
Log.e("PostActivity:", exception.toString())
} catch (exception: HttpException) {
private fun activateCommenter(credential: String) {
//Activate commenter
binding.submitComment.setOnClickListener {
val textIn = binding.editComment.text
//Open text input
if(textIn.isNullOrEmpty()) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.empty_comment),
Toast.LENGTH_SHORT
).show()
} else {
//Post the comment
lifecycleScope.launchWhenCreated {
apiHolder.api?.let { it1 -> postComment(it1, credential) }
}
}
}
}
private fun initializeFragment(arguments: Bundle, status: Status?){
supportActionBar?.title = getString(R.string.post_title).format(status!!.account?.getDisplayName())
arguments.putSerializable(POST_TAG, status)
postFragment.arguments = arguments
supportFragmentManager.isStateSaved
supportFragmentManager.beginTransaction()
.add(R.id.postFragmentSingle, postFragment).commit()
binding.postFragmentSingle.visibility = View.VISIBLE
private fun addComment(context: Context, commentContainer: LinearLayout,
commentUsername: String, commentContent: String, mentions: List<Mention>,
credential: String) {
val itemBinding = CommentBinding.inflate(
LayoutInflater.from(context), commentContainer, true
)
itemBinding.user.text = commentUsername
itemBinding.commentText.text = parseHTMLText(
commentContent,
mentions,
apiHolder.api!!,
context,
credential,
lifecycleScope
)
}
private fun retrieveComments(api: PixelfedAPI, credential: String) {
lifecycleScope.launchWhenCreated {
status.id.let {
try {
val statuses = api.statusComments(it, credential).descendants
binding.commentContainer.removeAllViews()
//Create the new views for each comment
for (status in statuses) {
addComment(binding.root.context, binding.commentContainer, status.account!!.username!!,
status.content!!, status.mentions.orEmpty(), credential
)
}
binding.commentContainer.visibility = View.VISIBLE
//Focus the comments
binding.scrollview.requestChildFocus(binding.commentContainer, binding.commentContainer)
} catch (exception: IOException) {
Log.e("COMMENT FETCH ERROR", exception.toString())
} catch (exception: HttpException) {
Log.e("COMMENT ERROR", "${exception.code()} with body ${exception.response()?.errorBody()}")
}
}
}
}
private suspend fun postComment(
api: PixelfedAPI,
credential: String,
) {
val textIn = binding.editComment.text
val nonNullText = textIn.toString()
status.id.let {
try {
val response = api.postStatus(credential, nonNullText, it)
binding.commentIn.visibility = View.GONE
//Add the comment to the comment section
addComment(
binding.root.context, binding.commentContainer, response.account!!.username!!,
response.content!!, response.mentions.orEmpty(), credential
)
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.comment_posted).format(textIn),
Toast.LENGTH_SHORT
).show()
} catch (exception: IOException) {
Log.e("COMMENT ERROR", exception.toString())
Toast.makeText(
binding.root.context, binding.root.context.getString(R.string.comment_error),
Toast.LENGTH_SHORT
).show()
} catch (exception: HttpException) {
Toast.makeText(
binding.root.context, binding.root.context.getString(R.string.comment_error),
Toast.LENGTH_SHORT
).show()
Log.e("ERROR_CODE", exception.code().toString())
}
}
}
}

View File

@ -1,49 +0,0 @@
package com.h.pixeldroid.posts
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.databinding.PostFragmentBinding
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.api.objects.Status.Companion.DOMAIN_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.utils.BaseFragment
import com.h.pixeldroid.utils.bindingLifecycleAware
class PostFragment : BaseFragment() {
private lateinit var statusDomain: String
private var currentStatus: Status? = null
var binding: PostFragmentBinding by bindingLifecycleAware()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
currentStatus = arguments?.getSerializable(POST_TAG) as Status?
statusDomain = arguments?.getString(DOMAIN_TAG)!!
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = PostFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
val holder = StatusViewHolder(binding)
holder.bind(currentStatus, api, db, lifecycleScope)
}
}

View File

@ -3,9 +3,7 @@ package com.h.pixeldroid.posts
import android.Manifest
import android.app.AlertDialog
import android.content.Intent
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.text.method.LinkMovementMethod
import android.util.Log
@ -19,15 +17,17 @@ import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.AlbumImageViewBinding
import com.h.pixeldroid.databinding.CommentBinding
import com.h.pixeldroid.databinding.PostFragmentBinding
import com.h.pixeldroid.utils.BlurHashDecoder
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Attachment
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_COMMENT_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG
import com.h.pixeldroid.utils.db.AppDatabase
import com.karumi.dexter.Dexter
import com.karumi.dexter.listener.PermissionDeniedResponse
@ -36,6 +36,7 @@ import com.karumi.dexter.listener.single.BasePermissionListener
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
import kotlin.math.roundToInt
/**
@ -45,25 +46,36 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
private var status: Status? = null
fun bind(status: Status?, pixelfedAPI: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope) {
fun bind(status: Status?, pixelfedAPI: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair<Int, Int>, isActivity: Boolean = false) {
this.itemView.visibility = View.VISIBLE
this.status = status
val metrics = itemView.context.resources.displayMetrics
//Limit the height of the different images
binding.postPicture.maxHeight = metrics.heightPixels * 3/4
val maxImageRatio: Float = status?.media_attachments?.map {
if (it.meta?.original?.width == null || it.meta.original.height == null) {
1f
} else {
it.meta.original.width.toFloat() / it.meta.original.height.toFloat()
}
}?.maxOrNull() ?: 1f
val (displayWidth, displayHeight) = displayDimensionsInPx
if (displayWidth / maxImageRatio > displayHeight * 3/4f) {
binding.postPager.layoutParams.width = ((displayHeight * 3 / 4f) * maxImageRatio).roundToInt()
binding.postPager.layoutParams.height = (displayHeight * 3 / 4f).toInt()
} else {
binding.postPager.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
binding.postPager.layoutParams.height = (displayWidth / maxImageRatio).toInt()
}
//Setup the post layout
val picRequest = Glide.with(itemView)
.asDrawable().fitCenter()
.placeholder(ColorDrawable(Color.GRAY))
val picRequest = Glide.with(itemView).asDrawable().fitCenter()
val user = db.userDao().getActiveUser()!!
setupPost(picRequest, user.instance_uri, false)
setupPost(picRequest, user.instance_uri, isActivity)
activateButtons(pixelfedAPI, db, lifecycleScope)
activateButtons(pixelfedAPI, db, lifecycleScope, isActivity)
}
@ -76,7 +88,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
binding.username.apply {
text = status?.account?.getDisplayName() ?: ""
setTypeface(null, Typeface.BOLD)
setOnClickListener { status?.account?.openProfile(rootView.context) }
setOnClickListener { status?.account?.openProfile(binding.root.context) }
}
binding.usernameDesc.apply {
@ -85,12 +97,12 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
}
binding.nlikes.apply {
text = status?.getNLikes(rootView.context)
text = status?.getNLikes(binding.root.context)
setTypeface(null, Typeface.BOLD)
}
binding.nshares.apply {
text = status?.getNShares(rootView.context)
text = status?.getNShares(binding.root.context)
setTypeface(null, Typeface.BOLD)
}
@ -116,15 +128,9 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
if(!status?.media_attachments.isNullOrEmpty()) {
setupPostPics(binding, request)
} else {
binding.postPicture.visibility = View.GONE
binding.postPager.visibility = View.GONE
binding.postTabs.visibility = View.GONE
binding.postIndicator.visibility = View.GONE
}
//Set comment initial visibility
binding.commentIn.visibility = View.GONE
binding.commentContainer.visibility = View.GONE
}
private fun setupPostPics(
@ -133,50 +139,44 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
) {
// Standard layout
binding.postPicture.visibility = View.VISIBLE
binding.postPager.visibility = View.GONE
binding.postTabs.visibility = View.GONE
binding.postPager.visibility = View.VISIBLE
//Attach the given tabs to the view pager
binding.postPager.adapter = AlbumViewPagerAdapter(status?.media_attachments ?: emptyList(), status?.sensitive)
if(status?.media_attachments?.size == 1) {
request.load(status?.getPostUrl()).into(binding.postPicture)
val imgDescription = status?.media_attachments?.get(0)?.description.orEmpty().ifEmpty { binding.root.context.getString(
R.string.no_description) }
binding.postPicture.contentDescription = imgDescription
binding.postPicture.setOnLongClickListener {
Snackbar.make(it, imgDescription, Snackbar.LENGTH_SHORT).show()
true
}
} else if(status?.media_attachments?.size!! > 1) {
setupTabsLayout(binding, request)
if(status?.media_attachments?.size ?: 0 > 1) {
binding.postIndicator.setViewPager(binding.postPager)
binding.postIndicator.visibility = View.VISIBLE
} else {
binding.postIndicator.visibility = View.GONE
}
if (status?.sensitive!!) {
status?.setupSensitiveLayout(binding)
if (status?.sensitive == true) {
setupSensitiveLayout()
} else {
// GONE is the default, but have to set it again because of how RecyclerViews work
binding.sensitiveWarning.visibility = View.GONE
}
}
private fun setupTabsLayout(
binding: PostFragmentBinding,
request: RequestBuilder<Drawable>,
) {
//Only show the viewPager and tabs
binding.postPicture.visibility = View.GONE
binding.postPager.visibility = View.VISIBLE
binding.postTabs.visibility = View.VISIBLE
//Attach the given tabs to the view pager
binding.postPager.adapter = AlbumViewPagerAdapter(status?.media_attachments ?: emptyList())
private fun setupSensitiveLayout() {
TabLayoutMediator(binding.postTabs, binding.postPager) { tab, _ ->
tab.icon = ContextCompat.getDrawable(binding.root.context, R.drawable.ic_dot_blue_12dp)
}.attach()
// Set dark layout and warning message
binding.sensitiveWarning.visibility = View.VISIBLE
//binding.postPicture.colorFilter = ColorMatrixColorFilter(censorMatrix)
fun uncensorPicture(binding: PostFragmentBinding) {
binding.sensitiveWarning.visibility = View.GONE
(binding.postPager.adapter as AlbumViewPagerAdapter).uncensor()
}
binding.sensitiveWarning.setOnClickListener {
uncensorPicture(binding)
}
}
private fun setDescription(
rootView: View,
api: PixelfedAPI,
credential: String,
lifecycleScope: LifecycleCoroutineScope
@ -189,7 +189,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
status?.content.orEmpty(),
status?.mentions,
api,
rootView.context,
binding.root.context,
credential,
lifecycleScope
)
@ -197,13 +197,18 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
}
}
}
private fun activateButtons(api: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){
//region buttons
private fun activateButtons(
api: PixelfedAPI,
db: AppDatabase,
lifecycleScope: LifecycleCoroutineScope,
isActivity: Boolean
){
val user = db.userDao().getActiveUser()!!
val credential = "Bearer ${user.accessToken}"
//Set the special HTML text
setDescription(binding.root, api, credential, lifecycleScope)
setDescription(api, credential, lifecycleScope)
//Activate onclickListeners
activateLiker(
@ -214,9 +219,23 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
api, credential, status?.reblogged ?: false,
lifecycleScope
)
activateCommenter(api, credential, lifecycleScope)
showComments(api, credential, lifecycleScope)
if(isActivity){
binding.commenter.visibility = View.INVISIBLE
}
else {
binding.commenter.setOnClickListener {
lifecycleScope.launchWhenCreated {
//Open status in activity
val intent = Intent(it.context, PostActivity::class.java)
intent.putExtra(POST_TAG, status)
intent.putExtra(POST_COMMENT_TAG, true)
it.context.startActivity(intent)
}
}
}
showComments(lifecycleScope, isActivity)
activateMoreButton(api, db, lifecycleScope)
}
@ -438,7 +457,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
//Activate double tap liking
var clicked = false
binding.postPicture.setOnClickListener {
binding.postPager.setOnClickListener {
lifecycleScope.launchWhenCreated {
//Check that the post isn't hidden
if(binding.sensitiveWarning.visibility == View.GONE) {
@ -458,7 +477,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
clicked = true
//Reset clicked to false after 500ms
binding.postPicture.handler.postDelayed(fun() { clicked = false }, 500)
binding.postPager.handler.postDelayed(fun() { clicked = false }, 500)
}
}
@ -511,156 +530,37 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
}
}
}
//endregion
private fun showComments(
api: PixelfedAPI,
credential: String,
lifecycleScope: LifecycleCoroutineScope
lifecycleScope: LifecycleCoroutineScope,
isActivity: Boolean
) {
//Show all comments of a post
//Show number of comments on the post
if (status?.replies_count == 0) {
binding.viewComments.text = binding.root.context.getString(R.string.NoCommentsToShow)
} else {
binding.viewComments.apply {
text = binding.root.context.getString(R.string.number_comments)
.format(status?.replies_count)
setOnClickListener {
visibility = View.GONE
lifecycleScope.launchWhenCreated {
//Retrieve the comments
retrieveComments(api, credential)
text = resources.getQuantityString(R.plurals.number_comments,
status?.replies_count ?: 0,
status?.replies_count ?: 0
)
if(!isActivity) {
setOnClickListener {
lifecycleScope.launchWhenCreated {
//Open status in activity
val intent = Intent(context, PostActivity::class.java)
intent.putExtra(POST_TAG, status)
intent.putExtra(VIEW_COMMENTS_TAG, true)
context.startActivity(intent)
}
}
}
}
}
}
private fun activateCommenter(
api: PixelfedAPI,
credential: String,
lifecycleScope: LifecycleCoroutineScope
) {
//Toggle comment button
toggleCommentInput()
//Activate commenterpostPicture
binding.submitComment.setOnClickListener {
val textIn = binding.editComment.text
//Open text input
if(textIn.isNullOrEmpty()) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.empty_comment),
Toast.LENGTH_SHORT
).show()
} else {
//Post the comment
lifecycleScope.launchWhenCreated {
postComment(api, credential)
}
}
}
}
private fun toggleCommentInput() {
//Toggle comment button
binding.commenter.setOnClickListener {
when(binding.commentIn.visibility) {
View.VISIBLE -> {
binding.commentIn.visibility = View.GONE
ImageConverter.setImageFromDrawable(
binding.root,
binding.commenter,
R.drawable.ic_comment_empty
)
}
View.GONE -> {
binding.commentIn.visibility = View.VISIBLE
ImageConverter.setImageFromDrawable(
binding.root,
binding.commenter,
R.drawable.ic_comment_blue
)
}
}
}
}
fun addComment(context: android.content.Context, commentContainer: LinearLayout, commentUsername: String, commentContent: String) {
val itemBinding = CommentBinding.inflate(
LayoutInflater.from(context), commentContainer, false
)
itemBinding.user.text = commentUsername
itemBinding.commentText.text = commentContent
}
private suspend fun retrieveComments(
api: PixelfedAPI,
credential: String,
) {
status?.id?.let {
try {
val statuses = api.statusComments(it, credential).descendants
binding.commentContainer.removeAllViews()
//Create the new views for each comment
for (status in statuses) {
addComment(binding.root.context, binding.commentContainer, status.account!!.username!!,
status.content!!
)
}
binding.commentContainer.visibility = View.VISIBLE
} catch (exception: IOException) {
Log.e("COMMENT FETCH ERROR", exception.toString())
} catch (exception: HttpException) {
Log.e("COMMENT ERROR", "${exception.code()} with body ${exception.response()?.errorBody()}")
}
}
}
private suspend fun postComment(
api: PixelfedAPI,
credential: String,
) {
val textIn = binding.editComment.text
val nonNullText = textIn.toString()
status?.id?.let {
try {
val response = api.postStatus(credential, nonNullText, it)
binding.commentIn.visibility = View.GONE
//Add the comment to the comment section
addComment(
binding.root.context, binding.commentContainer, response.account!!.username!!,
response.content!!
)
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.comment_posted).format(textIn),
Toast.LENGTH_SHORT
).show()
} catch (exception: IOException) {
Log.e("COMMENT ERROR", exception.toString())
Toast.makeText(
binding.root.context, binding.root.context.getString(R.string.comment_error),
Toast.LENGTH_SHORT
).show()
} catch (exception: HttpException) {
Toast.makeText(
binding.root.context, binding.root.context.getString(R.string.comment_error),
Toast.LENGTH_SHORT
).show()
Log.e("ERROR_CODE", exception.code().toString())
}
}
}
companion object {
@ -673,7 +573,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
}
}
class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>) :
private class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>, private var sensitive: Boolean?) :
RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@ -685,19 +585,42 @@ class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>) :
override fun getItemCount() = media_attachments.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
Glide.with(holder.binding.root)
.asDrawable().fitCenter().placeholder(ColorDrawable(Color.GRAY))
.load(media_attachments[position].url).into(holder.image)
media_attachments[position].apply {
val blurhashBitMap = blurhash?.let {
BlurHashDecoder.blurHashBitmap(
holder.binding.root.resources,
it,
meta?.original?.width,
meta?.original?.height
)
}
if (sensitive == false) {
Glide.with(holder.binding.root)
.asDrawable().fitCenter()
.placeholder(blurhashBitMap)
.load(url).into(holder.image)
} else {
Glide.with(holder.binding.root)
.asDrawable().fitCenter()
.load(blurhashBitMap).into(holder.image)
}
val description = media_attachments[position].description
.orEmpty().ifEmpty{ holder.binding.root.context.getString(R.string.no_description)}
val description = description
.orEmpty()
.ifEmpty { holder.binding.root.context.getString(R.string.no_description) }
holder.image.setOnLongClickListener {
Snackbar.make(it, description, Snackbar.LENGTH_SHORT).show()
true
holder.image.setOnLongClickListener {
Snackbar.make(it, description, Snackbar.LENGTH_SHORT).show()
true
}
holder.image.contentDescription = description
}
}
holder.image.contentDescription = description
fun uncensor(){
sensitive = false
notifyDataSetChanged()
}
class ViewHolder(val binding: AlbumImageViewBinding) : RecyclerView.ViewHolder(binding.root){

View File

@ -19,6 +19,7 @@ import com.h.pixeldroid.posts.feeds.cachedFeeds.CachedFeedFragment
import com.h.pixeldroid.posts.feeds.cachedFeeds.ViewModelFactory
import com.h.pixeldroid.utils.api.objects.FeedContentDatabase
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.displayDimensionsInPx
/**
@ -35,7 +36,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = PostsAdapter()
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
@Suppress("UNCHECKED_CAST")
if (requireArguments().get("home") as Boolean){
@ -67,7 +68,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
return view
}
inner class PostsAdapter : PagingDataAdapter<T, RecyclerView.ViewHolder>(
inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>) : PagingDataAdapter<T, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.id == newItem.id
@ -89,7 +90,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position) as Status
uiModel.let {
(holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope)
(holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope, displayDimensionsInPx)
}
}
}

View File

@ -97,7 +97,7 @@ class AccountViewHolder(binding: AccountListEntryBinding) : RecyclerView.ViewHol
.circleCrop().placeholder(R.drawable.ic_default_user)
.into(avatar)
username.text = account?.username
username.text = account?.display_name
@SuppressLint("SetTextI18n")
acct.text = "@${account?.acct}"
}

View File

@ -1,18 +1,20 @@
package com.h.pixeldroid.posts.feeds.uncachedFeeds.accountLists
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Account
import retrofit2.HttpException
import java.io.IOException
import java.math.BigInteger
class FollowersPagingSource(
private val api: PixelfedAPI,
private val accessToken: String,
private val accountId: String,
private val following: Boolean
) : PagingSource<Int, Account>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Account> {
) : PagingSource<String, Account>() {
override suspend fun load(params: LoadParams<String>): LoadResult<String, Account> {
val position = params.key
return try {
val response =
@ -23,14 +25,14 @@ class FollowersPagingSource(
api.followers(account_id = accountId,
authorization = "Bearer $accessToken",
limit = params.loadSize,
page = position?.toString(),
max_id = position?.toString())
page = position,
max_id = position)
} else {
api.following(account_id = accountId,
authorization = "Bearer $accessToken",
limit = params.loadSize,
page = position?.toString(),
max_id = position?.toString())
page = position,
max_id = position)
}
val accounts = if(response.isSuccessful){
@ -39,19 +41,19 @@ class FollowersPagingSource(
throw HttpException(response)
}
val nextPosition = if(response.headers()["Link"] != null){
val nextPosition: String = if(response.headers()["Link"] != null){
//Header is of the form:
// Link: <https://mastodon.social/api/v1/accounts/1/followers?limit=2&max_id=7628164>; rel="next", <https://mastodon.social/api/v1/accounts/1/followers?limit=2&since_id=7628165>; rel="prev"
// So we want the first max_id value. In case there are arguments after
// the max_id in the URL, we make sure to stop at the first '?'
response.headers()["Link"]
.orEmpty()
.substringAfter("max_id=")
.substringBefore('?')
.substringBefore('>')
.toIntOrNull() ?: 0
.substringAfter("max_id=", "")
.substringBefore('?', "")
.substringBefore('>', "")
} else {
params.key?.plus(1) ?: 2
// No Link header, so we just increment the position value
(position?.toBigIntegerOrNull() ?: 1.toBigInteger()).inc().toString()
}
LoadResult.Page(
@ -65,4 +67,9 @@ class FollowersPagingSource(
LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<String, Account>): String? =
state.anchorPosition?.run {
state.closestItemToPosition(this)?.id
}
}

View File

@ -1,6 +1,7 @@
package com.h.pixeldroid.posts.feeds.uncachedFeeds.search
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.FeedContent
import com.h.pixeldroid.utils.api.objects.Results
@ -44,4 +45,9 @@ class SearchPagingSource<T: FeedContent>(
LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, T>): Int? =
state.anchorPosition?.run {
state.closestItemToPosition(this)?.id?.toIntOrNull()
}
}

View File

@ -15,6 +15,7 @@ import com.h.pixeldroid.posts.StatusViewHolder
import com.h.pixeldroid.posts.feeds.uncachedFeeds.*
import com.h.pixeldroid.utils.api.objects.Results
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.displayDimensionsInPx
/**
* Fragment to show a list of [Status]es, as a result of a search.
@ -25,7 +26,7 @@ class SearchPostsFragment : UncachedFeedFragment<Status>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = PostsAdapter()
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
query = arguments?.getSerializable("searchFeed") as String
@ -57,7 +58,7 @@ class SearchPostsFragment : UncachedFeedFragment<Status>() {
return view
}
inner class PostsAdapter : PagingDataAdapter<Status, RecyclerView.ViewHolder>(
inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>) : PagingDataAdapter<Status, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<Status>() {
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean {
return oldItem.id == newItem.id
@ -79,7 +80,7 @@ class SearchPostsFragment : UncachedFeedFragment<Status>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position) as Status
uiModel.let {
(holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope)
(holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope, displayDimensionsInPx)
}
}
}

View File

@ -247,14 +247,23 @@ class ProfileActivity : BaseActivity() {
supportActionBar?.subtitle = "@${account.acct}"
}
binding.nbPostsTextView.text = applicationContext.getString(R.string.nb_posts)
.format(account.statuses_count.toString())
binding.nbPostsTextView.text = resources.getQuantityString(
R.plurals.nb_posts,
account.statuses_count ?: 0,
account.statuses_count ?: 0
)
binding.nbFollowersTextView.text = applicationContext.getString(R.string.nb_followers)
.format(account.followers_count.toString())
binding.nbFollowersTextView.text = resources.getQuantityString(
R.plurals.nb_followers,
account.followers_count ?: 0,
account.followers_count ?: 0
)
binding.nbFollowingTextView.text = applicationContext.getString(R.string.nb_following)
.format(account.following_count.toString())
binding.nbFollowingTextView.text = resources.getQuantityString(
R.plurals.nb_following,
account.following_count ?: 0,
account.following_count ?: 0
)
}
private fun onClickEditButton() {

View File

@ -4,25 +4,17 @@ import android.app.SearchManager
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 android.widget.*
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.FragmentSearchBinding
import com.h.pixeldroid.databinding.PostFragmentBinding
import com.h.pixeldroid.profile.ProfilePostViewHolder
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.DiscoverPost
import com.h.pixeldroid.utils.api.objects.DiscoverPosts
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.posts.PostActivity
import com.h.pixeldroid.utils.BaseFragment
@ -34,10 +26,7 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.color
import com.mikepenz.iconics.utils.paddingDp
import com.mikepenz.iconics.utils.sizeDp
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
/**
@ -122,12 +111,12 @@ class SearchDiscoverFragment : BaseFragment() {
}
/**
* [RecyclerView.Adapter] that can display a list of [DiscoverPost]s
* [RecyclerView.Adapter] that can display a list of [Status]s' thumbnails for the discover view
*/
class DiscoverRecyclerViewAdapter: RecyclerView.Adapter<ProfilePostViewHolder>() {
private val posts: ArrayList<DiscoverPost> = ArrayList()
private val posts: ArrayList<Status> = ArrayList()
fun addPosts(newPosts : List<DiscoverPost>) {
fun addPosts(newPosts : List<Status>) {
posts.clear()
posts.addAll(newPosts)
notifyDataSetChanged()
@ -141,15 +130,15 @@ class SearchDiscoverFragment : BaseFragment() {
override fun onBindViewHolder(holder: ProfilePostViewHolder, position: Int) {
val post = posts[position]
if(post.type?.contains("album") == true) {
if(post.media_attachments?.size ?: 0 > 1) {
holder.albumIcon.visibility = View.VISIBLE
} else {
holder.albumIcon.visibility = View.GONE
}
ImageConverter.setSquareImageFromURL(holder.postView, post.thumb, holder.postPreview)
ImageConverter.setSquareImageFromURL(holder.postView, post.media_attachments?.firstOrNull()?.preview_url, holder.postPreview, post.media_attachments?.firstOrNull()?.blurhash)
holder.postPreview.setOnClickListener {
val intent = Intent(holder.postView.context, PostActivity::class.java)
intent.putExtra(Status.DISCOVER_TAG, post)
intent.putExtra(Status.POST_TAG, post)
holder.postView.context.startActivity(intent)
}
}

View File

@ -0,0 +1,145 @@
package com.h.pixeldroid.utils
/**
* Blurhash implementation from blurhash project:
* https://github.com/woltapp/blurhash
* Minor modifications by charlag, for the Tusky project
* https://github.com/tuskyapp/Tusky/
*/
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import com.h.pixeldroid.utils.api.objects.Attachment
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.withSign
object BlurHashDecoder {
fun blurHashBitmap(resources: Resources, blurHash: String, width: Int?, height: Int?): BitmapDrawable {
val ratioOr0 = (width?.toFloat() ?: 1f) / (height?.toFloat() ?: 1f)
val ratio = if (ratioOr0 == 0f) 1f else ratioOr0
return BitmapDrawable(resources,
decode(blurHash,
(32f * ratio).toInt().coerceAtLeast(32),
(32f / ratio).toInt().coerceAtLeast(32))
)
}
fun decode(blurHash: String?, width: Int?, height: Int?, punch: Float = 1f): Bitmap? {
if (blurHash == null || width == null || height == null || blurHash.length < 6) {
return null
}
require(width > 0) { "Width must be greater than zero" }
require(height > 0) { "height must be greater than zero" }
val numCompEnc = decode83(blurHash, 0, 1)
val numCompX = (numCompEnc % 9) + 1
val numCompY = (numCompEnc / 9) + 1
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
return null
}
val maxAcEnc = decode83(blurHash, 1, 2)
val maxAc = (maxAcEnc + 1) / 166f
val colors = Array(numCompX * numCompY) { i ->
if (i == 0) {
val colorEnc = decode83(blurHash, 2, 6)
decodeDc(colorEnc)
} else {
val from = 4 + i * 2
val colorEnc = decode83(blurHash, from, from + 2)
decodeAc(colorEnc, maxAc * punch)
}
}
return composeBitmap(width, height, numCompX, numCompY, colors)
}
private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {
var result = 0
for (i in from until to) {
val index = charMap[str[i]] ?: -1
if (index != -1) {
result = result * 83 + index
}
}
return result
}
private fun decodeDc(colorEnc: Int): FloatArray {
val r = colorEnc shr 16
val g = (colorEnc shr 8) and 255
val b = colorEnc and 255
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), 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): FloatArray {
val r = value / (19 * 19)
val g = (value / 19) % 19
val b = value % 19
return floatArrayOf(
signedPow2((r - 9) / 9.0f) * maxAc,
signedPow2((g - 9) / 9.0f) * maxAc,
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: Array<FloatArray>
): Bitmap {
val imageArray = IntArray(width * height)
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) {
for (i in 0 until numCompX) {
val basis = (cos(PI * x * i / width) * cos(PI * y * j / height)).toFloat()
val color = colors[j * numCompX + i]
r += color[0] * basis
g += color[1] * basis
b += color[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 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 val charMap = listOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
)
.mapIndexed { i, c -> c to i }
.toMap()
}

View File

@ -69,8 +69,10 @@ class ImageConverter {
* @param url, the url of the image that will be loaded
* @param image, the imageView into which we will load the image
*/
fun setSquareImageFromURL(view : View, url : String?, image : ImageView) {
Glide.with(view).load(url).apply(RequestOptions().centerCrop()).into(image)
fun setSquareImageFromURL(view : View, url : String?, image : ImageView, blurhash: String? = null) {
Glide.with(view).load(url).placeholder(
blurhash?.let { BlurHashDecoder.blurHashBitmap(view.resources, it, 32,32) }
).apply(RequestOptions().centerCrop()).into(image)
}

View File

@ -8,12 +8,15 @@ import android.content.res.Resources
import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.util.DisplayMetrics
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.h.pixeldroid.R
import okhttp3.HttpUrl
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
@ -22,6 +25,35 @@ fun hasInternet(context: Context): Boolean {
return cm.activeNetwork != null
}
/**
* Check if domain is valid or not
*/
fun validDomain(domain: String?): Boolean {
domain?.apply {
try {
HttpUrl.Builder().host(replace("https://", "")).scheme("https").build()
} catch (e: IllegalArgumentException) {
return false
}
} ?: return false
return true
}
fun Context.displayDimensionsInPx(): Pair<Int, Int> {
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Pair(windowManager.currentWindowMetrics.bounds.width(), windowManager.currentWindowMetrics.bounds.height())
} else {
val metrics = DisplayMetrics()
@Suppress("DEPRECATION")
windowManager.defaultDisplay.getMetrics(metrics)
Pair(metrics.widthPixels, metrics.heightPixels)
}
}
fun normalizeDomain(domain: String): String {
return "https://" + domain
.replace("http://", "")

View File

@ -256,6 +256,7 @@ interface PixelfedAPI {
fun mediaUpload(
//The authorization header needs to be of the form "Bearer <token>"
@Header("Authorization") authorization: String,
@Part description: MultipartBody.Part? = null,
@Part file: MultipartBody.Part
): Observable<Attachment>

View File

@ -11,11 +11,31 @@ data class Attachment(
//Optional attributes
val remote_url: String? = null, //URL
val text_url: String? = null, //URL
//TODO meta
val meta: Meta?,
val description: String? = null,
val blurhash: String? = null
) : Serializable {
enum class AttachmentType {
unknown, image, gifv, video, audio
}
data class Meta (
val focus: Focus?,
val original: Image?
) : Serializable
{
data class Focus(
val x: Double?,
val y: Double?
) : Serializable
data class Image(
val width: Int?,
val height: Int?,
val size: String?,
val aspect: Double?
) : Serializable
}
}

View File

@ -1,13 +0,0 @@
package com.h.pixeldroid.utils.api.objects
import java.io.Serializable
/*
NOT DOCUMENTED, USE WITH CAUTION
*/
data class DiscoverPost(
val type: String?, //This is probably an enum, with these values: https://github.com/pixelfed/pixelfed/blob/700c7805cecc364b68b9cfe20df00608e0f6c465/app/Status.php#L31
val url: String?, //URL to post
val thumb: String? //URL to thumbnail
) : Serializable

View File

@ -4,5 +4,5 @@ import java.io.Serializable
data class DiscoverPosts(
//Required attributes
val posts: List<DiscoverPost>
val posts: List<Status>
) : Serializable

View File

@ -1,5 +1,7 @@
package com.h.pixeldroid.utils.api.objects
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_TOOT_CHARS
data class Instance (
val description: String?,
val email: String?,
@ -9,8 +11,4 @@ data class Instance (
val title: String?,
val uri: String?,
val version: String?
) {
companion object {
const val DEFAULT_MAX_TOOT_CHARS = 500
}
}
)

View File

@ -1,5 +1,7 @@
package com.h.pixeldroid.utils.api.objects
import com.h.pixeldroid.utils.validDomain
/*
See https://nodeinfo.diaspora.software/schema.html and https://pixelfed.social/api/nodeinfo/2.0.json
A lot of attributes we don't need are omitted, if in the future they are needed we
@ -11,8 +13,21 @@ data class NodeInfo (
val software: Software?,
val protocols: List<String>?,
val openRegistrations: Boolean?,
val metadata: PixelfedMetadata?
val metadata: PixelfedMetadata?,
){
/**
* Check if this NodeInfo has the fields we need or if we also need to look into the
* /api/v1/instance endpoint
* This only checks for values that might be in the /api/v1/instance endpoint.
*/
fun hasInstanceEndpointInfo(): Boolean {
return validDomain(metadata?.config?.site?.url)
&& !metadata?.config?.site?.name.isNullOrBlank()
&& metadata?.config?.uploader?.max_caption_length?.toIntOrNull() != null
}
data class Software(
val name: String?,
val version: String?
@ -31,7 +46,8 @@ data class NodeInfo (
val open_registration: Boolean?,
val uploader: Uploader?,
val activitypub: ActivityPub?,
val features: Features?
val features: Features?,
val site: Site?
){
data class Uploader(
val max_photo_size: String?,
@ -55,6 +71,13 @@ data class NodeInfo (
val stories: Boolean?,
val video: Boolean?
)
data class Site(
val name: String?,
val domain: String?,
val url: String?,
val description: String?
)
}
}

View File

@ -64,8 +64,8 @@ open class Status(
{
companion object {
const val POST_TAG = "postTag"
const val DOMAIN_TAG = "domainTag"
const val DISCOVER_TAG = "discoverTag"
const val VIEW_COMMENTS_TAG = "view_comments_tag"
const val POST_COMMENT_TAG = "post_comment_tag"
}
fun getPostUrl() : String? = media_attachments?.firstOrNull()?.url
@ -74,11 +74,19 @@ open class Status(
fun getNLikes(context: Context) : CharSequence {
return context.getString(R.string.likes).format(favourites_count.toString())
return context.resources.getQuantityString(
R.plurals.likes,
favourites_count ?: 0,
favourites_count ?: 0
)
}
fun getNShares(context: Context) : CharSequence {
return context.getString(R.string.shares).format(reblogs_count.toString())
return context.resources.getQuantityString(
R.plurals.shares,
reblogs_count ?: 0,
reblogs_count ?: 0
)
}
fun getStatusDomain(domain: String) : String {
@ -88,29 +96,6 @@ open class Status(
}
fun setupSensitiveLayout(binding: PostFragmentBinding) {
// Set dark layout and warning message
binding.sensitiveWarning.visibility = VISIBLE
val array = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1f, 0f)
val censorMatrix = ColorMatrix(array)
binding.postPicture.colorFilter = ColorMatrixColorFilter(censorMatrix)
fun uncensorPicture(binding: PostFragmentBinding) {
binding.sensitiveWarning.visibility = GONE
binding.postPicture.clearColorFilter()
}
binding.sensitiveWarning.setOnClickListener {
uncensorPicture(binding)
}
binding.postPicture.setOnClickListener {
uncensorPicture(binding)
}
}
fun downloadImage(context: Context, url: String, view: View, share: Boolean = false) {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager

View File

@ -20,7 +20,7 @@ import com.h.pixeldroid.utils.api.objects.Notification
PublicFeedStatusDatabaseEntity::class,
Notification::class
],
version = 2
version = 3
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {

View File

@ -4,23 +4,20 @@ import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.utils.api.objects.Account
import com.h.pixeldroid.utils.api.objects.Instance
import com.h.pixeldroid.utils.api.objects.NodeInfo
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_ALBUM_LIMIT
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_PHOTO_SIZE
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_TOOT_CHARS
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_VIDEO_SIZE
import com.h.pixeldroid.utils.normalizeDomain
private fun normalizeOrNot(uri: String): String{
return if(uri.startsWith("http://localhost")){
uri
} else {
normalizeDomain(uri)
}
}
import java.lang.IllegalArgumentException
fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String) {
db.userDao().insertUser(
UserDatabaseEntity(
user_id = account.id!!,
//make sure not to normalize to https when localhost, to allow testing
instance_uri = normalizeOrNot(instance_uri),
instance_uri = normalizeDomain(instance_uri),
username = account.username!!,
display_name = account.getDisplayName(),
avatar_static = account.avatar_static.orEmpty(),
@ -33,14 +30,24 @@ fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser:
)
}
fun storeInstance(db: AppDatabase, instance: Instance) {
val maxTootChars = instance.max_toot_chars?.toInt() ?: Instance.DEFAULT_MAX_TOOT_CHARS
val dbInstance = InstanceDatabaseEntity(
//make sure not to normalize to https when localhost, to allow testing
uri = normalizeOrNot(instance.uri.orEmpty()),
title = instance.title.orEmpty(),
max_toot_chars = maxTootChars,
thumbnail = instance.thumbnail.orEmpty()
)
fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
val dbInstance: InstanceDatabaseEntity = nodeInfo?.run {
InstanceDatabaseEntity(
uri = normalizeDomain(metadata?.config?.site?.url!!),
title = metadata.config.site.name!!,
maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(),
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_PHOTO_SIZE,
//Pixelfed doesn't distinguish between max photo and video size
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_VIDEO_SIZE,
albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT
)
} ?: instance?.run {
InstanceDatabaseEntity(
uri = normalizeDomain(uri.orEmpty()),
title = title.orEmpty(),
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
)
} ?: throw IllegalArgumentException("Cannot store instance where both are null")
db.instanceDao().insertInstance(dbInstance)
}

View File

@ -6,8 +6,23 @@ import com.h.pixeldroid.utils.api.objects.Instance
@Entity(tableName = "instances")
data class InstanceDatabaseEntity (
@PrimaryKey var uri: String,
var title: String = "",
var max_toot_chars: Int = Instance.DEFAULT_MAX_TOOT_CHARS,
var thumbnail: String = ""
)
@PrimaryKey var uri: String,
var title: String,
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
// Mastodon has different file limits for videos, default of 40MB
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
) {
companion object{
// Default max number of chars for Mastodon: used when their is no other value supplied by
// either NodeInfo or the instance endpoint
const val DEFAULT_MAX_TOOT_CHARS = 500
const val DEFAULT_MAX_PHOTO_SIZE = 8000
const val DEFAULT_MAX_VIDEO_SIZE = 40000
const val DEFAULT_ALBUM_LIMIT = 4
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

View File

@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- The background color, preferably the same as your normal theme -->
<item android:drawable="@android:color/white"/>
<!-- Your product logo - 144dp color version of your app icon -->
<item>
<bitmap
android:src="@drawable/index"
android:gravity="center"/>
</item>
<!-- The background color -->
<item android:drawable="@android:color/black"/>
<!-- The drawable (mascot) -->
<item android:drawable="@drawable/ic_fred_phone"
android:gravity="center"/>
</layer-list>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- The background color, preferably the same as your normal theme -->
<item android:drawable="@android:color/black"/>
<!-- Your product logo - 144dp color version of your app icon -->
<item>
<bitmap
android:src="@drawable/index_night"
android:gravity="center"/>
</item>
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,7v2.99s-1.99,0.01 -2,0L17,7h-3s0.01,-1.99 0,-2h3L17,2h2v3h3v2h-3zM16,11L16,8h-3L13,5L5,5c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-8h-3zM5,19l3,-4 2,3 3,-4 4,5L5,19z"
android:fillColor="#757575"/>
</vector>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false"
android:drawable="@drawable/add_photo_alternate_gray_30dp" /> <!-- disabled -->
<item android:drawable="@drawable/add_photo_alternate_white_30dp" /> <!-- default -->
</selector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="@color/white" android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
<path
android:fillColor="@color/colorButtonBg"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#BE505050" />
<padding
android:left="4dp"
android:right="4dp"
android:bottom="4dp"
android:top="4dp" />
<corners android:radius="5dp" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/ic_comment_empty"
android:state_pressed="false" />
<item
android:drawable="@drawable/ic_comment_blue"
android:state_pressed="true"/>
</selector>

View File

@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- The background color, preferably the same as your normal theme -->
<!-- The background color -->
<item android:drawable="@android:color/white"/>
<!-- Your product logo - 144dp color version of your app icon -->
<item>
<bitmap
android:src="@drawable/index"
android:gravity="center"/>
</item>
<!-- The drawable (mascot) -->
<item android:drawable="@drawable/ic_fred_phone"
android:gravity="center"/>
</layer-list>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- The background color, preferably the same as your normal theme -->
<item android:drawable="@android:color/black"/>
<!-- Your product logo - 144dp color version of your app icon -->
<item>
<bitmap
android:src="@drawable/index_night"
android:gravity="center"/>
</item>
</layer-list>

View File

@ -1,37 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".posts.PostActivity">
tools:context=".posts.PostActivity"
android:id="@+id/scrollview">
<ProgressBar
android:id="@+id/postProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/postFragmentSingle"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:context=".posts.PostFragment"
tools:visibility="visible"/>
</ScrollView>
android:layout_height="wrap_content">
</androidx.constraintlayout.widget.ConstraintLayout>
<include layout="@layout/post_fragment"
android:id="@+id/postFragmentSingle"/>
<LinearLayout
android:id="@+id/commentIn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/postFragmentSingle"
tools:layout_editor_absoluteX="10dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="3">
<EditText
android:id="@+id/editComment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/comment"
android:inputType="text"
android:importantForAutofill="no" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/submitComment"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_weight="1"
android:contentDescription="@string/submit_comment"
android:src="@drawable/ic_send_blue" />
</LinearLayout>
<LinearLayout
android:id="@+id/commentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/commentIn">
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -8,14 +8,15 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/upload_error"
android:visibility="gone"
android:layout_width="match_parent"
android:elevation="2dp"
android:layout_height="match_parent"
android:elevation="2dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/upload_error_text_view"
@ -30,15 +31,29 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/upload_error_text_explanation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#90000000"
tools:text="Error code returned by server: 413"
android:textColor="@color/colorPrimaryError"
android:textSize="20sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_view"
tools:visibility="visible" />
<Button
android:id="@+id/retry_upload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/retry"
app:layout_constraintEnd_toEndOf="@id/upload_error_text_view"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
app:layout_constraintTop_toBottomOf="@id/upload_error_text_view" />
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
</androidx.constraintlayout.widget.ConstraintLayout>
@ -47,6 +62,7 @@
android:id="@+id/carousel"
android:layout_width="match_parent"
android:layout_height="0dp"
app:showCaption="true"
app:layout_constraintBottom_toBottomOf="@+id/uploadProgressBar"
app:layout_constraintTop_toTopOf="parent"/>
@ -134,7 +150,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbar3"
android:id="@+id/toolbarPostCreation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#40000000"
@ -191,7 +207,7 @@
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/add_photo"
android:tooltipText='@string/add_photo'
android:src="@drawable/add_photo_alternate_white_30dp"
android:src="@drawable/add_photo_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@ -12,6 +12,6 @@
android:layout_height="50dp"
android:layout_centerInParent="true"
android:layout_centerVertical="true"
android:background="@drawable/add_photo_alternate_white_30dp"
android:background="@drawable/add_photo_button"
android:contentDescription="@string/add_photo" />
</com.h.pixeldroid.postCreation.SquareLayout>

View File

@ -8,8 +8,9 @@
<ImageView
android:id="@+id/imageImageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
tools:ignore="ContentDescription" />

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundTint="#FFFFFF">
android:foregroundTint="#000000">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
@ -25,6 +25,7 @@
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="marquee"
android:text="@string/no_media_description"
android:gravity="center"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
@ -37,6 +38,49 @@
app:layout_goneMarginBottom="8dp"
tools:text="@tools:sample/lorem[5]" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/editMediaDescriptionLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:background="#4D000000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/indicator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_goneMarginBottom="8dp">
<EditText
android:id="@+id/editTextMediaDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
android:gravity="start|top"
android:hint="@string/no_media_description"
android:importantForAutofill="no"
android:inputType="textMultiLine"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/imageDescriptionButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/imageDescriptionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/save_image_description"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/editTextMediaDescription"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/check_circle_24" />
</androidx.constraintlayout.widget.ConstraintLayout>
<me.relex.circleindicator.CircleIndicator2
android:id="@+id/indicator"
android:layout_width="wrap_content"
@ -45,21 +89,48 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<FrameLayout
android:id="@+id/previous_button_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_previous"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:backgroundTint="#22000000"
app:cornerRadius="48dp"
app:icon="@drawable/ic_chevron_left_black_24dp"
app:iconGravity="textStart"
app:iconSize="48dp"
app:iconTint="@color/white"
app:rippleColor="@color/white"
app:layout_constraintBottom_toBottomOf="@+id/recyclerView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/recyclerView" />
app:layout_constraintTop_toTopOf="@+id/recyclerView"/>
<FrameLayout
android:id="@+id/next_button_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_next"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:backgroundTint="#22000000"
app:cornerRadius="48dp"
app:icon="@drawable/ic_chevron_right_black_24dp"
app:iconGravity="textEnd"
app:iconSize="48dp"
app:iconTint="@color/white"
app:rippleColor="@color/white"
app:layout_constraintBottom_toBottomOf="@+id/recyclerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/recyclerView" />
app:layout_constraintTop_toTopOf="@+id/recyclerView"/>
<ImageButton
android:id="@+id/switchToGridButton"
@ -81,13 +152,13 @@
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/switch_to_carousel"
android:visibility="gone"
tools:visibility="visible"
android:src="@drawable/view_carousel_black_24dp"
android:tint="@color/white"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/indicator"
app:layout_constraintTop_toTopOf="@+id/indicator" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/indicator"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/btn_next"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:backgroundTint="#22000000"
app:cornerRadius="48dp"
app:icon="@drawable/ic_chevron_right_black_24dp"
app:iconGravity="textEnd"
app:iconSize="48dp"
app:iconTint="@color/white"
app:rippleColor="@color/white" />

View File

@ -6,8 +6,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto"
tools:context=".posts.PostFragment">
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@ -56,59 +55,59 @@
android:layout_marginTop="10dp"
app:layout_constraintTop_toBottomOf="@+id/profilePic">
<com.h.pixeldroid.posts.NestedScrollableHost
android:id="@+id/postPagerHost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/postPager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/postTabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/postPager"
app:tabMode="auto" />
<ImageView
android:id="@+id/postPicture"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:layout_height="200dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@color/browser_actions_bg_grey"
tools:ignore="ContentDescription" />
android:orientation="horizontal" />
</com.h.pixeldroid.posts.NestedScrollableHost>
<me.relex.circleindicator.CircleIndicator3
android:id="@+id/postIndicator"
android:layout_width="wrap_content"
android:layout_height="32dp"
app:layout_constraintBottom_toBottomOf="@+id/postPagerHost"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<FrameLayout
android:id="@+id/post_fragment_image_popup_menu_anchor"
android:layout_width="1dp"
android:layout_height="1dp"
app:layout_constraintBottom_toBottomOf="@+id/postPicture"
app:layout_constraintEnd_toEndOf="@+id/postPicture"
app:layout_constraintBottom_toBottomOf="@+id/postPagerHost"
app:layout_constraintEnd_toEndOf="@+id/postPagerHost"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toStartOf="@+id/postPicture"
app:layout_constraintTop_toTopOf="@+id/postPicture"
app:layout_constraintStart_toStartOf="@+id/postPagerHost"
app:layout_constraintTop_toTopOf="@+id/postPagerHost"
app:layout_constraintVertical_bias="0.1" />
<TextView
android:id="@+id/sensitiveWarning"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:background="@drawable/rounded_corner"
android:gravity="center|center_horizontal|center_vertical"
android:text="@string/cw_nsfw_hidden_media_n_click_to_show"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="@color/ic_launcher_background"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/postPicture"
app:layout_constraintBottom_toBottomOf="@+id/postPagerHost"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/postTabs"
app:layout_constraintTop_toTopOf="@+id/postPagerHost"
tools:src="@color/browser_actions_bg_grey" />
</androidx.constraintlayout.widget.ConstraintLayout>
@ -119,7 +118,7 @@
android:layout_width="30dp"
android:layout_height="30dp"
android:padding="4dp"
android:src="@drawable/ic_comment_empty"
android:src="@drawable/selector_commenter"
app:layout_constraintBottom_toBottomOf="@+id/liker"
app:layout_constraintEnd_toStartOf="@id/reblogger"
app:layout_constraintStart_toEndOf="@id/liker"
@ -227,49 +226,8 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/commentIn"
tools:text="3 comments" />
<LinearLayout
android:id="@+id/commentIn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/postDate"
tools:layout_editor_absoluteX="10dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="3">
<EditText
android:id="@+id/editComment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/comment"
android:inputType="text"
android:importantForAutofill="no" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/submitComment"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_weight="1"
android:contentDescription="@string/submit_comment"
android:src="@drawable/ic_send_blue" />
</LinearLayout>
<LinearLayout
android:id="@+id/commentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/viewComments">
</LinearLayout>
tools:text="3 comments" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/btn_previous"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:backgroundTint="#22000000"
app:cornerRadius="48dp"
app:icon="@drawable/ic_chevron_left_black_24dp"
app:iconGravity="textStart"
app:iconSize="48dp"
app:iconTint="@color/white"
app:rippleColor="@color/white" />

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/whats_an_instance_explanation"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:autoLink="web"
android:padding="16dp"
android:text="@string/whats_an_instance_explanation" />

View File

@ -47,19 +47,13 @@
<string name="light_theme">فاتح</string>
<string name="save_image_success">تم حفظ الصورة بنجاح</string>
<string name="default_system">افتراضي (يتبع النظام)</string>
<string name="description_max_characters">يجب أن يحتوي الوصف على %1$s حرفًا على الأكثر.</string>
<string name="upload_post_success">تم تحميل المنشور بنجاح</string>
<string name="upload_post_failed">فشل في تحميل المنشور</string>
<string name="upload_picture_failed">خطأ في تحميل الصورة!</string>
<string name="loading_toast">حدث خلل اثناء التحميل</string>
<string name="normal_filter">عادي</string>
<string name="upload_post_error">فشل في تحميل المنشور</string>
<string name="nb_following">%1$s
\nيتابعون</string>
<string name="nb_followers">%1$s
\nمتابع</string>
<string name="nb_posts">%1$s
\nمشاركة</string>
<string name="comment">تعليق</string>
<string name="comment_posted">التعليق: تم نشر%1$s!</string>
<string name="comment_error">خطأ في التعليق!</string>
@ -68,8 +62,6 @@
<string name="write_permission_download_pic">تحتاج إلى منح إذن الكتابة لتنزيل الصور!</string>
<string name="empty_comment">لا يجب ان يكون التعليق فارغًا!</string>
<string name="posted_on">نُشِر في %1$s</string>
<string name="shares">%1$s مشاركات</string>
<string name="likes">%1$s إعجاب</string>
<string name="no_description">مِن دون وصف</string>
<string name="feed_failed">تعذّر جلب التدفق</string>
<string name="retry">حاول مجدّدًا</string>

View File

@ -41,7 +41,6 @@
<string name="add_account_description">Afegeix un altre compte</string>
<string name="add_account_name">Afegeix un compte</string>
<string name="instance_error">No s\'ha pogut obtenir informació sobre la instància</string>
<string name="shares">%1$s Accions</string>
<string name="hashtags">HASHTAGS</string>
<string name="accounts">COMPTES</string>
<string name="posts">PUBLICACIONS</string>
@ -61,12 +60,6 @@
<string name="follow_error">No s\'ha pogut seguir</string>
<string name="follow_button_failed">No s\'ha pogut mostrar el botó de seguir</string>
<string name="follow_status_failed">No s\'ha pogut obtenir l\'estat de seguiment</string>
<string name="nb_following">%1$s
\nSeguits/des</string>
<string name="nb_followers">%1$s
\nSeguidors/es</string>
<string name="nb_posts">%1$s
\nPublicacions</string>
<string name="comment">Comentari</string>
<string name="comment_posted">Comentari: %1$s publicat!</string>
<string name="comment_error">Error de comentari!</string>
@ -75,7 +68,6 @@
<string name="write_permission_download_pic">Has de concedir permís descriptura per baixar imatges!</string>
<string name="empty_comment">El comentari no ha de estar buit!</string>
<string name="posted_on">Publica\'t el %1$s</string>
<string name="likes">%1$s M\'agrades</string>
<string name="no_description">Sense descripció</string>
<string name="feed_failed">No s\'ha pogut obtenir el fil</string>
<string name="loading_toast">Alguna cosa no ha funcionat correctament mentre es carregava</string>
@ -86,7 +78,6 @@
<string name="request_format_error">Error d\'enviament: format de sol·licitud dolent</string>
<string name="picture_format_error">Error d\'enviament: format d\'imatge erroni.</string>
<string name="upload_picture_failed">Error d\'enviament d\'imatges!</string>
<string name="description_max_characters">La descripció ha de contenir %1$s caràcters com a mínim.</string>
<string name="save_image_success">La imatge s\'ha desat correctament</string>
<string name="save_image_failed">No es pot desar la imatge</string>
<string name="permission_denied">Permís denegat</string>

View File

@ -43,7 +43,6 @@
<string name="default_system">Standard (Systemeinstellung)</string>
<string name="permission_denied">Berechtigung verweigert</string>
<string name="save_image_failed">Bild kann nicht gespeichert werden</string>
<string name="description_max_characters">Die Beschreibung darf höchstens %1$s Zeichen enthalten.</string>
<string name="normal_filter">Normal</string>
<string name="save_image_success">Bild erfolgreich gespeichert</string>
<string name="picture_format_error">Upload-Fehler: falsches Bildformat.</string>
@ -55,8 +54,6 @@
<string name="comment">Kommentieren</string>
<string name="share_image">Bild teilen</string>
<string name="empty_comment">Der Kommentar darf nicht leer sein!</string>
<string name="shares">%1$s Geteilt</string>
<string name="likes">%1$s Likes</string>
<string name="no_description">Keine Beschreibung</string>
<string name="loading_toast">Beim Laden ist etwas schiefgegangen</string>
<string name="search">Suchen</string>
@ -66,12 +63,6 @@
<string name="upload_post_error">Fehler beim Senden des Beitrags</string>
<string name="comment_error">Kommentarfehler!</string>
<string name="comment_posted">Kommentar: %1$s geschrieben!</string>
<string name="nb_posts">%1$s
\nBeiträge</string>
<string name="nb_followers">%1$s
\nFollower</string>
<string name="nb_following">%1$s
\nFolgt</string>
<string name="follow_button_failed">Konnte Knopf zum Folgen nicht anzeigen</string>
<string name="follow_error">Konnte nicht folgen</string>
<string name="no_username">Kein Benutzername</string>
@ -142,11 +133,32 @@
<string name="post_is_album">Dieser Beitrag ist ein Album</string>
<string name="submit_comment">Kommentar senden</string>
<string name="add_comment">Erstelle einen Kommentar</string>
<string name="number_comments">%1$s Kommentare</string>
<string name="crop_button">Schaltfläche zum ausschneiden oder rotieren des Bildes</string>
<string name="image_preview">Vorschau des bearbeiteten Bildes</string>
<string name="filter_thumbnail">Vorschaubild des Filters</string>
<string name="click_image_edit">Klicke auf das Bild um es zu ändern</string>
<string name="post_image">Eines der Bilder im Beitrag</string>
<string name="verify_credentials">Benutzerdaten konnten nicht geladen werden</string>
<plurals name="nb_posts">
<item quantity="one">%d Beitrag</item>
<item quantity="other">%d Beiträge</item>
</plurals>
<plurals name="number_comments">
<item quantity="one">%d Kommentar</item>
<item quantity="other">%d Kommentar</item>
</plurals>
<string name="no_media_description">Ergänze hier eine Medienbeschreibung hier…</string>
<string name="save_image_description">Bildbeschreibung speichern</string>
<plurals name="description_max_characters">
<item quantity="one">Die Beschreibung muss mindestens %d Zeichen enthalten.</item>
<item quantity="other">Die Beschreibung muss mindestens %d Zeichen enthalten.</item>
</plurals>
<string name="whats_an_instance_explanation">Das Textfeld, mit der Frage nach der Domain deiner Instanz, hat dich vielleicht verwirrt.
\n
\n
\nPixelfed ist eine Föderierte Platform und Teil des \"Fediverse\". Das bedeutet, dass es mit anderen Plattformen kommunizieren kann, wie Mastodon (https://joinmastodon.org).
\n
\nEs bedeutend auch, dass du wählen musst welchen Server, oder \"Pixelfed-Instanz\", du nutzen möchtest.
\nWenn du keine kennst kannst u hier nachsehen: https://pixelfed.org/join
\n
\nMehr Informationen zu Pixelfed findest du hier: https://pixelfed.org</string>
</resources>

View File

@ -64,12 +64,6 @@
<string name="follow_error">No pudo seguir</string>
<string name="follow_button_failed">No se pudo mostrar el botón de seguimiento</string>
<string name="follow_status_failed">No se pudo obtener el estado de seguidores</string>
<string name="nb_following">%1$s
\nSeguidos</string>
<string name="nb_followers">%1$s
\nSeguidores</string>
<string name="nb_posts">%1$s
\nPublicaciones</string>
<string name="comment">Comentar</string>
<string name="comment_posted">¡Comentario: %1$s publicado!</string>
<string name="comment_error">¡Error al comentar!</string>
@ -78,8 +72,6 @@
<string name="write_permission_download_pic">¡Tienes que dar permiso de escritura para descargar fotos!</string>
<string name="empty_comment">¡Los comentarios no deben estar vacíos!</string>
<string name="posted_on">Publicado en %1$s</string>
<string name="shares">%1$s Comparticiones</string>
<string name="likes">%1$s Me gustas</string>
<string name="no_description">Sin descripción</string>
<string name="feed_failed">No se pudieron obtener publicaciones</string>
<string name="loading_toast">Algo salió mal al cargar</string>
@ -90,7 +82,6 @@
<string name="request_format_error">Error de carga: formato de petición erróneo</string>
<string name="picture_format_error">Error de carga: formato de imagen incorrecto.</string>
<string name="upload_picture_failed">¡Error al subir la foto!</string>
<string name="description_max_characters">La descripción debe contener %1$s caracteres como máximo.</string>
<string name="save_image_success">Imagen guardada correctamente</string>
<string name="save_image_failed">No se puede guardar la imagen</string>
<string name="permission_denied">Permiso denegado</string>

View File

@ -49,10 +49,6 @@
\nJarraitzaile</string>
<string name="default_nposts">-
\nArgitalpen</string>
<string name="nb_following">%1$s
\nJarraitzen</string>
<string name="nb_followers">%1$s
\nJarraitzaile</string>
<string name="share_image">Irudia partekatu</string>
<string name="upload_picture_failed">Ezin izan da irudia kargatu!</string>
<string name="save_image_failed">Ezinezkoa irudia gordetzea</string>
@ -60,16 +56,13 @@
<string name="light_theme">Argia</string>
<string name="default_system">Lehenetsia (Sistemaren lehentsia)</string>
<string name="no_description">Deskribapenik ez</string>
<string name="description_max_characters">Deskribapenak %1$s karaktere izan behar ditu gehienez.</string>
<string name="save_image_success">Irudia behar bezala gorde da</string>
<string name="feed_failed">Ezin izan da jarioa lortu</string>
<string name="loading_toast">Zerbait gaizki joan da kargatzean</string>
<string name="shares">%1$s Partekatze</string>
<string name="write_permission_download_pic">Idazteko baimena eman behar duzu irudiak deskargatzeko!</string>
<string name="empty_comment">Iruzkina ez da hutsik egon behar!</string>
<string name="likes">%1$s Atsegite</string>
<string name="normal_filter">Normala</string>
<string name="upload_post_error">Argitalpenak huts egin du</string>
<string name="upload_post_error">Argitalpena igotzean errorea</string>
<string name="comment_posted">Iruzkina: %1$s argitaratua!</string>
<string name="upload_post_success">Argitalpena ongi kargatu da</string>
<string name="media_upload_failed">{gmd_cloud_off} Multimedia-kargak huts egin du, saiatu berriro edo egiaztatu sareko egoera</string>
@ -80,7 +73,7 @@
<string name="permission_denied">Baimena ukatuta</string>
<string name="retry">Saiatu berriz</string>
<string name="posting_image_accessibility_hint">Argitaratzen ari den irudia</string>
<string name="edit_profile">Profila editatu</string>
<string name="edit_profile">Editatu profila</string>
<string name="no_username">Erabiltzaile-izenik ez</string>
<string name="access_token_invalid">Sartzeko tokena baliogabea</string>
<string name="unfollow_error">Ezin izan da jarraitzeari utzi</string>
@ -88,13 +81,98 @@
<string name="follow_error">Ezin izan da jarraitu</string>
<string name="follow_button_failed">Ezin izan da jarraipen-botoia erakutsi</string>
<string name="follow_status_failed">Ezin izan da segimendu-egoera lortu</string>
<string name="nb_posts">%1$s
\nArgitalpen</string>
<string name="comment">Iruzkin</string>
<string name="write_permission_share_pic">Idazteko baimena eman behar duzu argazkiak partekatzeko!</string>
<string name="posted_on">%1$s(e)n argitaratua</string>
<string name="hashtags">TRAOLAK</string>
<string name="accounts">KONTUAK</string>
<string name="media_upload_completed">{gmd_cloud_done} Multimedia karga burutu da</string>
<string name="upload_post_failed">Argitalpenak huts egin du</string>
<string name="media_upload_completed">{gmd_cloud_done} Multimedia karga amaitu da</string>
<string name="upload_post_failed">Argitalpena igotzeak huts egin du</string>
<string name="no_cancel_edit">Ez, utzi editatzeari</string>
<string name="save_before_returning">Zure edizioak gorde\?</string>
<string name="mascot_description">Panda gorri bat, Pixelfeden maskota, mugikor bat erabiltzen erakusten duen irudia</string>
<string name="issues_contribute">Jakinarazi arazoak edo aplikazioan lagundu:</string>
<string name="help_translate">Lagundu PixelDroid zure hizkuntzara itzultzen:</string>
<string name="language">Hizkuntza</string>
<string name="delete_dialog">Argitalpen hau ezabatu\?</string>
<string name="delete">Ezabatu</string>
<string name="panda_pull_to_refresh_to_try_again">Panda hau ez dago pozik. Tira freskatzeko berriz saiatzeko.</string>
<string name="something_went_wrong">Arazoren bat izan da…</string>
<string name="discover">AURKITU</string>
<string name="open_drawer_menu">Ireki menu lerrakorra</string>
<string name="profile_picture">Profileko irudia</string>
<string name="toolbar_title_edit">Editatu</string>
<string name="report_error">Ezin izan da salaketa bidali</string>
<string name="reported">Salatua {gmd_check_circle}</string>
<string name="report_target">Salatu @%1$s erabiltzailearen argitalpena</string>
<string name="optional_report_comment">Moderatzaile/administratzaileentzako hautazko mezua</string>
<string name="share_link">Partekatu esteka</string>
<string name="report">Salatu</string>
<string name="status_more_options">Aukera gehiago</string>
<string name="search_empty_error">Bilaketak ezin du hutsik egon</string>
<string name="follows_title">%1$s erabiltzaileak jarraituak</string>
<string name="followers_title">%1$s erabiltzailearen jarraitzaileak</string>
<string name="post_title">%1$s erabiltzailearen argitalpena</string>
<string name="about">Honi buruz</string>
<string name="license_info">PixelDroid software libre eta kode irekikoa da, GNU General Public License (3 bertsioa edo berriagoa) lizentzia duena</string>
<string name="project_website">Proiektuaren webgunea: https://pixeldroid.org</string>
<string name="dependencies_licenses">Menpekotasunak eta lizentziak</string>
<string name="about_pixeldroid">PixelDroidi buruz</string>
<string name="nothing_to_see_here">Hemen ez dago ezer!</string>
<string name="unfollow">Utzi jarraitzeari</string>
<plurals name="nb_following">
<item quantity="one">%d jarraitzen</item>
<item quantity="other">%d jarraitzen</item>
</plurals>
<plurals name="nb_followers">
<item quantity="one">Jarraitzaile %d</item>
<item quantity="other">%d jarraitzaile</item>
</plurals>
<plurals name="nb_posts">
<item quantity="one">Argitalpen %d</item>
<item quantity="other">%d argitalpen</item>
</plurals>
<string name="post_is_album">Argitalpen hau album bat da</string>
<string name="submit_comment">Bidali iruzkina</string>
<string name="add_comment">Gehitu iruzkin bat</string>
<plurals name="number_comments">
<item quantity="one">Iruzkin %d</item>
<item quantity="other">%d iruzkin</item>
</plurals>
<plurals name="shares">
<item quantity="one">Partekatze %d</item>
<item quantity="other">%d partekatze</item>
</plurals>
<plurals name="likes">
<item quantity="one">Gogoko %d</item>
<item quantity="other">%d gogoko</item>
</plurals>
<string name="crop_button">Irudia moztu edo biratzeko botoia</string>
<string name="image_preview">Editatutako irudiaren aurrebista</string>
<string name="crop_result_error">Ezin izan da irudia berreskuratu moztu ondoren</string>
<string name="busy_dialog_ok_button">Ados, itxaron.</string>
<string name="busy_dialog_text">Oraindik irudia prozesatzen ari da, itxaron amaitu arte!</string>
<string name="filter_thumbnail">Iragazkiaren koadro txikia</string>
<string name="no_media_description">Gehitu multimediaren deskribapena hemen…</string>
<string name="save_image_description">Gorde irudiaren deskribapena</string>
<string name="switch_to_carousel">Aldatu karruselera</string>
<string name="switch_to_grid">Aldatu sareta-ikuspegira</string>
<string name="post_image">Argitalpeneko irudietako bat</string>
<string name="add_photo">Gehitu argazki bat</string>
<plurals name="description_max_characters">
<item quantity="one">Deskribapenak karaktere %d izan behar du gehienez.</item>
<item quantity="other">Deskribapenak %d karaktere izan behar ditu gehienez.</item>
</plurals>
<string name="whats_an_instance_explanation">Behar bada zure \'instantziaren\' domeinua eskatzen dizun testu-eremua nahasgarria iruditu zaizu.
\n
\nPixelfed plataforma federatu bat da, eta \'fedibertsoaren\' parte da. Horrek esan nahi du hizkuntza bera erabiltzen duten beste plataforma batzuekin hitz egin dezakeela, adibidez Mastodonekin (ikusi https://joinmastodon.org).
\n
\nHorregatik Pixelfeden ze zerbitzari, edo \'instantzia\', erabili nahi duzun aukeratu behar duzu. Ez baduzu bat ere ezagutzen, hemen begira dezakezu: https://pixelfed.org/join
\n
\nPixelfedi buruzko informazio gehiago nahi baduzu: https://pixelfed.org</string>
<string name="poll_notification">%1$s erabiltzailearen inkesta amaitu da</string>
<string name="instance_not_pixelfed_cancel">Utzi saioa hastea</string>
<string name="instance_not_pixelfed_continue">Ados, jarraitu hala ere</string>
<string name="instance_not_pixelfed_warning">Honek ez dirudi Pixelfed instantzia bat, aplikazioa ustekabeko moduetan huts egin dezake.</string>
<string name="verify_credentials">Ezin izan da erabiltzailearen informazioa eskuratu</string>
</resources>

View File

@ -43,7 +43,6 @@
<string name="instance_error">نتوانستیم اطلاعات نمونه را دریافت کنیم</string>
<string name="permission_denied">ممنوعیت دسترسی</string>
<string name="save_image_failed">نتوانستیم تصویر را ذخیره کنیم</string>
<string name="description_max_characters">توضیحات حداکثر می‌توانند تا %1$s حرف داشته باشند.</string>
<string name="default_system">پیش‌گزیده (پیروی از سامانه)</string>
<string name="light_theme">روشن</string>
<string name="dark_theme">تاریک</string>
@ -58,10 +57,8 @@
<string name="loading_toast">مشکلی حین بارکردن رخ داد</string>
<string name="feed_failed">نتوانستیم خوراک را دریافت کنیم</string>
<string name="no_description">بدون توضیحات</string>
<string name="likes">%1$s پسند</string>
<string name="accounts">حساب‌ها</string>
<string name="hashtags">هشتگ‌ها</string>
<string name="shares">%1$s هم‌رسانی</string>
<string name="posted_on">منتشر شده در %1$s</string>
<string name="empty_comment">نظر نمی‌تواند خالی باشد!</string>
<string name="write_permission_download_pic">برای بارگیری تصاویر بایستی مجوز نوشتن را بدهید!</string>
@ -70,12 +67,6 @@
<string name="comment_error">خطا در درج نظر!</string>
<string name="comment_posted">نظر: %1$s منتشر کرد!</string>
<string name="comment">نظر</string>
<string name="nb_posts">%1$s
\nمطلب</string>
<string name="nb_followers">%1$s
\nپیگیرنده</string>
<string name="nb_following">%1$s
\nپیگیری</string>
<string name="follow_status_failed">نتوانستیم وضعیت پی‌گیری را دریافت کنیم</string>
<string name="follow_button_failed">نتوانستیم دکمه پی‌گیری را نمایش دهیم</string>
<string name="follow_error">نتوانستیم پی‌گیری کنیم</string>
@ -137,11 +128,55 @@
<string name="post_is_album">این فرسته، یک آلبوم است</string>
<string name="submit_comment">فرستادن نظر</string>
<string name="add_comment">نظری اضافه کنید</string>
<string name="number_comments">%1$s نظر</string>
<string name="crop_button">دکمه برش یا چرخش تصویر</string>
<string name="image_preview">پیش‌نمایش تصویری که در حال ویرایش است</string>
<string name="filter_thumbnail">تصویر بندانگشتی پالایه</string>
<string name="click_image_edit">برای ویرایش تصویر، روی آن کلیک کنید</string>
<string name="post_image">یکی از تصاویر در فرسته</string>
<string name="verify_credentials">ناتوانی در دریافت اطلاعات کاربر</string>
<string name="mascot_description">تصویر، یک پاندای قرمز که شگونهٔ پیکسل‌فد است را گوشی به دست نشان می‌دهد</string>
<plurals name="nb_posts">
<item quantity="one">%d فرسته</item>
<item quantity="other">%d فرسته</item>
</plurals>
<plurals name="number_comments">
<item quantity="one">%d نظر</item>
<item quantity="other">%d نظر</item>
</plurals>
<plurals name="shares">
<item quantity="one">%d هم‌رسانی</item>
<item quantity="other">%d هم‌رسانی</item>
</plurals>
<plurals name="likes">
<item quantity="one">%d پسند</item>
<item quantity="other">%d پسند</item>
</plurals>
<string name="switch_to_carousel">تغییر وضعیت به نمای گردونه</string>
<string name="no_cancel_edit">خیر، ویرایش لغو شود</string>
<string name="save_before_returning">ویرایش‌ها ذخیره شوند؟</string>
<plurals name="nb_followers">
<item quantity="one">%d پی‌گیر</item>
<item quantity="other">%d پی‌گیر</item>
</plurals>
<plurals name="nb_following">
<item quantity="one">%d پی‌گرفته</item>
<item quantity="other">%d پی‌گرفته</item>
</plurals>
<string name="no_media_description">این جا توضیحات رسانه را وارد کنید…</string>
<string name="save_image_description">ذخیره توضیحات تصویر</string>
<string name="switch_to_grid">تغییر وضعیت به نمای شبکه‌ای</string>
<plurals name="description_max_characters">
<item quantity="one">توضیحات باید در بیشترین حالت حاوی %d حرف باشد.</item>
<item quantity="other">توضیحات باید در بیشترین حالت حاوی %d حرف باشد.</item>
</plurals>
<string name="whats_an_instance_explanation">ممکن است از دیدن بخشی که از شما می‌خواهد که دامنه مربوط به «نمونه» را وارد کنید گیج شده باشید.
\n
\nپیکسلفد یک بستر فدرالیزه و عضوی از«فدیورس» (دنیای شبکه‌های فدرالیزه) است به این معنا که می‌تواند با سایر بسترها مانند ماستودون (https://joinmastodon.org را ببینید) که زبان مشترکی دارند صحبت کرده و ارتباط برقرار کند.
\n
\nمعنای دیگر این موضوع آن است که شما باید انتخاب کنید که از کدام کارساز یا «نمونه» از پیکسل‌فد استفاده می‌کنید. اگر چیزی در این باره نمی‌دانید می‌توانید به این نشانی مراجعه کنید: https://pixelfed.org/join
\n
\nبرای کسب اطلاعات بیشتر درباره پیکسل‌فد می‌توانید این نشانی را ببینید: https://pixelfed.org</string>
<string name="upload_error">کد خطای دریافت شده از کارساز: %1$s</string>
<string name="size_exceeds_instance_limit">اندازه تصویر شماره %1$s در آلبوم از بیشینه اندازه مجاز در نمونه (%2$s کیلوبایت در مقابل آستانه مجاز %3$s کیلوبایت) تجاوز رده است.</string>
<string name="total_exceeds_album_limit">تعداد تصاویر انتخاب شده شما بیش از بیشینه مجاز روی کارساز است (%1$s). تصاویر بیش از آن تعداد، نادیده گرفته شده‌اند.</string>
<string name="api_not_enabled_dialog">روی این نمونه، API فعال نیست. با مدیر نمونه تماس گرفته و از او بخواهید آن را فعال کند.</string>
</resources>

View File

@ -12,13 +12,13 @@
<string name="post">envoyer</string>
<string name="whats_an_instance">Qu\'est-ce qu\'une instance \?</string>
<string name="logout">Se déconnecter</string>
<string name="save_to_gallery">Sauvegarder dans la galerie…</string>
<string name="save_to_gallery">Enregistrer dans la galerie…</string>
<string name="image_download_downloading">En cours de téléchargement…</string>
<string name="image_download_success">Image téléchargée avec succès</string>
<string name="share_picture">Partager l\'image…</string>
<string name="invalid_domain">Domaine invalide</string>
<string name="image_download_failed">Le téléchargement a échoué, veuillez réessayer</string>
<string name="browser_launch_failed">Impossible de lancer un navigateur, en avez-vous un \?</string>
<string name="browser_launch_failed">Impossible de lancer un navigateur web, en avez-vous un \?</string>
<string name="mention_notification">%1$s vous a mentionné</string>
<string name="app_name">PixelDroid</string>
<string name="token_error">Erreur lors de l\'obtention du token</string>
@ -26,17 +26,17 @@
<string name="theme_title">Thème de lapplication</string>
<string name="theme_header">Thème</string>
<string name="lbl_brightness">Luminosité</string>
<string name="lbl_contrast">CONTRASTE</string>
<string name="lbl_contrast">Contraste</string>
<string name="lbl_saturation">Saturation</string>
<string name="tab_filters">Filtres</string>
<string name="edit">MODIFIER</string>
<string name="edit">Modifier</string>
<string name="capture_button_alt">Prendre une photo</string>
<string name="switch_camera_button_alt">Changer de caméra</string>
<string name="gallery_button_alt">Galerie</string>
<string name="NoCommentsToShow">Pas de commentaire pour cette publication…</string>
<string name="NoCommentsToShow">Aucun commentaire pour cette publication…</string>
<string name="domain_of_your_instance">Domaine de votre instance</string>
<string name="cw_nsfw_hidden_media_n_click_to_show">CW / NSFW / Media caché
\n(Cliquer pour le montrer)</string>
<string name="cw_nsfw_hidden_media_n_click_to_show">CW / NSFW / Média caché
\n(Cliquer pour l\'afficher)</string>
<string name="login_connection_required_once">Vous devez être connecté à internet pour pouvoir ajouter un compte et utiliser PixelDroid :(</string>
<string name="add_account_description">Ajouter un autre compte Pixelfed</string>
<string name="add_account_name">Ajouter un compte Pixelfed</string>
@ -44,9 +44,8 @@
<string name="request_format_error">Erreur de téléchargement: format de requête incorrect</string>
<string name="picture_format_error">Erreur de téléversement: format d\'image incorrect.</string>
<string name="upload_picture_failed">Erreur de téléversement d\'image!</string>
<string name="description_max_characters">La description doit contenir au plus %1$s caractères.</string>
<string name="save_image_success">Image enregistrée avec succès</string>
<string name="save_image_failed">Impossible de sauvegarder l\'image</string>
<string name="save_image_failed">Impossible d\'enregistrer l\'image</string>
<string name="permission_denied">Permission refusée</string>
<string name="dark_theme">Sombre</string>
<string name="light_theme">Clair</string>
@ -63,19 +62,11 @@
<string name="loading_toast">Une erreur s\'est produite lors du chargement</string>
<string name="feed_failed">Impossible d\'obtenir le flux</string>
<string name="no_description">Sans description</string>
<string name="likes">%1$s J\'aimes</string>
<string name="shares">%1$s Partages</string>
<string name="write_permission_download_pic">Vous devez accorder une autorisation d\'écriture pour télécharger des photos !</string>
<string name="share_image">Partager image</string>
<string name="comment_error">Erreur de commentaire !</string>
<string name="comment_posted">Commentaire : %1$s publié !</string>
<string name="comment">Commenter</string>
<string name="nb_posts">%1$s
\nPublications</string>
<string name="nb_followers">%1$s
\nAbonné·e·s</string>
<string name="nb_following">%1$s
\nAbonnements</string>
<string name="follow_status_failed">Impossible d\'obtenir l\'état de suivi</string>
<string name="follow_button_failed">Impossible d\'afficher le bouton de suivi</string>
<string name="follow_error">Impossible de suivre</string>
@ -89,7 +80,7 @@
<string name="no_username">Pas de nom d\'utilisateur</string>
<string name="follow">S\'abonner</string>
<string name="edit_profile">Modifier le profil</string>
<string name="search">Chercher</string>
<string name="search">Rechercher</string>
<string name="posts">PUBLICATIONS</string>
<string name="accounts">COMPTES</string>
<string name="hashtags">HASHTAGS</string>
@ -99,29 +90,28 @@
<string name="media_upload_failed">{gmd_cloud_off} Échec du téléversement du média, réessayez ou vérifiez l\'état du réseau</string>
<string name="crop_result_error">Impossible de récupérer l\'image après le recadrage</string>
<string name="busy_dialog_ok_button">D\'accord, attendez pour ça.</string>
<string name="busy_dialog_text">Toujours en train de traiter l\'image, attendez que cela se termine en premier !</string>
<string name="busy_dialog_text">Toujours en train de traiter l\'image, attendez tout d\'abord que cela se termine !</string>
<string name="nothing_to_see_here">Rien à voir ici !</string>
<string name="issues_contribute">Rapporter des problèmes ou contribuer à l\'application:</string>
<string name="issues_contribute">Signaler des problèmes ou contribuer à l\'application:</string>
<string name="something_went_wrong">Il y a eu un problème…</string>
<string name="reported">Rapporté {gmd_check_circle}</string>
<string name="report_target">Rapporter la publication de @%1$s</string>
<string name="report">Rapporter</string>
<string name="reported">Signalé {gmd_check_circle}</string>
<string name="report_target">Signaler la publication de @%1$s</string>
<string name="report">Signaler</string>
<string name="image_preview">Aperçu de l\'image en cours de modification</string>
<string name="filter_thumbnail">Vignette d\'un filtre</string>
<string name="click_image_edit">Appuyez sur l\'image pour la modifier</string>
<string name="mascot_description">Image montrant un panda rouge (la mascotte de Pixelfed) qui utilise un téléphone</string>
<string name="help_translate">Aidez pour traduire PixelDroid dans votre langue:</string>
<string name="mascot_description">Image montrant un panda rouge (la mascotte de Pixelfed) utilisant un téléphone</string>
<string name="help_translate">Aidez à traduire PixelDroid dans votre langue:</string>
<string name="language">Langue</string>
<string name="delete_dialog">Supprimer cette publication\?</string>
<string name="delete">Supprimer</string>
<string name="discover">DÉCOUVRIR</string>
<string name="profile_picture">Photo de profil</string>
<string name="toolbar_title_edit">Modifier</string>
<string name="report_error">Échec lors de l\'envoi du rapport</string>
<string name="optional_report_comment">Message facultatif pour les mods/admins</string>
<string name="report_error">Échec lors de l\'envoi du signalement</string>
<string name="optional_report_comment">Message facultatif pour les modérateurs/admins</string>
<string name="share_link">Partager le lien</string>
<string name="status_more_options">Plus d\'options</string>
<string name="search_empty_error">La requête de recherche ne peut pas être vide</string>
<string name="search_empty_error">La recherche ne peut pas être vide</string>
<string name="post_title">Publication de %1$s</string>
<string name="about">À propos</string>
<string name="license_info">PixelDroid est un logiciel libre, sous licence GNU General Public License (version 3 ou ultérieure)</string>
@ -132,7 +122,6 @@
<string name="post_is_album">Cette publication est un album</string>
<string name="submit_comment">Envoyer le commentaire</string>
<string name="add_comment">Ajouter un commentaire</string>
<string name="number_comments">%1$s commentaires</string>
<string name="crop_button">Bouton pour effectuer une rotation ou recadrement de l\'image</string>
<string name="post_image">Une des images de la publication</string>
<string name="add_photo">Ajouter une photo</string>
@ -146,7 +135,48 @@
<string name="save_before_returning">Enregistrer vos modifications\?</string>
<string name="panda_pull_to_refresh_to_try_again">Ce panda est malheureux. Glissez pour actualiser et réessayer.</string>
<string name="open_drawer_menu">Ouvrir le menu latéral</string>
<string name="follows_title">%1$s abonnements</string>
<string name="followers_title">%1$s abonnés</string>
<string name="follows_title">Abonnements de %1$s</string>
<string name="followers_title">Abonné·e·s de %1$s</string>
<string name="verify_credentials">Impossible d\'obtenir les informations sur l\'utilisateur</string>
<string name="size_exceeds_instance_limit">La taille de l\'image n°%1$s dans l\'album dépasse la taille maximale autorisée (%2$s ko mais la limite est %3$s ko). Il se peut que vous ne puissiez pas la télécharger.</string>
<string name="upload_error">Le serveur a retourné le code d\'erreur : %1$s</string>
<string name="total_exceeds_album_limit">Vous avez choisi plus d\'images que le maximum autorisé par votre serveur (%1$s). Les images au delà de la limite ont été ignorées.</string>
<string name="no_media_description">Ajouter une description du média ici…</string>
<string name="save_image_description">Enregistrer la description de l\'image</string>
<string name="api_not_enabled_dialog">L\'API n\'est pas activée sur cette instance. Veuillez contacter votre administrateur pour lui demander de l\'activer.</string>
<string name="whats_an_instance_explanation">Vous risquez d\'être déroutés par le champ demandant le domaine de votre \"instance\".
\n
\nPixelfed est une plateforme fédérée, et fait partie de \"fediverse\", ce qui signifie qu\'elle peut communiquer avec d\'autres plateformes qui parlent la même langue, comme Mastodon (voir https://joinmastodon.org).
\n
\nCela signifie également que vous devez choisir quel serveur ou \"instance\" de Pixelfed utiliser. Si vous n\'en connaissez aucun, vous pouvez regarder ici : https://pixelfed.org/join
\n
\nPour plus d\'informations sur Pixelfed, vous pouvez consulter le site suivant : https://pixelfed.org</string>
<plurals name="nb_following">
<item quantity="one">%d Abonnement</item>
<item quantity="other">%d Abonnements</item>
</plurals>
<plurals name="nb_followers">
<item quantity="one">%d Abonné·e</item>
<item quantity="other">%d Abonné·e·s</item>
</plurals>
<plurals name="nb_posts">
<item quantity="one">%d Publication</item>
<item quantity="other">%d Publications</item>
</plurals>
<plurals name="number_comments">
<item quantity="one">%d commentaire</item>
<item quantity="other">%d commentaires</item>
</plurals>
<plurals name="shares">
<item quantity="one">%d Partage</item>
<item quantity="other">%d Partages</item>
</plurals>
<plurals name="likes">
<item quantity="one">%d J\'aime</item>
<item quantity="other">%d J\'aime</item>
</plurals>
<plurals name="description_max_characters">
<item quantity="one">La description doit contenir au moins %d lettre.</item>
<item quantity="other">La description doit contenir au moins %d lettres.</item>
</plurals>
</resources>

View File

@ -61,12 +61,6 @@
<string name="follow_error">Non se puido seguir</string>
<string name="follow_button_failed">Non se pode mostrar o botón para seguir</string>
<string name="follow_status_failed">Non se obtivo o estado de relacións</string>
<string name="nb_following">%1$s
\nSeguindo</string>
<string name="nb_followers">%1$s
\nSeguidoras</string>
<string name="nb_posts">%1$s
\nPublicacións</string>
<string name="comment">Comentar</string>
<string name="comment_posted">Comentario: %1$s publicado!</string>
<string name="comment_error">Fallo ao comentar!</string>
@ -75,19 +69,16 @@
<string name="write_permission_download_pic">Tes que conceder permiso de escritura para descargar fotos!</string>
<string name="empty_comment">O comentario non debe quedar baleiro!</string>
<string name="posted_on">Publicado o %1$s</string>
<string name="shares">Compartido %1$s</string>
<string name="likes">%1$s Gústame</string>
<string name="no_description">Sen descrición</string>
<string name="feed_failed">Non se obtivo a fonte</string>
<string name="loading_toast">Algo fallou ao realizar a subida</string>
<string name="normal_filter">Normal</string>
<string name="upload_post_error">Fallou a publicación: not 200</string>
<string name="upload_post_error">Erro ao subir a publicación</string>
<string name="upload_post_success">Publicación correcta</string>
<string name="upload_post_failed">Fallou a subida da publicación</string>
<string name="request_format_error">Erro na subida: formato da solicitude non válido</string>
<string name="picture_format_error">Erro na subida: formato de imaxe non válido.</string>
<string name="upload_picture_failed">Erro ao subir a foto!</string>
<string name="description_max_characters">A descrición non pode superar os %1$s caracteres.</string>
<string name="save_image_failed">Non se gardou a imaxe</string>
<string name="permission_denied">Permiso denegado</string>
<string name="dark_theme">Escuro</string>
@ -134,4 +125,58 @@
<string name="language">Idioma</string>
<string name="delete_dialog">Eliminar esta publicación\?</string>
<string name="delete">Eliminar</string>
<string name="verify_credentials">Non se obtivo a información da usuaria</string>
<string name="mascot_description">Imaxe amosando un panda vermello, mascota de Pixelfed, usando un móbil</string>
<plurals name="nb_following">
<item quantity="one">%d seguimento</item>
<item quantity="other">%d seguimentos</item>
</plurals>
<plurals name="nb_followers">
<item quantity="one">%d Seguidora</item>
<item quantity="other">%d Seguidoras</item>
</plurals>
<plurals name="nb_posts">
<item quantity="one">%d Publicación</item>
<item quantity="other">%d Publicacións</item>
</plurals>
<string name="post_is_album">Esta publicación é un álbume</string>
<string name="submit_comment">Enviar comentario</string>
<string name="add_comment">Engadir un comentario</string>
<plurals name="number_comments">
<item quantity="one">%d comentario</item>
<item quantity="other">%d comentarios</item>
</plurals>
<plurals name="shares">
<item quantity="one">%d Compartición</item>
<item quantity="other">%d Comparticións</item>
</plurals>
<plurals name="likes">
<item quantity="one">%d Gústame</item>
<item quantity="other">%d Gústame</item>
</plurals>
<string name="no_cancel_edit">Non, cancela a edición</string>
<string name="save_before_returning">Gardar a edición\?</string>
<string name="crop_button">Botón para recortar ou rotar a imaxe</string>
<string name="image_preview">Vista previa da imaxe a ser editada</string>
<string name="filter_thumbnail">Miniatura do filtro</string>
<string name="upload_error">Código de erro devolto polo servidor: %1$s</string>
<string name="size_exceeds_instance_limit">O tamaño do número de imaxes %1$s no álbume supera o máximo permitido pola instancia (%2$s kB pero o límite é %3$s kB). É posible que non poidas subilas.</string>
<string name="total_exceeds_album_limit">Elexiches máis imaxes das que permite o servidor (%1$s). As imaxes que superan o límite serán ignoradas.</string>
<string name="no_media_description">Engade aquí unha descrición…</string>
<string name="save_image_description">Gardar descrición da imaxe</string>
<string name="switch_to_carousel">Cambiar ao carrusel</string>
<string name="switch_to_grid">Mudar á vista en grella</string>
<string name="post_image">Unha das imaxes da publicación</string>
<plurals name="description_max_characters">
<item quantity="one">A descrición ten que ter %d caracter como moito.</item>
<item quantity="other">A descrición ten que ter %d caracteres como moito.</item>
</plurals>
<string name="api_not_enabled_dialog">O API non está activado nesta instancia. Contacta coa administración e pídelle que o activen.</string>
<string name="whats_an_instance_explanation">Pode que o campo de texto pedíndoche o dominio da túa \'instancia\' sexa confuso.
\n
\nPixelfed é unha plataforma federada, parte do \'fediverso\', e isto significa que pode comunicarse con outras plataformas que utilizan a mesma linguaxe, como Mastodon (see https://joinmastodon.org).
\n
\nTamén significa que tes que escoller un servidor, ou \'instancia\' de Pixelfed para utilizar. Se non coñeces ningunha, mira aquí: https://pixelfed.org/join
\n
\nPara saber máis acerca de Pixelfed, podes mirar aquí: https://pixelfed.org</string>
</resources>

View File

@ -48,7 +48,6 @@
<string name="picture_format_error">Errore di caricamento: formato immagine errato.</string>
<string name="add_account_name">Aggiungi Account</string>
<string name="add_account_description">Aggiungi un altro account Pixelfed</string>
<string name="description_max_characters">La descrizione deve contenere al massimo %1$s caratteri.</string>
<string name="loading_toast">Qualcosa è andato storto durante il caricamento</string>
<string name="write_permission_download_pic">È necessario concedere i permessi di scrittura per scaricare le immagini!</string>
<string name="request_format_error">Errore di caricamento: formato di richiesta errato</string>
@ -58,8 +57,6 @@
<string name="normal_filter">Normale</string>
<string name="feed_failed">Impossibile ottenere il feed</string>
<string name="no_description">Nessuna descrizione</string>
<string name="likes">%1$s Mi piace</string>
<string name="shares">%1$s Condivisioni</string>
<string name="posted_on">Postato su %1$s</string>
<string name="empty_comment">Il commento non deve essere vuoto!</string>
<string name="write_permission_share_pic">È necessario concedere i permessi di scrittura per condividere le immagini!</string>
@ -67,10 +64,6 @@
<string name="comment_error">Errore nel commento!</string>
<string name="comment_posted">Commento: %1$s postato!</string>
<string name="comment">Commento</string>
<string name="nb_followers">%1$s
\nSeguaci</string>
<string name="nb_following">%1$s
\nSeguiti</string>
<string name="follow_error">Impossibile seguire</string>
<string name="unfollow_error">Impossibile smettere di seguire</string>
<string name="access_token_invalid">Il token di accesso non è valido</string>
@ -88,8 +81,6 @@
<string name="accounts">ACCOUNTS</string>
<string name="hashtags">HASHTAGS</string>
<string name="posting_image_accessibility_hint">Immagine in corso di pubblicazione</string>
<string name="nb_posts">%1$s
\nPosts</string>
<string name="action_not_allowed">Questa azione non è consentita</string>
<string name="upload_post_error">Errore di caricamento del post</string>
<string name="media_upload_completed">{gmd_cloud_done} Caricamento contenuti completato</string>
@ -138,11 +129,50 @@
<string name="post_is_album">Questo post è un album</string>
<string name="submit_comment">Invia commento</string>
<string name="add_comment">Aggiungi un commento</string>
<string name="number_comments">%1$s commenti</string>
<string name="crop_button">Pulsante per ritagliare o ruotare l\'immagine</string>
<string name="image_preview">Anteprima dell\'immagine in fase di modifica</string>
<string name="filter_thumbnail">Miniatura del filtro</string>
<string name="click_image_edit">Clicca sull\'immagine per modificarla</string>
<string name="post_image">Una delle immagini nel post</string>
<string name="verify_credentials">Impossibile ottenere le informazioni sull\'utente</string>
<string name="switch_to_grid">Passa alla visualizzazione a griglia</string>
<string name="no_cancel_edit">No, cancella la modifica</string>
<string name="save_before_returning">Salavre le tue modifiche\?</string>
<string name="switch_to_carousel">Passa alla panoramica</string>
<string name="whats_an_instance_explanation">Potresti essere confuso dal campo di testo che chiede il dominio della tua \'instanza\'.
\n
\nPixelfed è una piattaforma federata, e parte del \'fediverso\', il che significa che può comunicare con altre piattaforme che parlano la stessa lingua, come Mastodon (vedi https://joinmastodon.org).
\n
\nSignifica anche che è necessario scegliere quale server, o \'istanza\' di Pixelfed da utilizzare. Se non ne conosci uno, puoi guardare qui: https://pixelfed.org/join
\n
\nPer maggiori informazioni su Pixelfed, puoi controllare qui: https://pixelfed.org</string>
<plurals name="nb_followers">
<item quantity="one">%d Seguace</item>
<item quantity="other">%d Seguaci</item>
</plurals>
<plurals name="nb_posts">
<item quantity="one">%d Post</item>
<item quantity="other">%d Post</item>
</plurals>
<plurals name="number_comments">
<item quantity="one">%d commento</item>
<item quantity="other">%d commenti</item>
</plurals>
<plurals name="shares">
<item quantity="one">%d Condivisione</item>
<item quantity="other">%d Condivisioni</item>
</plurals>
<plurals name="likes">
<item quantity="one">%d Mi piace</item>
<item quantity="other">%d Mi piace</item>
</plurals>
<string name="upload_error">Codice di errore restituito dal server: %1$s</string>
<string name="size_exceeds_instance_limit">La dimensione del numero dell\'immagine %1$s nell\'album supera la dimensione massima consentita dall\'istanza (%2$s kB ma il limite è %3$s kB). Potresti non essere in grado di caricarla.</string>
<string name="total_exceeds_album_limit">Hai scelto più immagini del massimo che il server consente (%1$s). Le immagini oltre il limite sono state ignorate.</string>
<string name="no_media_description">Aggiungi una descrizione dell\'immagine qui…</string>
<string name="save_image_description">Salva la descrizione dell\'immagine</string>
<plurals name="description_max_characters">
<item quantity="one">La descrizione deve contenere al massimo %d carattere</item>
<item quantity="other">La descrizione deve contenere al massimo %d caratteri</item>
</plurals>
<string name="api_not_enabled_dialog">L\'API non è attivata su questa istanza. Contatta l\'amministratore per chiedergli di attivarlo.</string>
</resources>

View File

@ -46,9 +46,6 @@
<string name="write_permission_share_pic">画像を共有するためには書き込みの権限を更新する必要があります</string>
<string name="write_permission_download_pic">画像をダウンロードするには書き込みの権限を更新する必要があります</string>
<string name="empty_comment">コメントは空欄にできません</string>
<string name="shares">%1$s再共有</string>
<string name="likes">%1$sお気に入り</string>
<string name="description_max_characters">説明には最低 %1$s文字以上含める必要があります。</string>
<string name="comment_posted">コメント: %1$s が投稿されました</string>
<string name="retry">再試行</string>
<string name="posting_image_accessibility_hint">投稿された画像</string>
@ -72,12 +69,6 @@
<string name="follow_error">フォローできませんでした</string>
<string name="follow_button_failed">フォローボタンを表示できませんでした</string>
<string name="follow_status_failed">フォローステータスを取得できませんでした</string>
<string name="nb_following">%1$s
\nフォロー</string>
<string name="nb_followers">%1$s
\nフォロワー</string>
<string name="nb_posts">%1$s
\n投稿</string>
<string name="comment">コメント</string>
<string name="comment_error">コメントエラー</string>
<string name="share_image">画像を共有</string>

View File

@ -1,5 +0,0 @@
<resources>
<style name="AppTheme.Launcher">
<item name="android:windowBackground">@drawable/theme_night</item>
</style>
</resources>

View File

@ -41,7 +41,6 @@
<string name="add_account_description">Andere Pixelfed account toevoegen</string>
<string name="add_account_name">Account toevoegen</string>
<string name="upload_picture_failed">Fout bij het uploaden!</string>
<string name="description_max_characters">De beschrijving mag maximaal %1$s karakters bevatten.</string>
<string name="dark_theme">Donker</string>
<string name="light_theme">Licht</string>
<string name="search">Zoeken</string>
@ -53,10 +52,6 @@
<string name="default_nfollowers">-
\nVolgers</string>
<string name="action_not_allowed">Deze actie is niet toegestaan</string>
<string name="nb_following">%1$s
\nVolgend</string>
<string name="nb_followers">%1$s
\nVolgers</string>
<string name="empty_comment">Commentaar mag niet leeg zijn!</string>
<string name="no_description">Geen beschrijving</string>
<string name="loading_toast">Er ging iets mis tijdens het laden</string>
@ -75,9 +70,7 @@
<string name="permission_denied">Toestemming geweigerd</string>
<string name="default_system">Standaard (systeem instelling)</string>
<string name="instance_error">Kon instance-informatie niet ophalen</string>
<string name="likes">%1$s Vind-ik-leuks</string>
<string name="request_format_error">Fout bij het uploaden: slecht verzoekformaat</string>
<string name="shares">%1$s keer gedeeld</string>
<string name="posted_on">Gepost op %1$s</string>
<string name="comment">Commentaar maken</string>
<string name="comment_posted">Commentaar: %1$s gepost!</string>
@ -86,8 +79,6 @@
<string name="write_permission_share_pic">Je moet schrijven toestaan om afbeeldingen te delen!</string>
<string name="write_permission_download_pic">Je moet schrijven toestaan om afbeeldingen te downloaden!</string>
<string name="follow_status_failed">Kon volgstatus niet ophalen</string>
<string name="nb_posts">%1$s
\nPosts</string>
<string name="busy_dialog_ok_button">Ok, wacht.</string>
<string name="crop_result_error">Kon de afbeelding niet ophalen na bijsnijden</string>
<string name="busy_dialog_text">Bezig met het verwerken van de afbeelding, wacht even tot dat het klaar is!</string>
@ -129,11 +120,10 @@
<string name="post_is_album">Deze post is een album</string>
<string name="submit_comment">Commentaar versturen</string>
<string name="add_comment">Commentaar toevoegen</string>
<string name="number_comments">%1$s commentaren</string>
<string name="click_image_edit">Klik op de afbeelding om het te bewerken</string>
<string name="add_photo">Foto toevoegen</string>
<string name="instance_not_pixelfed_cancel">Aanmelding annuleren</string>
<string name="instance_not_pixelfed_continue">OK, toch verdergaan</string>
<string name="instance_not_pixelfed_warning">Dit is geen Pixelfed instantie, dus de app zou zich op onverwachte wijze kunnen gedragen.</string>
<string name="report">Melden</string>
<string name="poll_notification">De peiling van %1$s is beëindigd</string>
</resources>

View File

@ -45,8 +45,6 @@
<string name="image_download_downloading">Pobieranie…</string>
<string name="image_download_success">Pobieranie zakończone powodzeniem</string>
<string name="no_description">Bez opisu</string>
<string name="likes">Polubienia: %1$s</string>
<string name="shares">Udostępnienia: %1$s</string>
<string name="posted_on">Opublikowano %1$s</string>
<string name="empty_comment">Komentarz nie może być pusty!</string>
<string name="write_permission_share_pic">Musisz nadać uprawnienia do zapisu, aby móc udostępniać obrazki!</string>
@ -64,7 +62,6 @@
<string name="whats_an_instance">Co to jest instancja\?</string>
<string name="login_connection_required_once">Musisz być online, aby dodać konto i korzystać z PixelDroida :(</string>
<string name="permission_denied">Brak dostępu</string>
<string name="description_max_characters">Opis może zawierać najwyżej %1$s znaków.</string>
<string name="picture_format_error">Błąd wgrywania na serwer: niewłaściwy format pliku.</string>
<string name="upload_post_error">Nie udało się opublikować posta</string>
<string name="busy_dialog_text">Przetwarzam obrazek, zaczekaj aż skończę!</string>

View File

@ -41,4 +41,60 @@
<string name="menu_settings">Configurações</string>
<string name="menu_account">Meu Perfil</string>
<string name="app_name">PixelDroid</string>
<string name="no_cancel_edit">Não, cancelar edição</string>
<string name="help_translate">Ajude a traduzir o PixelDroid para a sua língua:</string>
<string name="delete">Apagar</string>
<string name="discover">DESCOBRIR</string>
<string name="toolbar_title_edit">Editar</string>
<string name="share_link">Partilhar Ligação</string>
<string name="report">Reportar</string>
<string name="status_more_options">Mais opções</string>
<string name="about">Sobre</string>
<string name="project_website">Website do projeto: https://pixeldroid.org</string>
<string name="about_pixeldroid">Sobre PixelDroid</string>
<string name="retry">Tentar novamente</string>
<string name="search">Pesquisar</string>
<string name="edit_profile">Editar perfil</string>
<string name="follow">Seguir</string>
<string name="default_nfollowers">-
\nSeguidores</string>
<plurals name="nb_followers">
<item quantity="one">%d Seguidor</item>
<item quantity="other">%d Seguidores</item>
</plurals>
<string name="submit_comment">Submeter comentário</string>
<string name="add_comment">Adicionar comentário</string>
<plurals name="number_comments">
<item quantity="one">%d comentário</item>
<item quantity="other">%d comentários</item>
</plurals>
<string name="share_image">Partilhar Imagem</string>
<plurals name="shares">
<item quantity="one">%d Partilha</item>
<item quantity="other">%d Partilhas</item>
</plurals>
<plurals name="likes">
<item quantity="one">%d Gosto</item>
<item quantity="other">%d Gostos</item>
</plurals>
<string name="no_description">Sem descrição</string>
<string name="feed_failed">Não foi possível obter feed</string>
<string name="crop_button">Botão para cortar ou rodar imagem</string>
<string name="image_preview">Pré-visualização da imagem editada</string>
<string name="busy_dialog_text">A processar imagem, espere que termine primeiro!</string>
<string name="normal_filter">Normal</string>
<string name="save_image_description">Guardar descrição da imagem</string>
<string name="post_image">Uma das imagens na publicação</string>
<string name="add_photo">Adicionar foto</string>
<string name="upload_post_error">Erro ao publicar</string>
<string name="upload_post_success">Publicado com sucesso</string>
<string name="upload_post_failed">Falha ao publicar</string>
<string name="picture_format_error">Erro de publicação: formato errado de imagem.</string>
<string name="upload_picture_failed">Erro ao publicar imagem!</string>
<string name="save_image_success">Imagem guardada com sucesso</string>
<string name="save_image_failed">Não foi possível guardar a imagem</string>
<string name="permission_denied">Permissão recusada</string>
<string name="dark_theme">Escuro</string>
<string name="light_theme">Claro</string>
<string name="verify_credentials">Não foi possível obter informação do utilizador</string>
</resources>

View File

@ -3,7 +3,7 @@
<string name="auth_failed">Не удалось авторизоваться</string>
<string name="token_error">Ошибка получения токена</string>
<string name="title_activity_settings2">Настройки</string>
<string name="theme_title">Тема приложения</string>
<string name="theme_title">Тема Приложения</string>
<string name="theme_header">Тема</string>
<string name="followed_notification">%1$s подписался(-лась) на вас</string>
<string name="mention_notification">%1$s упомянул(а) вас</string>
@ -17,8 +17,8 @@
<string name="tab_filters">ФИЛЬТРЫ</string>
<string name="edit">РЕДАКТИРОВАТЬ</string>
<string name="save_to_gallery">Сохранить в Галерею…</string>
<string name="image_download_downloading">Сохранение</string>
<string name="image_download_success">Изображение успешно сохранено</string>
<string name="image_download_downloading">Загрузка</string>
<string name="image_download_success">Изображение успешно загружено</string>
<string name="switch_camera_button_alt">Переключить камеру</string>
<string name="gallery_button_alt">Галерея</string>
<string name="NoCommentsToShow">Нет комментариев…</string>
@ -35,56 +35,47 @@
<string name="share_picture">Поделиться изображением…</string>
<string name="login_connection_required_once">Вам необходимо быть в сети что бы добавить аккаунт и использовать PixelDroid :(</string>
<string name="cw_nsfw_hidden_media_n_click_to_show">CW / NSFW / Скрытое медиа
\n(кликните что бы показать)</string>
\n(кликните что бы показать 18+)</string>
<string name="registration_failed">Не удалось зарегистрировать приложение на этом инстансе</string>
<string name="capture_button_alt">Сделать снимок</string>
<string name="add_account_description">Добавить другой аккаунт Pixelfed</string>
<string name="add_account_name">Добавить аккаунт</string>
<string name="instance_error">Не удалось получить информацию об инстансе</string>
<string name="nb_following">%1$s
\nПодписки</string>
<string name="nb_followers">%1$s
\nПодписчики</string>
<string name="nb_posts">%1$s
\nПосты</string>
<string name="comment">Комментарий</string>
<string name="comment_posted">Комментарий: %1$s опубликован!</string>
<string name="comment_error">Ошибка при добавлении комментария!</string>
<string name="share_image">Поделиться изображением</string>
<string name="write_permission_download_pic">Вы должны дать разрешение на запись файлов чтобы сохранять изображения!</string>
<string name="write_permission_share_pic">Вы должны дать разрешение на запись файлов чтобы делиться изображениями!</string>
<string name="comment_error">Ошибка комментария!</string>
<string name="share_image">Поделиться Изображением</string>
<string name="write_permission_download_pic">Вы должны дать разрешение на запись для загрузки изображений!</string>
<string name="write_permission_share_pic">Вы должны дать разрешение на запись чтобы делиться изображениями!</string>
<string name="empty_comment">Комментарий не может быть пустым!</string>
<string name="posted_on">Опубликовано в %1$s</string>
<string name="shares">%1$s репостов</string>
<string name="likes">%1$s лайков</string>
<string name="no_description">Нет описания</string>
<string name="feed_failed">Не удалось загрузить ленту</string>
<string name="loading_toast">При загрузке что-то пошло не так</string>
<string name="normal_filter">Обычный</string>
<string name="upload_post_error">Не удалось загрузить пост</string>
<string name="upload_post_error">Ошибка загрузки поста</string>
<string name="upload_post_success">Пост успешно загружен</string>
<string name="upload_post_failed">Не удалось загрузить пост</string>
<string name="request_format_error">Ошибка загрузки: некорректный формат запроса</string>
<string name="picture_format_error">Ошибка загрузки: неверный формат изображения.</string>
<string name="upload_picture_failed">Ошибка загрузки изображения!</string>
<string name="description_max_characters">Описание должно содержать максимум %1$s символов.</string>
<string name="save_image_success">Изображение успешно сохранено</string>
<string name="save_image_failed">Не удалось сохранить изображение</string>
<string name="permission_denied">Недостаточно прав</string>
<string name="permission_denied">Отказано в доступе</string>
<string name="dark_theme">Тёмная</string>
<string name="light_theme">Светлая</string>
<string name="default_system">По умолчанию (как в системе)</string>
<string name="default_system">По умолчанию (Как в системе)</string>
<string name="follow_status_failed">Не удалось получить статус подписки</string>
<string name="follow_error">Не удалось подписаться</string>
<string name="action_not_allowed">Это действие запрещено</string>
<string name="follow_error">Подписка не удалась</string>
<string name="action_not_allowed">Это действие недопустимо</string>
<string name="unfollow_error">Не удалось отписаться</string>
<string name="access_token_invalid">Недействительный токен доступа</string>
<string name="access_token_invalid">Токен доступа недействителен</string>
<string name="default_nposts">-
\nПосты</string>
<string name="default_nfollowers">-
\nПодписчики</string>
<string name="default_nfollowing">-
\nПодписки</string>
\nПодписан</string>
<string name="no_username">Нет имени пользователя</string>
<string name="follow">Подписаться</string>
<string name="edit_profile">Редактировать профиль</string>
@ -94,15 +85,15 @@
<string name="hashtags">ХЭШТЕГИ</string>
<string name="follow_button_failed">Не удалось отобразить кнопку подписки</string>
<string name="posting_image_accessibility_hint">Изображение, которое будет опубликовано</string>
<string name="media_upload_completed">{gmd_cloud_done} Загрузка завершена</string>
<string name="retry">Попробовать снова</string>
<string name="media_upload_failed">{gmd_cloud_off} Загрузка не удалась, попробуйте снова или проверьте сеть</string>
<string name="nothing_to_see_here">Здесь ничего нет!</string>
<string name="media_upload_completed">{gmd_cloud_done} Загрузка медиа завершена</string>
<string name="retry">Ещё раз</string>
<string name="media_upload_failed">{gmd_cloud_off} Загрузка медиа не удалась, попробуйте снова или проверьте сеть</string>
<string name="nothing_to_see_here">Здесь нечего смотреть!</string>
<string name="crop_result_error">Не удалось получить изображение после обрезки</string>
<string name="busy_dialog_ok_button">Хорошо, подожду.</string>
<string name="busy_dialog_ok_button">Ок, подожду.</string>
<string name="busy_dialog_text">Изображение обрабатывается, пожалуйста, ожидайте!</string>
<string name="open_drawer_menu">Открыть навигационное меню</string>
<string name="panda_pull_to_refresh_to_try_again">Панде грустно. Потяни, чтобы обновить.</string>
<string name="open_drawer_menu">Открыть меню навигации</string>
<string name="panda_pull_to_refresh_to_try_again">Эта панда несчастлива. Потяни, чтобы обновить.</string>
<string name="something_went_wrong">Что-то пошло не так…</string>
<string name="discover">ОБЗОР</string>
<string name="profile_picture">Изображение профиля</string>
@ -115,9 +106,9 @@
<string name="report">Пожаловаться</string>
<string name="status_more_options">Больше опций</string>
<string name="search_empty_error">Поисковый запрос не может быть пустым</string>
<string name="follows_title">Подписки %1$s</string>
<string name="followers_title">Подписчики %1$s</string>
<string name="post_title">Пост %1$s</string>
<string name="follows_title">%1$s подписок</string>
<string name="followers_title">%1$s подписчиков</string>
<string name="post_title">%1$s пост</string>
<string name="about">О приложении</string>
<string name="license_info">PixelDroid это свободное ПО с открытым исходным кодом, выпускаемое под лицензией GNU General Public License (версии 3 или новее)</string>
<string name="project_website">Сайт проекта: https://pixeldroid.org</string>
@ -125,8 +116,77 @@
<string name="about_pixeldroid">О PixelDroid</string>
<string name="unfollow">Отписаться</string>
<string name="add_photo">Добавить фото</string>
<string name="poll_notification">Опрос %1$s завершён</string>
<string name="instance_not_pixelfed_cancel">Отмена</string>
<string name="instance_not_pixelfed_continue">OK, всё равно продолжить</string>
<string name="poll_notification">%1$s завершил опрос</string>
<string name="instance_not_pixelfed_cancel">Отмена входа</string>
<string name="instance_not_pixelfed_continue">OK, продолжить всё равно</string>
<string name="instance_not_pixelfed_warning">Это не похоже на инстанс Pixelfed, приложение может работать нестабильно.</string>
<string name="save_image_description">Сохранить описание изображения</string>
<string name="post_image">Одно из изображений в посте</string>
<string name="verify_credentials">Невозможно получить информацию о пользователе</string>
<plurals name="description_max_characters">
<item quantity="one">Описание должно содержать максимум %d символ.</item>
<item quantity="few">Описание должно содержать максимум %d символа.</item>
<item quantity="many">Описание должно содержать максимум %d символов.</item>
<item quantity="other">Описание должно содержать максимум %d символов.</item>
</plurals>
<string name="no_cancel_edit">Нет, отменить редактирование</string>
<string name="save_before_returning">Сохранить ваши редактирования\?</string>
<string name="mascot_description">Изображение красной панды, талисман Pixelfed , использующей телефон</string>
<string name="issues_contribute">Сообщайте о проблемах или вносите свой вклад в приложение:</string>
<string name="help_translate">Помогите перевести PixelDroid на ваш язык:</string>
<string name="language">Язык</string>
<string name="delete_dialog">Удалить этот пост\?</string>
<string name="delete">Удалить</string>
<plurals name="nb_following">
<item quantity="one">%d Подписка</item>
<item quantity="few">%d Подписки</item>
<item quantity="many">%d Подписок</item>
<item quantity="other">%d Подписок</item>
</plurals>
<plurals name="nb_followers">
<item quantity="one">%d Подписчик</item>
<item quantity="few">%d Подписчика</item>
<item quantity="many">%d Подписчиков</item>
<item quantity="other">%d Подписчиков</item>
</plurals>
<plurals name="nb_posts">
<item quantity="one">%d Пост</item>
<item quantity="few">%d Поста</item>
<item quantity="many">%d Постов</item>
<item quantity="other">%d Постов</item>
</plurals>
<string name="post_is_album">Этот пост в альбоме</string>
<string name="submit_comment">Отправить комментарий</string>
<string name="add_comment">Добавить комментарий</string>
<plurals name="number_comments">
<item quantity="one">%d комментарий</item>
<item quantity="few">%d комментария</item>
<item quantity="many">%d комментариев</item>
<item quantity="other">%d комментариев</item>
</plurals>
<plurals name="shares">
<item quantity="one">%d Репост</item>
<item quantity="few">%d Репоста</item>
<item quantity="many">%d Репостов</item>
<item quantity="other">%d Репостов</item>
</plurals>
<plurals name="likes">
<item quantity="one">%d Лайк</item>
<item quantity="few">%d Лайка</item>
<item quantity="many">%d Лайков</item>
<item quantity="other">%d Лайков</item>
</plurals>
<string name="image_preview">Предварительный просмотр редактируемого изображения</string>
<string name="filter_thumbnail">Фильтр миниатюр</string>
<string name="no_media_description">Добавить описание медиа файла здесь…</string>
<string name="switch_to_carousel">Показывать в режиме «карусели»</string>
<string name="whats_an_instance_explanation">Вас может смутить текстовое поле, запрашивающее доменное имя вашего \'инстанса\'.
\n
\nPixelfed это федеративная платформа и часть \"федиверса\", что означает, что она может общаться с другими платформами, говорящими на том же языке, как например Mastodon (см. https://joinmastodon.org).
\n
\nЭто также означает, что вы должны выбрать, какой сервер или \'инстанс\' Pixelfed использовать. Если вы еще ничего не знаете об этом, перейдите по ссылке: https://pixelfed.org/join
\n
\nДополнительную информации о Pixelfed вы можете посмотреть здесь: https://pixelfed.org</string>
<string name="crop_button">Кнопка для обрезки или поворота изображения</string>
<string name="switch_to_grid">Переключить в виде сетки</string>
</resources>

View File

@ -64,12 +64,6 @@
<string name="follow_error">Kunde inte följa</string>
<string name="follow_button_failed">Kunde inte visa följarknapp</string>
<string name="follow_status_failed">Kunde inte hämta status för följare</string>
<string name="nb_following">%1$s
\nFöljer</string>
<string name="nb_followers">%1$s
\nFöljare</string>
<string name="nb_posts">%1$s
\nInlägg</string>
<string name="comment">Kommentera</string>
<string name="comment_posted">Kommentar: %$s inlagd!</string>
<string name="comment_error">Kommentarsfel!</string>
@ -78,8 +72,6 @@
<string name="write_permission_download_pic">Du måste tillåta skrivrättigheter för att ladda ned bilder!</string>
<string name="empty_comment">Kommentaren får inte vara tom!</string>
<string name="posted_on">Inlagt på %1$s</string>
<string name="shares">%1$s delningar</string>
<string name="likes">%1$s gillningar</string>
<string name="no_description">Ingen beskrivning</string>
<string name="feed_failed">Kunde inte hämta flöde</string>
<string name="loading_toast">Något gick fel vid laddningen</string>
@ -90,7 +82,6 @@
<string name="request_format_error">Uppladdningsfel: felaktig begäran</string>
<string name="picture_format_error">Uppladdningsfel: fel bildformat.</string>
<string name="upload_picture_failed">Fel vid uppladdning av bild!</string>
<string name="description_max_characters">Beskrivningen måste minst innehålla %1$s tecken.</string>
<string name="save_image_success">Bild sparades</string>
<string name="save_image_failed">Kunde inte spara bild</string>
<string name="permission_denied">Åtkomst nekas</string>

View File

@ -64,12 +64,6 @@
<string name="follow_error">无法关注</string>
<string name="follow_button_failed">无法显示关注按钮</string>
<string name="follow_status_failed">无法获得关注状态</string>
<string name="nb_following">%1$s
\n正在关注</string>
<string name="nb_followers">%1$s
\n关注者</string>
<string name="nb_posts">%1$s
\n帖文</string>
<string name="comment">评论</string>
<string name="comment_posted">评论: %1$s 已发布!</string>
<string name="comment_error">评论错误!</string>
@ -78,19 +72,16 @@
<string name="write_permission_download_pic">您需要允许读写权限才能下载图片!</string>
<string name="empty_comment">评论不能为空!</string>
<string name="posted_on">发表于 %1$s</string>
<string name="shares">%1$s 次分享</string>
<string name="likes">%1$s 个赞</string>
<string name="no_description">没有描述</string>
<string name="feed_failed">无法获取订阅</string>
<string name="loading_toast">加载时出了一些问题</string>
<string name="normal_filter">正常</string>
<string name="upload_post_error">帖文上传失败</string>
<string name="upload_post_error">帖文上传错误</string>
<string name="upload_post_failed">帖文上传失败</string>
<string name="upload_post_success">帖文成功上传</string>
<string name="request_format_error">上传错误:错误的请求格式</string>
<string name="picture_format_error">上传错误:图片格式错误。</string>
<string name="upload_picture_failed">图片上传错误!</string>
<string name="description_max_characters">描述最多只能包含 %1$s 个字符。</string>
<string name="save_image_success">图片成功保存</string>
<string name="save_image_failed">无法保存图像</string>
<string name="permission_denied">没有权限</string>
@ -101,4 +92,17 @@
<string name="crop_result_error">图像裁剪后无法恢复</string>
<string name="busy_dialog_ok_button">好的</string>
<string name="busy_dialog_text">图像仍在处理中,请先等待完成!</string>
<string name="post_image">帖文中的一张图片</string>
<string name="add_photo">添加照片</string>
<string name="poll_notification">%1$s投票已经结束</string>
<string name="instance_not_pixelfed_cancel">取消登录</string>
<string name="instance_not_pixelfed_continue">了解,请继续</string>
<string name="instance_not_pixelfed_warning">这似乎不是一个 Pixelfed 实例,可能会导致应用意外退出</string>
<string name="verify_credentials">无法获得用户信息</string>
<string name="about">关于</string>
<string name="project_website">项目主页https://pixeldroid.org</string>
<string name="unfollow">取消关注</string>
<string name="about_pixeldroid">关于 PixelDroid</string>
<string name="share_link">分享链接</string>
<string name="delete">删除</string>
</resources>

View File

@ -28,18 +28,29 @@
<!-- Login page -->
<string name="whats_an_instance">"What's an instance?"</string>
<string name="whats_an_instance_explanation">"You might be confused by the text field asking for the domain of your 'instance'.
Pixelfed is a federated platform, and part of the 'fediverse', which means it can talk to other platforms which speak the same language, like Mastodon (see https://joinmastodon.org).
It also means you have to choose which server, or 'instance' of Pixelfed to use. If you don't know any, you can look here: https://pixelfed.org/join
For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="domain_of_your_instance">Domain of your instance</string>
<string name="connect_to_pixelfed">Connect to Pixelfed</string>
<string name="login_connection_required_once">You need to be online to be able to add the first account and use PixelDroid :(</string>
<string name="add_account_name">Add Account</string>
<string name="add_account_description">Add another Pixelfed Account</string>
<string name="api_not_enabled_dialog">The API is not activated on this instance. Contact your administrator to ask them to activate it.</string>
<!-- Drawer -->
<string name="logout">Log out</string>
<string name="add_account_name">Add Account</string>
<string name="add_account_description">Add another Pixelfed Account</string>
<!-- Post creation -->
<string name="permission_denied">Permission denied</string>
<string name="save_image_failed">Unable to save image</string>
<string name="save_image_success">Image successfully saved</string>
<string name="description_max_characters">"Description must contain %1$s characters at most."</string>
<plurals name="description_max_characters">
<item quantity="one">"Description must contain %d character at most."</item>
<item quantity="other">"Description must contain %d characters at most."</item>
</plurals>
<string name="upload_picture_failed">Picture upload error!</string>
<string name="picture_format_error">Upload error: wrong picture format.</string>
<string name="request_format_error">Upload error: bad request format</string>
@ -50,7 +61,15 @@
<string name="post">post</string>
<string name="add_photo">Add a photo</string>
<string name="post_image">One of the images in the post</string>
<string name="click_image_edit">Click the image to edit it</string>
<string name="switch_to_grid">Switch to grid view</string>
<string name="switch_to_carousel">Switch to carousel</string>
<string name="save_image_description">Save image description</string>
<string name="no_media_description">Add a media description here…</string>
<string name="total_exceeds_album_limit">You chose more images than the maximum your server allows (%1$s). Images beyond the limit have been ignored.</string>
<string name="size_exceeds_instance_limit">Size of image number %1$s in the album exceeds the maximum size allowed by the instance (%2$s kB but the limit is %3$s kB). You might not be able to upload it.</string>
<string name="upload_error">Error code returned by server: %1$s</string>
<!-- Post editing -->
<string name="lbl_brightness">BRIGHTNESS</string>
<string name="lbl_contrast">CONTRAST</string>
@ -64,6 +83,9 @@
<string name="crop_result_error">"Couldn't retrieve image after crop"</string>
<string name="image_preview">Preview of the image being edited</string>
<string name="crop_button">Button to crop or rotate the image</string>
<string name="save_before_returning">Save your edits?</string>
<string name="no_cancel_edit">No, cancel edit</string>
<!-- Camera -->
<string name="capture_button_alt">Capture</string>
<string name="switch_camera_button_alt">Switch camera</string>
@ -79,8 +101,14 @@
<string name="image_download_success">Image downloaded successfully</string>
<!-- Post attributes -->
<string name="no_description">No description</string>
<string name="likes">"%1$s Likes"</string>
<string name="shares">"%1$s Shares"</string>
<plurals name="likes">
<item quantity="one">%d Like</item>
<item quantity="other">%d Likes</item>
</plurals>
<plurals name="shares">
<item quantity="one">%d Share</item>
<item quantity="other">%d Shares</item>
</plurals>
<string name="posted_on">"Posted on %1$s"</string>
<string name="NoCommentsToShow">No comments on this post…</string>
<string name="empty_comment">Comment must not be empty!</string>
@ -90,14 +118,32 @@
<string name="comment_error">Comment error!</string>
<string name="comment_posted">"Comment: %1$s posted!"</string>
<string name="comment">Comment</string>
<string name="number_comments">%1$s comments</string>
<plurals name="number_comments">
<item quantity="one">%d comment</item>
<item quantity="other">%d comments</item>
</plurals>
<string name="add_comment">Add a comment</string>
<string name="submit_comment">Submit comment</string>
<string name="post_is_album">This post is an album</string>
<!-- Profile page -->
<string name="nb_posts">"%1$s\nPosts"</string>
<string name="nb_followers">"%1$s\nFollowers"</string>
<string name="nb_following">"%1$s\nFollowing"</string>
<plurals name="nb_posts">
<item quantity="one">"%d
Post"</item>
<item quantity="other">"%d
Posts"</item>
</plurals>
<plurals name="nb_followers">
<item quantity="one">"%d
Follower"</item>
<item quantity="other">"%d
Followers"</item>
</plurals>
<plurals name="nb_following">
<item quantity="one">"%d
Following"</item>
<item quantity="other">"%d
Following"</item>
</plurals>
<string name="follow_status_failed">Could not get follow status</string>
<string name="follow_button_failed">Could not display follow button</string>
<string name="follow_error">Could not follow</string>
@ -117,7 +163,7 @@
<string name="accounts">ACCOUNTS</string>
<string name="hashtags">HASHTAGS</string>
<!-- Sensitive media -->
<string name="cw_nsfw_hidden_media_n_click_to_show">CW / NSFW / Hidden Media \n (click to show)</string>
<string name="cw_nsfw_hidden_media_n_click_to_show">CW / NSFW / Hidden Media\n (click to show)</string>
<!-- Shown when image has finished uploading. {gmd_cloud_done} is an icon, position it as is appropriate in target language -->
<string name="media_upload_completed">{gmd_cloud_done} Media upload completed</string>
<!-- Shown when image uploading has failed. {gmd_cloud_off} is an icon, position it as is appropriate in target language -->
@ -154,11 +200,4 @@
<string name="help_translate">Help translate PixelDroid to your language:</string>
<string name="issues_contribute">Report issues or contribute to the application:</string>
<string name="mascot_description">Image showing a red panda, Pixelfed\'s mascot, using a phone</string>
<string name="save_before_returning">Save your edits?</string>
<string name="no_cancel_edit">No, cancel edit</string>
<string name="switch_to_grid">Switch to grid view</string>
<string name="switch_to_carousel">Switch to carousel</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
</resources>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>

View File

@ -31,7 +31,7 @@ class APIUnitTest {
visibility=Status.Visibility.public, sensitive=false, spoiler_text="",
media_attachments= listOf(Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg",
preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg",
remote_url=null, text_url=null, description=null, blurhash=null)),
remote_url=null, text_url=null, description=null, blurhash=null, meta = null)),
application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(),
tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)),
emojis= emptyList(), reblogs_count=0, favourites_count=0, replies_count=0, url="https://pixelfed.de/p/Miike/140364967936397312",

View File

@ -20,7 +20,7 @@ class PostUnitTest {
media_attachments= listOf(
Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg",
preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg",
remote_url=null, text_url=null, description=null, blurhash=null)
remote_url=null, text_url=null, description=null, blurhash=null, meta = null)
),
application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(),
tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)),