diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ad20fb06..5f10e24d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,6 +2,7 @@ image: registry.gitlab.com/fdroid/fdroidserver:buildserver-bullseye
variables:
GIT_SUBMODULE_STRATEGY: recursive
+ GIT_SUBMODULE_FORCE_HTTPS: "true"
before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle
@@ -19,7 +20,7 @@ before_script:
- test -e $cmdline_tools_latest && export PATH="$cmdline_tools_latest:$PATH"
- export GRADLE_USER_HOME=$PWD/.gradle
- - export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle`
+ - export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdk\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle`
- echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null
- apt-get update || apt-get update
diff --git a/.gitmodules b/.gitmodules
index 09e1d5f5..771ec4a3 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "scrambler"]
path = scrambler
url = https://gitlab.com/artectrex/scrambler.git
+[submodule "pixel_common"]
+ path = pixel_common
+ url = git@gitlab.shinice.net:pixeldroid/pixel_common.git
diff --git a/app/build.gradle b/app/build.gradle
index 6e133f07..666b1890 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,41 +1,34 @@
import com.android.build.api.dsl.ManagedVirtualDevice
-plugins {
- id "com.mikepenz.aboutlibraries.plugin" version "10.5.2"
-}
-
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
-apply plugin: 'com.mikepenz.aboutlibraries.plugin'
-apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply plugin: "kotlin-parcelize"
-
-
-
-// Force latest version of Jacoco, initially done to resolve https://github.com/jacoco/jacoco/issues/1155
-jacoco.toolVersion = "0.8.7"
-
+apply plugin: 'com.google.devtools.ksp'
android {
namespace 'org.pixeldroid.app'
- compileSdkVersion 33
- buildToolsVersion '33.0.0'
+ compileSdk 34
+
compileOptions {
coreLibraryDesugaringEnabled true
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
}
+ androidResources {
+ generateLocaleConfig true
+ }
+
+ kotlin {
+ jvmToolchain(17)
+ }
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_1_8
freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
}
defaultConfig {
minSdkVersion 23
- targetSdkVersion 33
- versionCode 24
+ versionCode 26
+ targetSdkVersion 34
versionName "1.0.beta" + versionCode
//TODO add resConfigs("en", "fr", "ja",...) ?
@@ -87,8 +80,9 @@ android {
/**
* Make a string with the application_id (available in xml etc)
*/
- android.applicationVariants.all { variant ->
+ android.applicationVariants.configureEach { variant ->
variant.resValue 'string', 'application_id', variant.applicationId
+ variant.resValue "string", "versionName", variant.versionName
}
testOptions {
@@ -113,11 +107,9 @@ android {
}
buildFeatures {
viewBinding true
- dataBinding = true
buildConfig = true
}
- apply plugin: 'kotlin-kapt'
lint {
//We can't expect translators to always keep up immediately:
// don't fail if a a string is untranslated
@@ -131,40 +123,40 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
/**
* AndroidX dependencies:
*/
implementation 'androidx.appcompat:appcompat:1.6.1'
- implementation 'androidx.core:core-splashscreen:1.0.0'
- implementation 'androidx.core:core-ktx:1.9.0'
- implementation 'androidx.preference:preference-ktx:1.2.0'
+ implementation 'androidx.core:core-splashscreen:1.0.1'
+ implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
- implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
- implementation "androidx.browser:browser:1.5.0"
- implementation 'androidx.recyclerview:recyclerview:1.3.0'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
+ implementation "androidx.browser:browser:1.7.0"
+ implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
- implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
- implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
- implementation 'androidx.paging:paging-runtime-ktx:3.1.1'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1'
- implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
- implementation "androidx.lifecycle:lifecycle-common-java8:2.6.1"
- implementation "androidx.annotation:annotation:1.6.0"
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
+ implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2'
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
+ implementation "androidx.lifecycle:lifecycle-common-java8:2.6.2"
+ implementation "androidx.annotation:annotation:1.7.1"
implementation 'androidx.gridlayout:gridlayout:1.0.0'
- implementation "androidx.activity:activity-ktx:1.7.0"
- implementation 'androidx.fragment:fragment-ktx:1.5.6'
- implementation 'androidx.work:work-runtime-ktx:2.8.1'
+ implementation "androidx.activity:activity-ktx:1.8.2"
+ implementation 'androidx.fragment:fragment-ktx:1.6.2'
+ implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'androidx.media2:media2-widget:1.2.1'
implementation 'androidx.media2:media2-player:1.2.1'
// Use the most recent version of CameraX
- def cameraX_version = '1.2.2'
+ def cameraX_version = '1.3.1'
implementation "androidx.camera:camera-core:$cameraX_version"
implementation "androidx.camera:camera-camera2:$cameraX_version"
// CameraX Lifecycle library
@@ -173,9 +165,9 @@ dependencies {
// CameraX View class
implementation "androidx.camera:camera-view:$cameraX_version"
- def room_version = "2.5.1"
+ def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
- kapt "androidx.room:room-compiler:$room_version"
+ ksp "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation "androidx.room:room-paging:$room_version"
@@ -186,16 +178,13 @@ dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
- implementation 'com.google.android.material:material:1.8.0'
+ implementation 'com.google.android.material:material:1.11.0'
//Dagger (dependency injection)
- implementation 'com.google.dagger:dagger-android:2.45'
- implementation 'com.google.dagger:dagger-android-support:2.44'
- // if you use the support libraries
- kapt 'com.google.dagger:dagger-android-processor:2.44'
- kapt 'com.google.dagger:dagger-compiler:2.44'
+ implementation 'com.google.dagger:dagger:2.48'
+ ksp 'com.google.dagger:dagger-compiler:2.48'
- implementation 'com.squareup.okhttp3:okhttp:4.9.3'
+ implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
@@ -203,11 +192,11 @@ dependencies {
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'com.github.connyduck:sparkbutton:4.1.0'
-
- implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.4'
+ implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.5'
implementation project(path: ':scrambler')
+ implementation project(path: ':pixel_common')
- implementation('com.github.bumptech.glide:glide:4.14.2') {
+ implementation('com.github.bumptech.glide:glide:4.16.0') {
exclude group: "com.android.support"
}
@@ -216,9 +205,9 @@ dependencies {
// Excludes the support library because it's already included by Glide.
transitive = false
}
- implementation 'com.github.bumptech.glide:annotations:4.14.2'
+ implementation 'com.github.bumptech.glide:annotations:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'
- kapt 'com.github.bumptech.glide:compiler:4.14.2'
+ ksp 'com.github.bumptech.glide:ksp:4.14.2'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
@@ -232,15 +221,10 @@ dependencies {
implementation 'com.mikepenz:iconics-views:5.4.0'
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
-
- implementation 'com.karumi:dexter:6.2.3'
-
implementation 'com.github.ligi:tracedroid:4.1'
implementation 'me.relex:circleindicator:2.1.6'
- implementation 'com.mikepenz:aboutlibraries-core:10.6.0'
-
/**
* Not in release, so not mentioned in licenses list
*/
@@ -251,7 +235,7 @@ dependencies {
androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
androidTestUtil 'com.linkedin.testbutler:test-butler-app:2.2.1'
- androidTestImplementation 'androidx.work:work-testing:2.8.1'
+ androidTestImplementation 'androidx.work:work-testing:2.9.0'
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
testImplementation 'junit:junit:4.13.2'
@@ -271,7 +255,7 @@ dependencies {
}
-tasks.withType(Test) {
+tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']
}
diff --git a/app/src/debug/res/xml/shortcuts.xml b/app/src/debug/res/xml/shortcuts.xml
index 0f8fb719..b075383a 100644
--- a/app/src/debug/res/xml/shortcuts.xml
+++ b/app/src/debug/res/xml/shortcuts.xml
@@ -8,7 +8,7 @@
+ android:targetClass="org.pixeldroid.app.postCreation.camera.CameraActivity" />
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3b7de319..8d8a315e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,14 +5,13 @@
-
+
-
@@ -47,7 +46,6 @@
android:theme="@style/AppTheme.ActionBar.Transparent" />
+
+
+ android:windowSoftInputMode="adjustResize"
+ android:theme="@style/BaseAppTheme">
@@ -87,27 +89,32 @@
+ tools:ignore="LockedOrientationActivity"
+ android:theme="@style/BaseAppTheme" />
+
-
+ tools:ignore="LockedOrientationActivity"
+ android:theme="@style/BaseAppTheme"/>
+
+ android:theme="@style/BaseAppTheme" />
@@ -149,6 +156,7 @@
@@ -160,17 +168,8 @@
android:name="android.app.searchable"
android:resource="@xml/searchable" />
-
-
-
+
when (position){
1 -> launchActivity(ProfileActivity())
@@ -244,6 +242,18 @@ class MainActivity : BaseThemedWithoutBarActivity() {
}
false
}
+
+ // Closes the drawer if it is open, when we press the back button
+ onBackPressedDispatcher.addCallback(this) {
+ // Handle the back button event
+ if(binding.drawerLayout.isDrawerOpen(GravityCompat.START)){
+ binding.drawerLayout.closeDrawer(GravityCompat.START)
+ }
+ else {
+ this.isEnabled = false
+ super.onBackPressedDispatcher.onBackPressed()
+ }
+ }
}
private fun logOut(){
@@ -486,16 +496,4 @@ class MainActivity : BaseThemedWithoutBarActivity() {
}
startActivity(intent)
}
-
- /**
- * Closes the drawer if it is open, when we press the back button
- */
- override fun onBackPressed() {
- if(binding.drawerLayout.isDrawerOpen(GravityCompat.START)){
- binding.drawerLayout.closeDrawer(GravityCompat.START)
- } else {
- super.onBackPressed()
- }
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt
index 634546d2..932b2e4f 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt
@@ -5,13 +5,13 @@ import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
-import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
+import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
const val TAG = "Post Creation Activity"
-class PostCreationActivity : BaseThemedWithoutBarActivity() {
+class PostCreationActivity : BaseActivity() {
companion object {
internal const val PICTURE_DESCRIPTION = "picture_description"
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt
index 9c7bc3f6..44d6da62 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt
@@ -33,8 +33,10 @@ import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentPostCreationBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity
+import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.utils.BaseFragment
+import org.pixeldroid.app.utils.bindingLifecycleAware
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.fileExtension
@@ -52,7 +54,7 @@ class PostCreationFragment : BaseFragment() {
private var user: UserDatabaseEntity? = null
private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "")
- private lateinit var binding: FragmentPostCreationBinding
+ private var binding: FragmentPostCreationBinding by bindingLifecycleAware()
private lateinit var model: PostCreationViewModel
override fun onCreateView(
@@ -63,6 +65,7 @@ class PostCreationFragment : BaseFragment() {
// Inflate the layout for this fragment
binding = FragmentPostCreationBinding.inflate(layoutInflater)
+
return binding.root
}
@@ -83,7 +86,8 @@ class PostCreationFragment : BaseFragment() {
requireActivity().intent.clipData!!,
instance,
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
- requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
+ requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false),
+ requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false),
)
}
model = _model
@@ -99,6 +103,7 @@ class PostCreationFragment : BaseFragment() {
)
}
)
+ binding.postCreationNextButton.isEnabled = newPhotoData.isNotEmpty()
}
lifecycleScope.launch {
@@ -119,13 +124,26 @@ class PostCreationFragment : BaseFragment() {
binding.toolbarPostCreation.visibility =
if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE
binding.carousel.layoutCarousel = uiState.isCarousel
+
+ if(uiState.storyCreation){
+ binding.toggleStoryPost.check(binding.buttonStory.id)
+ binding.buttonStory.isPressed = true
+ binding.carousel.showLayoutSwitchButton = false
+ binding.carousel.showIndicator = false
+ } else {
+ binding.toggleStoryPost.check(binding.buttonPost.id)
+ binding.carousel.showLayoutSwitchButton = true
+ binding.carousel.showIndicator = true
+ }
+ binding.carousel.maxEntries = uiState.maxEntries
+
}
}
}
binding.carousel.apply {
layoutCarouselCallback = { model.becameCarousel(it)}
- maxEntries = instance.albumLimit
+ maxEntries = if(model.uiState.value.storyCreation) 1 else instance.albumLimit
addPhotoButtonCallback = {
addPhoto()
}
@@ -133,9 +151,10 @@ class PostCreationFragment : BaseFragment() {
model.updateDescription(position, description)
}
}
- // get the description and send the post
- binding.postCreationSendButton.setOnClickListener {
- if (validatePost() && model.isNotEmpty()) {
+
+ // Validate the post and go to the next step of the post creation process
+ binding.postCreationNextButton.setOnClickListener {
+ if (validatePost()) {
findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment)
}
}
@@ -163,6 +182,23 @@ class PostCreationFragment : BaseFragment() {
}
}
+ binding.toggleStoryPost.addOnButtonCheckedListener { _, checkedId, isChecked ->
+ // Only handle checked events
+ if (!isChecked) return@addOnButtonCheckedListener
+
+ when (checkedId) {
+ R.id.buttonStory -> {
+ model.storyMode(true)
+ }
+ R.id.buttonPost -> {
+ model.storyMode(false)
+ }
+ }
+
+ }
+
+ binding.backbutton.setOnClickListener{requireActivity().onBackPressedDispatcher.onBackPressed()}
+
// Clean up temporary files, if any
val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES)
tempFiles?.asList()?.forEach {
@@ -275,14 +311,17 @@ class PostCreationFragment : BaseFragment() {
private fun validatePost(): Boolean {
- if (model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false) {
- MaterialAlertDialogBuilder(requireActivity()).apply {
- setMessage(R.string.still_encoding)
- setNegativeButton(android.R.string.ok) { _, _ -> }
- }.show()
- return false
+ if (model.getPhotoData().value?.none { it.video && it.videoEncodeComplete == false } == true) {
+ // Encoding is done, i.e. none of the items are both a video and not done encoding.
+ // We return true if the post is not empty, false otherwise.
+ return model.getPhotoData().value?.isNotEmpty() == true
}
- return true
+ // Encoding is not done, show a dialog and return false to indicate validation failed
+ MaterialAlertDialogBuilder(requireActivity()).apply {
+ setMessage(R.string.still_encoding)
+ setNegativeButton(android.R.string.ok) { _, _ -> }
+ }.show()
+ return false
}
private val editResultContract: ActivityResultLauncher = registerForActivityResult(
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
index 8cd7a3a8..8f0bfa94 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
@@ -22,6 +22,7 @@ import androidx.preference.PreferenceManager
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
import com.jarsilio.android.scrambler.stripMetadata
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -54,7 +55,6 @@ import kotlin.collections.forEach
import kotlin.collections.get
import kotlin.collections.getOrNull
import kotlin.collections.indexOfFirst
-import kotlin.collections.isNotEmpty
import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf
import kotlin.collections.plus
@@ -70,6 +70,7 @@ data class PostCreationActivityUiState(
val addPhotoButtonEnabled: Boolean = true,
val editPhotoButtonEnabled: Boolean = true,
val removePhotoButtonEnabled: Boolean = true,
+ val maxEntries: Int?,
val isCarousel: Boolean = true,
@@ -86,6 +87,11 @@ data class PostCreationActivityUiState(
val uploadErrorVisible: Boolean = false,
val uploadErrorExplanationText: String = "",
val uploadErrorExplanationVisible: Boolean = false,
+
+ val storyCreation: Boolean,
+ val storyDuration: Int = 10,
+ val storyReplies: Boolean = true,
+ val storyReactions: Boolean = true,
)
@Parcelize
@@ -98,7 +104,7 @@ data class PhotoData(
var video: Boolean,
var videoEncodeProgress: Int? = null,
var videoEncodeStabilizationFirstPass: Boolean? = null,
- var videoEncodeComplete: Boolean = true,
+ var videoEncodeComplete: Boolean? = null,
var videoEncodeError: Boolean = false,
) : Parcelable
@@ -107,8 +113,10 @@ class PostCreationViewModel(
clipdata: ClipData? = null,
val instance: InstanceDatabaseEntity? = null,
existingDescription: String? = null,
- existingNSFW: Boolean = false
+ existingNSFW: Boolean = false,
+ storyCreation: Boolean = false,
) : AndroidViewModel(application) {
+ private var storyPhotoDataBackup: MutableList? = null
private val photoData: MutableLiveData> by lazy {
MutableLiveData>().also {
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
@@ -128,7 +136,9 @@ class PostCreationViewModel(
_uiState = MutableStateFlow(PostCreationActivityUiState(
newPostDescriptionText = existingDescription ?: templateDescription,
- nsfw = existingNSFW
+ nsfw = existingNSFW,
+ maxEntries = if(storyCreation) 1 else instance?.albumLimit,
+ storyCreation = storyCreation
))
}
@@ -145,35 +155,41 @@ class PostCreationViewModel(
}
}
+ /**
+ * Read-only public view on [photoData]
+ */
fun getPhotoData(): LiveData> = photoData
/**
* Will add as many images as possible to [photoData], from the [clipData], and if
- * ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images
+ * ([photoData].size + [clipData].itemCount) > uiState.value.maxEntries 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.
*/
fun addPossibleImages(clipData: ClipData, previousList: MutableList? = photoData.value): MutableList {
val dataToAdd: ArrayList = arrayListOf()
var count = clipData.itemCount
- if(count + (previousList?.size ?: 0) > instance!!.albumLimit){
- _uiState.update { currentUiState ->
- currentUiState.copy(userMessage = getApplication().getString(R.string.total_exceeds_album_limit).format(instance.albumLimit))
+ uiState.value.maxEntries?.let {
+ if(count + (previousList?.size ?: 0) > it){
+ _uiState.update { currentUiState ->
+ currentUiState.copy(userMessage = getApplication().getString(R.string.total_exceeds_album_limit).format(it))
+ }
+ count = count.coerceAtMost(it - (previousList?.size ?: 0))
}
- count = count.coerceAtMost(instance.albumLimit - (previousList?.size ?: 0))
- }
- if (count + (previousList?.size ?: 0) >= instance.albumLimit) {
- // Disable buttons to add more images
- _uiState.update { currentUiState ->
- currentUiState.copy(addPhotoButtonEnabled = false)
- }
- }
- for (i in 0 until count) {
- clipData.getItemAt(i).let {
- val sizeAndVideoPair: Pair =
- getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
- dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString()))
+ if (count + (previousList?.size ?: 0) >= it) {
+ // Disable buttons to add more images
+ _uiState.update { currentUiState ->
+ currentUiState.copy(addPhotoButtonEnabled = false)
+ }
+ }
+ for (i in 0 until count) {
+ clipData.getItemAt(i).let {
+ val sizeAndVideoPair: Pair =
+ getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
+ dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString()))
+ }
}
}
+
return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf()
}
@@ -185,18 +201,20 @@ class PostCreationViewModel(
* Returns the size of the file of the Uri, and whether it is a video,
* and opens a dialog in case it is too big or in case the file is unsupported.
*/
- fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair {
+ private fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair {
val size: Long =
if (uri.scheme =="content") {
getApplication().contentResolver.query(uri, 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.
- */
+ * 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)
+ if(sizeIndex >= 0) {
+ cursor.moveToFirst()
+ cursor.getLong(sizeIndex)
+ } else null
} ?: 0
} else {
uri.toFile().length()
@@ -213,6 +231,7 @@ class PostCreationViewModel(
}
if ((!isVideo && sizeInkBytes > instance!!.maxPhotoSize) || (isVideo && sizeInkBytes > instance!!.maxVideoSize)) {
+ //TODO Offer remedy for too big file: re-compress it
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
_uiState.update { currentUiState ->
currentUiState.copy(
@@ -223,8 +242,6 @@ class PostCreationViewModel(
return Pair(size, isVideo)
}
- fun isNotEmpty(): Boolean = photoData.value?.isNotEmpty() ?: false
-
fun updateDescription(position: Int, description: String) {
photoData.value?.getOrNull(position)?.imageDescription = description
photoData.value = photoData.value
@@ -234,8 +251,8 @@ class PostCreationViewModel(
photoData.value?.removeAt(currentPosition)
_uiState.update {
it.copy(
- addPhotoButtonEnabled = true
- )
+ addPhotoButtonEnabled = (photoData.value?.size ?: 0) < (uiState.value.maxEntries ?: 0),
+ )
}
photoData.value = photoData.value
}
@@ -254,7 +271,7 @@ class PostCreationViewModel(
videoEncodeProgress = 0
videoEncodeComplete = false
- VideoEditActivity.startEncoding(imageUri, it,
+ VideoEditActivity.startEncoding(imageUri, null, it,
context = getApplication(),
registerNewFFmpegSession = ::registerNewFFmpegSession,
trackTempFile = ::trackTempFile,
@@ -442,7 +459,10 @@ class PostCreationViewModel(
apiHolder.setToCurrentUser(it)
} ?: apiHolder.api ?: apiHolder.setToCurrentUser()
- val inter = api.mediaUpload(description, requestBody.parts[0])
+ val inter: Observable =
+ //TODO validate that image is correct (?) aspect ratio
+ if (uiState.value.storyCreation) api.storyUpload(requestBody.parts[0])
+ else api.mediaUpload(description, requestBody.parts[0])
apiHolder.api = null
postSub = inter
@@ -451,7 +471,11 @@ class PostCreationViewModel(
.subscribe(
{ attachment: Attachment ->
data.progress = 0
- data.uploadId = attachment.id!!
+ data.uploadId = if(uiState.value.storyCreation){
+ attachment.media_id!!
+ } else {
+ attachment.id!!
+ }
},
{ e: Throwable ->
_uiState.update { currentUiState ->
@@ -507,11 +531,23 @@ class PostCreationViewModel(
apiHolder.setToCurrentUser(it)
} ?: apiHolder.api ?: apiHolder.setToCurrentUser()
- api.postStatus(
- statusText = description,
- media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(),
- sensitive = nsfw
- )
+ if(uiState.value.storyCreation){
+ val canReact = if (uiState.value.storyReactions) "1" else "0"
+ val canReply = if (uiState.value.storyReplies) "1" else "0"
+
+ api.storyPublish(
+ media_id = getPhotoData().value!!.firstNotNullOf { it.uploadId },
+ can_react = canReact,
+ can_reply = canReply,
+ duration = uiState.value.storyDuration
+ )
+ } else {
+ api.postStatus(
+ statusText = description,
+ media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(),
+ sensitive = nsfw
+ )
+ }
Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
val intent = Intent(getApplication(), MainActivity::class.java)
@@ -551,10 +587,52 @@ class PostCreationViewModel(
fun chooseAccount(which: UserDatabaseEntity) {
_uiState.update { it.copy(chosenAccount = which) }
}
+
+ fun storyMode(storyMode: Boolean) {
+ //TODO check ratio of files in story mode? What is acceptable?
+
+ val newMaxEntries = if (storyMode) 1 else instance?.albumLimit
+ var newUiState = _uiState.value.copy(
+ storyCreation = storyMode,
+ maxEntries = newMaxEntries,
+ addPhotoButtonEnabled = (photoData.value?.size ?: 0) < (newMaxEntries ?: 0),
+ )
+
+ // Carousel on if in story mode
+ if (storyMode) newUiState = newUiState.copy(isCarousel = true)
+
+ // If switching to story, and there are too many pictures, keep the first and backup the rest
+ if (storyMode && (photoData.value?.size ?: 0) > 1){
+ storyPhotoDataBackup = photoData.value
+
+ photoData.value = photoData.value?.let { mutableListOf(it.firstOrNull()).filterNotNull().toMutableList() }
+
+ //Show message saying extraneous pictures were removed but can be restored
+ newUiState = newUiState.copy(
+ userMessage = getApplication().getString(R.string.extraneous_pictures_stories)
+ )
+ }
+ // Restore if backup not null and first value is unchanged
+ else if (storyPhotoDataBackup != null && storyPhotoDataBackup?.firstOrNull() == photoData.value?.firstOrNull()){
+ photoData.value = storyPhotoDataBackup
+ storyPhotoDataBackup = null
+ }
+ _uiState.update { newUiState }
+ }
+
+ fun storyDuration(value: Int) {
+ _uiState.update {
+ it.copy(storyDuration = value)
+ }
+ }
+
+ fun updateStoryReactions(checked: Boolean) { _uiState.update { it.copy(storyReactions = checked) } }
+
+ fun updateStoryReplies(checked: Boolean) { _uiState.update { it.copy(storyReplies = checked) } }
}
-class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean) : ViewModelProvider.Factory {
+class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean, val storyCreation: Boolean) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
- return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW)
+ return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW, storyCreation)
}
}
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt
index 4ce76ab0..3fadc832 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt
@@ -20,10 +20,13 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding
+import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.utils.BaseFragment
+import org.pixeldroid.app.utils.bindingLifecycleAware
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.setSquareImageFromURL
+import kotlin.math.roundToInt
class PostSubmissionFragment : BaseFragment() {
@@ -34,7 +37,7 @@ class PostSubmissionFragment : BaseFragment() {
private var user: UserDatabaseEntity? = null
private lateinit var instance: InstanceDatabaseEntity
- private lateinit var binding: FragmentPostSubmissionBinding
+ private var binding: FragmentPostSubmissionBinding by bindingLifecycleAware()
private lateinit var model: PostCreationViewModel
override fun onCreateView(
@@ -68,7 +71,8 @@ class PostSubmissionFragment : BaseFragment() {
requireActivity().intent.clipData!!,
instance,
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
- requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
+ requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false),
+ requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false)
)
}
model = _model
@@ -77,6 +81,18 @@ class PostSubmissionFragment : BaseFragment() {
binding.nsfwSwitch.isChecked = model.uiState.value.nsfw
binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText)
+ if(model.uiState.value.storyCreation){
+ binding.nsfwSwitch.visibility = View.GONE
+ binding.postTextInputLayout.visibility = View.GONE
+ binding.privateTitle.visibility = View.GONE
+ binding.postPreview.visibility = View.GONE
+
+ binding.storyOptions.visibility = View.VISIBLE
+ binding.storyDurationSlider.value = model.uiState.value.storyDuration.toFloat()
+ binding.storyRepliesSwitch.isChecked = model.uiState.value.storyReplies
+ binding.storyReactionsSwitch.isChecked = model.uiState.value.storyReactions
+ }
+
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
@@ -114,13 +130,24 @@ class PostSubmissionFragment : BaseFragment() {
binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
model.updateNSFW(isChecked)
}
+ binding.storyRepliesSwitch.setOnCheckedChangeListener { _, isChecked ->
+ model.updateStoryReplies(isChecked)
+ }
+ binding.storyReactionsSwitch.setOnCheckedChangeListener { _, isChecked ->
+ model.updateStoryReactions(isChecked)
+ }
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
+ binding.storyDurationSlider.addOnChangeListener { _, value, _ ->
+ // Responds to when slider's value is changed
+ model.storyDuration(value.roundToInt())
+ }
+
setSquareImageFromURL(View(requireActivity()), model.getPhotoData().value?.get(0)?.imageUri.toString(), binding.postPreview)
// Get the description and send the post
- binding.postCreationSendButton.setOnClickListener {
+ binding.postSubmissionSendButton.setOnClickListener {
if (validatePost()) model.upload()
}
@@ -179,13 +206,13 @@ class PostSubmissionFragment : BaseFragment() {
}
private fun enableButton(enable: Boolean = true){
- binding.postCreationSendButton.isEnabled = enable
+ binding.postSubmissionSendButton.isEnabled = enable
if(enable){
binding.postingProgressBar.visibility = View.GONE
- binding.postCreationSendButton.visibility = View.VISIBLE
+ binding.postSubmissionSendButton.visibility = View.VISIBLE
} else {
binding.postingProgressBar.visibility = View.VISIBLE
- binding.postCreationSendButton.visibility = View.GONE
+ binding.postSubmissionSendButton.visibility = View.GONE
}
}
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt
index 8ea1eff6..b007ea5c 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt
@@ -5,47 +5,51 @@ import android.os.Bundle
import android.view.MenuItem
import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.databinding.ActivityCameraBinding
+import org.pixeldroid.app.postCreation.camera.CameraFragment.Companion.CAMERA_ACTIVITY
+import org.pixeldroid.app.postCreation.camera.CameraFragment.Companion.CAMERA_ACTIVITY_STORY
+import org.pixeldroid.app.utils.BaseActivity
-class CameraActivity : BaseThemedWithBarActivity() {
+class CameraActivity : BaseActivity() {
+ private lateinit var binding: ActivityCameraBinding
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_camera)
+
+ binding = ActivityCameraBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setTitle(R.string.add_photo)
val cameraFragment = CameraFragment()
- val arguments = Bundle()
- arguments.putBoolean("CameraActivity", true)
- cameraFragment.arguments = arguments
+ val story: Boolean = intent.getBooleanExtra(CAMERA_ACTIVITY_STORY, false)
- supportFragmentManager.beginTransaction()
- .add(R.id.camera_activity_fragment, cameraFragment).commit()
- }
-}
+ if(story) supportActionBar?.setTitle(R.string.add_story)
+ else supportActionBar?.setTitle(R.string.add_photo)
-/**
- * Launch without arguments so that it will open the
- * [org.pixeldroid.app.postCreation.PostCreationActivity] instead of "returning" to a non-existent
- * [org.pixeldroid.app.postCreation.PostCreationActivity]
- */
-class CameraActivityShortcut : BaseThemedWithBarActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_camera)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setTitle(R.string.new_post_shortcut_long)
-
- val cameraFragment = CameraFragment()
+ // If this CameraActivity wasn't started from the shortcut,
+ // tell the fragment it's in an activity (so that it sends back the result instead of
+ // starting a new post creation process)
+ if (intent.action != "android.intent.action.VIEW") {
+ val arguments = Bundle()
+ arguments.putBoolean(CAMERA_ACTIVITY, true)
+ arguments.putBoolean(CAMERA_ACTIVITY_STORY, story)
+ cameraFragment.arguments = arguments
+ } else {
+ supportActionBar?.setTitle(R.string.new_post_shortcut_long)
+ }
supportFragmentManager.beginTransaction()
.add(R.id.camera_activity_fragment, cameraFragment).commit()
}
- //Start a new MainActivity when "going back" on this activity
override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ // If this CameraActivity wasn't started from the shortcut, behave as usual
+ if (intent.action != "android.intent.action.VIEW") return super.onOptionsItemSelected(item)
+
+ // Else, start a new MainActivity when "going back" on this activity
when (item.itemId) {
android.R.id.home -> {
val intent = Intent(this, MainActivity::class.java)
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt
index e492def2..3cb6cc4c 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt
@@ -34,13 +34,12 @@ import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
-import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentCameraBinding
import org.pixeldroid.app.postCreation.PostCreationActivity
import org.pixeldroid.app.utils.BaseFragment
+import org.pixeldroid.app.utils.bindingLifecycleAware
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -61,7 +60,7 @@ class CameraFragment : BaseFragment() {
private val cameraLifecycleOwner = CameraLifecycleOwner()
- private lateinit var binding: FragmentCameraBinding
+ private var binding: FragmentCameraBinding by bindingLifecycleAware()
private var displayId: Int = -1
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
@@ -70,6 +69,7 @@ class CameraFragment : BaseFragment() {
private var camera: Camera? = null
private var inActivity by Delegates.notNull()
+ private var addToStory by Delegates.notNull()
private var filePermissionDialogLaunched: Boolean = false
@@ -89,7 +89,8 @@ class CameraFragment : BaseFragment() {
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
- inActivity = arguments?.getBoolean("CameraActivity") ?: false
+ inActivity = arguments?.getBoolean(CAMERA_ACTIVITY) ?: false
+ addToStory = arguments?.getBoolean(CAMERA_ACTIVITY_STORY) ?: false
binding = FragmentCameraBinding.inflate(layoutInflater)
@@ -106,7 +107,7 @@ class CameraFragment : BaseFragment() {
thumbnail.setPadding(10)
// Load thumbnail into circular button using Glide
- Glide.with(thumbnail)
+ if(activity?.isDestroyed == false) Glide.with(thumbnail)
.load(uri)
.apply(RequestOptions.circleCropTransform())
.into(thumbnail)
@@ -337,7 +338,8 @@ class CameraFragment : BaseFragment() {
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
action = Intent.ACTION_GET_CONTENT
addCategory(Intent.CATEGORY_OPENABLE)
- putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+ // Don't allow multiple for story
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !addToStory)
uploadImageResultContract.launch(
Intent.createChooser(this, null)
)
@@ -464,15 +466,20 @@ class CameraFragment : BaseFragment() {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
- if(inActivity){
+ if(inActivity && !addToStory){
requireActivity().setResult(Activity.RESULT_OK, intent)
requireActivity().finish()
} else {
+ if(addToStory){
+ intent.putExtra(CAMERA_ACTIVITY_STORY, addToStory)
+ }
startActivity(intent)
}
}
companion object {
+ const val CAMERA_ACTIVITY = "CameraActivity"
+ const val CAMERA_ACTIVITY_STORY = "CameraActivityStory"
private const val TAG = "CameraFragment"
private const val RATIO_4_3_VALUE = 4.0 / 3.0
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt
index 1b48ff95..fdede499 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt
@@ -8,6 +8,6 @@ data class CarouselItem constructor(
val video: Boolean,
var encodeProgress: Int?,
var stabilizationFirstPass: Boolean?,
- var encodeComplete: Boolean = false,
+ var encodeComplete: Boolean? = null,
var encodeError: Boolean = false,
)
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt
index 6c977864..aeca38d7 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt
@@ -18,6 +18,9 @@ import androidx.recyclerview.widget.*
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ImageCarouselBinding
import me.relex.circleindicator.CircleIndicator2
+import org.pixeldroid.common.dpToPx
+import org.pixeldroid.common.getSnapPosition
+import org.pixeldroid.common.spToPx
class ImageCarousel(
context: Context,
@@ -40,7 +43,6 @@ class ImageCarousel(
)
private lateinit var recyclerView: RecyclerView
- private lateinit var tvCaption: TextView
private var snapHelper: SnapHelper = PagerSnapHelper()
var indicator: CircleIndicator2? = null
@@ -107,7 +109,7 @@ class ImageCarousel(
set(value) {
field = value
- tvCaption.visibility = if (showCaption) View.VISIBLE else View.GONE
+ binding.tvCaption.visibility = if (showCaption) View.VISIBLE else View.GONE
}
@Dimension(unit = Dimension.PX)
@@ -115,7 +117,7 @@ class ImageCarousel(
set(value) {
field = value
- tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat())
+ binding.tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat())
}
var showIndicator = false
@@ -245,14 +247,14 @@ class ImageCarousel(
showNavigationButtons = showNavigationButtons
binding.editMediaDescriptionLayout.visibility = if(editingMediaDescription) VISIBLE else INVISIBLE
- tvCaption.visibility = if(editingMediaDescription) INVISIBLE else VISIBLE
+ binding.tvCaption.visibility = if(editingMediaDescription || !showCaption) INVISIBLE else VISIBLE
} else {
recyclerView.layoutManager = GridLayoutManager(context, 3)
binding.btnNext.visibility = GONE
binding.btnPrevious.visibility = GONE
binding.editMediaDescriptionLayout.visibility = INVISIBLE
- tvCaption.visibility = INVISIBLE
+ binding.tvCaption.visibility = INVISIBLE
}
showIndicator = value
@@ -279,8 +281,7 @@ class ImageCarousel(
updateDescriptionCallback?.invoke(currentPosition, description)
}
binding.editMediaDescriptionLayout.visibility = if(value) VISIBLE else INVISIBLE
- tvCaption.visibility = if(value) INVISIBLE else VISIBLE
-
+ binding.tvCaption.visibility = if(value || !showCaption) INVISIBLE else VISIBLE
}
}
@@ -289,10 +290,10 @@ class ImageCarousel(
set(value) {
if(!value.isNullOrEmpty()) {
field = value
- tvCaption.text = value
+ binding.tvCaption.text = value
} else {
field = null
- tvCaption.text = context.getText(R.string.no_media_description)
+ binding.tvCaption.text = context.getText(R.string.no_media_description)
}
}
@@ -317,12 +318,11 @@ class ImageCarousel(
binding = ImageCarouselBinding.inflate(LayoutInflater.from(context),this, true)
recyclerView = binding.recyclerView
- tvCaption = binding.tvCaption
recyclerView.setHasFixedSize(true)
// For marquee effect
- tvCaption.isSelected = true
+ binding.tvCaption.isSelected = true
}
@@ -441,7 +441,7 @@ class ImageCarousel(
caption.apply {
if(layoutCarousel){
binding.editMediaDescriptionLayout.visibility = INVISIBLE
- tvCaption.visibility = VISIBLE
+ showCaption = true
}
currentDescription = this
}
@@ -472,7 +472,7 @@ class ImageCarousel(
}
})
- tvCaption.setOnClickListener {
+ binding.tvCaption.setOnClickListener {
editingMediaDescription = true
}
@@ -562,7 +562,7 @@ class ImageCarousel(
binding.encodeInfoText.setText(R.string.encode_error)
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error),
null, null, null)
- } else if(it.encodeComplete){
+ } else if(it.encodeComplete == true){
binding.encodeInfoCard.visibility = VISIBLE
binding.encodeProgress.visibility = GONE
binding.encodeInfoText.setText(R.string.encode_success)
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/Utils.kt b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/Utils.kt
deleted file mode 100644
index 4f60e488..00000000
--- a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/Utils.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.pixeldroid.app.postCreation.carousel
-
-import android.content.Context
-import android.util.DisplayMetrics
-import android.util.TypedValue
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.SnapHelper
-
-
-/**
- * This method converts device specific pixels to density independent pixels.
- */
-fun Int.pxToDp(context: Context): Int {
- return (this / (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
-}
-
-/**
- * This method converts dp unit to equivalent pixels, depending on device density.
- */
-fun Int.dpToPx(context: Context): Int {
- return TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP,
- this.toFloat(),
- context.resources.displayMetrics
- ).toInt()
-}
-
-/**
- * This method converts sp unit to equivalent pixels, depending on device density.
- */
-fun Int.spToPx(context: Context): Int {
- return TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_SP,
- this.toFloat(),
- context.resources.displayMetrics
- ).toInt()
-}
-
-/**
- * Get current snap item position of a recyclerView.
- *
- * @param layoutManager Target recyclerView
- * @return Position of the item or RecyclerView.NO_POSITION (-1)
- */
-fun SnapHelper.getSnapPosition(layoutManager: RecyclerView.LayoutManager?): Int {
- if (layoutManager == null) {
- return RecyclerView.NO_POSITION
- }
- val snapView: View = this.findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
- return layoutManager.getPosition(snapView)
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/posts/AlbumActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/AlbumActivity.kt
index 07456188..eb5982a2 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/AlbumActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/AlbumActivity.kt
@@ -1,12 +1,14 @@
package org.pixeldroid.app.posts
import android.os.Bundle
+import android.view.MenuItem
import android.view.View
+import androidx.appcompat.app.AppCompatActivity
import org.pixeldroid.app.databinding.ActivityAlbumBinding
-import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Attachment
-class AlbumActivity : BaseActivity() {
+
+class AlbumActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityAlbumBinding.inflate(layoutInflater)
@@ -36,4 +38,17 @@ class AlbumActivity : BaseActivity() {
supportActionBar?.setDisplayShowTitleEnabled(false)
supportActionBar?.setBackgroundDrawable(null)
}
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ // Handle up arrow manually,
+ // since "up" isn't defined for this activity
+ onBackPressedDispatcher.onBackPressed()
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt b/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt
index e56e64fe..c60a8a0f 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt
@@ -11,6 +11,7 @@ import android.view.View
import android.widget.TextView
import androidx.core.text.toSpanned
import androidx.lifecycle.LifecycleCoroutineScope
+import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account.Companion.openAccountFromId
@@ -106,7 +107,7 @@ fun parseHTMLText(
override fun onClick(widget: View) {
// Retrieve the account for the given profile
- lifecycleScope.launchWhenCreated {
+ lifecycleScope.launch {
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
openAccountFromId(accountId, api, context)
}
@@ -130,7 +131,7 @@ fun parseHTMLText(
}
-fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Boolean, context: Context) {
+fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Boolean) {
val now = Date.from(Instant.now()).time
try {
@@ -140,7 +141,7 @@ fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Bool
android.text.format.DateUtils.SECOND_IN_MILLIS,
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE).toString()
- textView.text = if(absoluteTime) context.getString(R.string.posted_on).format(date)
+ textView.text = if(absoluteTime) textView.context.getString(R.string.posted_on).format(date)
else formattedDate
} catch (e: ParseException) {
diff --git a/app/src/main/java/org/pixeldroid/app/posts/MediaViewerActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/MediaViewerActivity.kt
index 091faf72..638fd615 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/MediaViewerActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/MediaViewerActivity.kt
@@ -14,9 +14,9 @@ import androidx.media2.common.MediaMetadata
import androidx.media2.common.UriMediaItem
import androidx.media2.player.MediaPlayer
import org.pixeldroid.app.databinding.ActivityMediaviewerBinding
-import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
+import org.pixeldroid.app.utils.BaseActivity
-class MediaViewerActivity : BaseThemedWithoutBarActivity() {
+class MediaViewerActivity : BaseActivity() {
private lateinit var mediaPlayer: MediaPlayer
private lateinit var binding: ActivityMediaviewerBinding
diff --git a/app/src/main/java/org/pixeldroid/app/posts/NestedScrollableHost.kt b/app/src/main/java/org/pixeldroid/app/posts/NestedScrollableHost.kt
index 4385f35b..d0c16c5b 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/NestedScrollableHost.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/NestedScrollableHost.kt
@@ -96,11 +96,12 @@ class NestedScrollableHost(context: Context, attrs: AttributeSet? = null) :
return super.onSingleTapConfirmed(e)
}
override fun onScroll(
- e1: MotionEvent,
+ e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
+ if (e1 == null) return false
val orientation = parentViewPager?.orientation ?: return true
val dx = e2.x - e1.x
diff --git a/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt
index 13b5adaf..79c18222 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt
@@ -5,13 +5,15 @@ import android.util.Log
import android.view.View
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment
import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment.Companion.COMMENT_DOMAIN
import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment.Companion.COMMENT_STATUS_ID
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.api.objects.Status.Companion.POST_COMMENT_TAG
@@ -19,7 +21,7 @@ import org.pixeldroid.app.utils.api.objects.Status.Companion.POST_TAG
import org.pixeldroid.app.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG
import org.pixeldroid.app.utils.displayDimensionsInPx
-class PostActivity : BaseThemedWithBarActivity() {
+class PostActivity : BaseActivity() {
private lateinit var binding: ActivityPostBinding
private var commentFragment = CommentFragment()
@@ -30,7 +32,7 @@ class PostActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState)
binding = ActivityPostBinding.inflate(layoutInflater)
setContentView(binding.root)
-
+ setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
status = intent.getSerializableExtra(POST_TAG) as Status
@@ -43,7 +45,10 @@ class PostActivity : BaseThemedWithBarActivity() {
val holder = StatusViewHolder(binding.postFragmentSingle)
- holder.bind(status, apiHolder, db, lifecycleScope, displayDimensionsInPx(), isActivity = true)
+ holder.bind(
+ status, apiHolder, db, lifecycleScope, displayDimensionsInPx(),
+ requestPermissionDownloadPic, isActivity = true
+ )
activateCommenter()
initCommentsFragment(domain = user?.instance_uri.orEmpty())
@@ -60,6 +65,17 @@ class PostActivity : BaseThemedWithBarActivity() {
}
}
+ private val requestPermissionDownloadPic =
+ registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted: Boolean ->
+ if (!isGranted) {
+ MaterialAlertDialogBuilder(this)
+ .setMessage(R.string.write_permission_download_pic)
+ .setNegativeButton(android.R.string.ok) { _, _ -> }
+ .show()
+ }
+ }
private fun activateCommenter() {
//Activate commenter
binding.submitComment.setOnClickListener {
diff --git a/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt
index 3b83a31b..88bff903 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt
@@ -5,10 +5,10 @@ import android.view.View
import androidx.lifecycle.lifecycleScope
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityReportBinding
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Status
-class ReportActivity : BaseThemedWithBarActivity() {
+class ReportActivity : BaseActivity() {
private lateinit var binding: ActivityReportBinding
@@ -16,9 +16,9 @@ class ReportActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState)
binding = ActivityReportBinding.inflate(layoutInflater)
setContentView(binding.root)
+ setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setTitle(R.string.report)
val status = intent.getSerializableExtra(Status.POST_TAG) as Status?
diff --git a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt
index 39898014..b77cf62c 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt
@@ -1,14 +1,16 @@
package org.pixeldroid.app.posts
-import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.Intent
+import android.content.pm.PackageManager.PERMISSION_DENIED
+import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.graphics.Typeface
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
+import android.os.Build
import android.os.Looper
import android.text.method.LinkMovementMethod
import android.util.Log
@@ -17,6 +19,7 @@ import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.widget.*
+import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
@@ -36,10 +39,6 @@ import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
-import com.karumi.dexter.Dexter
-import com.karumi.dexter.listener.PermissionDeniedResponse
-import com.karumi.dexter.listener.PermissionGrantedResponse
-import com.karumi.dexter.listener.single.BasePermissionListener
import kotlinx.coroutines.launch
import okhttp3.*
import okio.BufferedSink
@@ -75,7 +74,11 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
private var status: Status? = null
- fun bind(status: Status?, pixelfedAPI: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair, isActivity: Boolean = false) {
+ fun bind(
+ status: Status?, pixelfedAPI: PixelfedAPIHolder, db: AppDatabase,
+ lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair,
+ requestPermissionDownloadPic: ActivityResultLauncher, isActivity: Boolean = false
+ ) {
this.itemView.visibility = View.VISIBLE
this.status = status
@@ -104,7 +107,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
setupPost(picRequest, user.instance_uri, isActivity)
- activateButtons(pixelfedAPI, db, lifecycleScope, isActivity)
+ activateButtons(pixelfedAPI, db, lifecycleScope, isActivity, requestPermissionDownloadPic)
}
@@ -139,8 +142,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
setTextViewFromISO8601(
status?.created_at!!,
binding.postDate,
- isActivity,
- binding.root.context
+ isActivity
)
binding.postDomain.text = status?.getStatusDomain(domain, binding.postDomain.context)
@@ -233,6 +235,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
db: AppDatabase,
lifecycleScope: LifecycleCoroutineScope,
isActivity: Boolean,
+ requestPermissionDownloadPic: ActivityResultLauncher,
){
//Set the special HTML text
setDescription(apiHolder, lifecycleScope)
@@ -262,7 +265,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
showComments(lifecycleScope, isActivity)
- activateMoreButton(apiHolder, db, lifecycleScope)
+ activateMoreButton(apiHolder, db, lifecycleScope, requestPermissionDownloadPic)
}
private fun activateReblogger(
@@ -364,7 +367,12 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
return null
}
- private fun activateMoreButton(apiHolder: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){
+ private fun activateMoreButton(
+ apiHolder: PixelfedAPIHolder,
+ db: AppDatabase,
+ lifecycleScope: LifecycleCoroutineScope,
+ requestPermissionDownloadPic: ActivityResultLauncher
+ ){
var bookmarked: Boolean? = null
binding.statusMore.setOnClickListener {
PopupMenu(it.context, it).apply {
@@ -402,50 +410,29 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
true
}
R.id.post_more_menu_save_to_gallery -> {
- Dexter.withContext(binding.root.context)
- .withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- .withListener(object : BasePermissionListener() {
- override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
- Toast.makeText(
- binding.root.context,
- binding.root.context.getString(R.string.write_permission_download_pic),
- Toast.LENGTH_SHORT
- ).show()
- }
-
- override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
- status?.downloadImage(
- binding.root.context,
- status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
- ?: "",
- binding.root
- )
- }
- }).check()
+ // Check permissions on old Android versions: on new versions it is not
+ // needed when storing a file.
+ if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(binding.root.context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) {
+ requestPermissionDownloadPic.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ } else {
+ status?.downloadImage(
+ binding.root.context,
+ status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
+ ?: "",
+ binding.root
+ )
+ }
true
}
- R.id.post_more_menu_share_picture -> {
- Dexter.withContext(binding.root.context)
- .withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- .withListener(object : BasePermissionListener() {
- override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
- Toast.makeText(
- binding.root.context,
- binding.root.context.getString(R.string.write_permission_share_pic),
- Toast.LENGTH_SHORT
- ).show()
- }
- override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
- status?.downloadImage(
- binding.root.context,
- status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
- ?: "",
- binding.root,
- share = true,
- )
- }
- }).check()
+ R.id.post_more_menu_share_picture -> {
+ status?.downloadImage(
+ binding.root.context,
+ status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
+ ?: "",
+ binding.root,
+ share = true,
+ )
true
}
R.id.post_more_menu_delete -> {
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt
index b7b0d436..cffb588c 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt
@@ -6,13 +6,16 @@ import android.widget.ProgressBar
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleCoroutineScope
+import androidx.paging.CombinedLoadStates
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter
import androidx.paging.PagingDataAdapter
+import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.gson.Gson
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -20,6 +23,7 @@ import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ErrorLayoutBinding
import org.pixeldroid.app.databinding.LoadStateFooterViewItemBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.FeedViewModel
+import org.pixeldroid.app.stories.StoriesAdapter
import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.api.objects.Status
import retrofit2.HttpException
@@ -48,14 +52,28 @@ private fun showError(
internal fun initAdapter(
progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout,
recyclerView: RecyclerView, motionLayout: MotionLayout, errorLayout: ErrorLayoutBinding,
- adapter: PagingDataAdapter) {
+ adapter: PagingDataAdapter,
+ header: StoriesAdapter? = null
+) {
- recyclerView.adapter = adapter.withLoadStateFooter(
- footer = ReposLoadStateAdapter { adapter.retry() }
+
+ val footer = ReposLoadStateAdapter { adapter.retry() }
+
+ adapter.addLoadStateListener { loadStates: CombinedLoadStates ->
+ footer.loadState = loadStates.append
+ }
+
+ recyclerView.adapter = ConcatAdapter(
+ *listOfNotNull(
+ header, // need to filter it if null
+ adapter,
+ footer
+ ).toTypedArray()
)
swipeRefreshLayout.setOnRefreshListener {
adapter.refresh()
+ header?.refreshStories()
}
adapter.addLoadStateListener { loadState ->
@@ -80,6 +98,11 @@ internal fun initAdapter(
?: loadState.append as? LoadState.Error
?: loadState.prepend as? LoadState.Error
?: loadState.refresh as? LoadState.Error
+
+ if(errorState?.error is CancellationException){
+ return@addLoadStateListener
+ }
+
errorState?.let {
val error: String = (it.error as? HttpException)?.response()?.errorBody()?.string()?.ifEmpty { null }?.let { s ->
try {
@@ -143,6 +166,8 @@ class ReposLoadStateAdapter(
}
}
+
+
/**
* [RecyclerView.ViewHolder] that is shown at the end of the feed to indicate loading or errors
* in the loading of appending values.
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt
index 592f5a53..4aa9abea 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt
@@ -18,8 +18,10 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import org.pixeldroid.app.databinding.FragmentFeedBinding
import org.pixeldroid.app.posts.feeds.initAdapter
+import org.pixeldroid.app.stories.StoriesAdapter
import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
+import org.pixeldroid.app.utils.bindingLifecycleAware
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
import org.pixeldroid.app.utils.limitedLengthSmoothScrollToPosition
@@ -31,8 +33,9 @@ open class CachedFeedFragment : BaseFragment() {
internal lateinit var viewModel: FeedViewModel
internal lateinit var adapter: PagingDataAdapter
+ internal var headerAdapter: StoriesAdapter? = null
- private lateinit var binding: FragmentFeedBinding
+ private var binding: FragmentFeedBinding by bindingLifecycleAware()
private var job: Job? = null
@@ -49,6 +52,7 @@ open class CachedFeedFragment : BaseFragment() {
}
}
+ //TODO rename function to something that makes sense
internal fun initSearch() {
// Scroll to top when the list is refreshed from network.
lifecycleScope.launchWhenStarted {
@@ -73,7 +77,9 @@ open class CachedFeedFragment : BaseFragment() {
binding = FragmentFeedBinding.inflate(layoutInflater)
initAdapter(binding.progressBar, binding.swipeRefreshLayout,
- binding.list, binding.motionLayout, binding.errorLayout, adapter)
+ binding.list, binding.motionLayout, binding.errorLayout, adapter,
+ headerAdapter
+ )
return binding.root
}
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt
index 7dd5beea..b1469fe3 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt
@@ -221,8 +221,7 @@ class NotificationsFragment : CachedFeedFragment() {
setTextViewFromISO8601(
it,
notificationTime,
- false,
- itemView.context
+ false
)
}
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt
index 41e2f29f..c0b974ba 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt
@@ -47,7 +47,7 @@ class HomeFeedRemoteMediator @Inject constructor(
HomeStatusDatabaseEntity(user.user_id, user.instance_uri, it)
}
- val endOfPaginationReached = apiResponse.isEmpty()
+ val endOfPaginationReached = apiResponse.isEmpty() || maxId == apiResponse.sortedBy { it.created_at }.last().id
db.withTransaction {
// Clear table in the database
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt
index 48c13fe4..40a76c83 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt
@@ -11,14 +11,14 @@ import androidx.paging.PagingDataAdapter
import androidx.paging.RemoteMediator
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
-import org.pixeldroid.app.R
-import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
import org.pixeldroid.app.posts.StatusViewHolder
-import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
+import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory
+import org.pixeldroid.app.stories.StoriesAdapter
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
import org.pixeldroid.app.utils.api.objects.Status
+import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
import org.pixeldroid.app.utils.displayDimensionsInPx
import kotlin.properties.Delegates
@@ -38,14 +38,18 @@ class PostFeedFragment: CachedFeedFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- adapter = PostsAdapter(requireContext().displayDimensionsInPx())
+ home = requireArguments().getBoolean("home")
- home = requireArguments().get("home") as Boolean
+ adapter = PostsAdapter(requireContext().displayDimensionsInPx())
@Suppress("UNCHECKED_CAST")
if (home){
mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator
dao = db.homePostDao() as FeedContentDao
+ headerAdapter = StoriesAdapter(lifecycleScope, apiHolder)
+ headerAdapter?.showStories = false
+
+ headerAdapter?.refreshStories()
}
else {
mediator = PublicFeedRemoteMediator(apiHolder, db) as RemoteMediator
@@ -55,7 +59,7 @@ class PostFeedFragment: CachedFeedFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
+ savedInstanceState: Bundle?,
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
@@ -70,6 +74,7 @@ class PostFeedFragment: CachedFeedFragment() {
return view
}
+
inner class PostsAdapter(private val displayDimensionsInPx: Pair) : PagingDataAdapter(
object : DiffUtil.ItemCallback() {
override fun areItemsTheSame (oldItem: T, newItem: T): Boolean = oldItem.id == newItem.id
@@ -81,15 +86,19 @@ class PostFeedFragment: CachedFeedFragment() {
return StatusViewHolder.create(parent)
}
- override fun getItemViewType(position: Int): Int {
- return R.layout.post_fragment
- }
-
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
- val uiModel = getItem(position) as Status?
- uiModel?.let {
- (holder as StatusViewHolder).bind(it, apiHolder, db, lifecycleScope, displayDimensionsInPx)
- }
+ holder.itemView.visibility = View.VISIBLE
+ holder.itemView.layoutParams =
+ RecyclerView.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ val uiModel = getItem(position) as Status?
+ uiModel?.let {
+ (holder as StatusViewHolder).bind(
+ it, apiHolder, db, lifecycleScope, displayDimensionsInPx, requestPermissionDownloadPic
+ )
+ }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt
index fd5f19e9..9651ff0e 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt
@@ -62,7 +62,7 @@ class PublicFeedRemoteMediator @Inject constructor(
val dbObjects = apiResponse.map{
PublicFeedStatusDatabaseEntity(user.user_id, user.instance_uri, it)
}
- val endOfPaginationReached = apiResponse.isEmpty()
+ val endOfPaginationReached = apiResponse.isEmpty() || maxId == apiResponse.sortedBy { it.created_at }.last().id
db.withTransaction {
// Clear table in the database
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt
index 9037bfe2..db58c98e 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt
@@ -61,8 +61,10 @@ open class UncachedFeedFragment : BaseFragment() {
binding = FragmentFeedBinding.inflate(layoutInflater)
- initAdapter(binding.progressBar, binding.swipeRefreshLayout, binding.list,
- binding.motionLayout, binding.errorLayout, adapter)
+ initAdapter(
+ binding.progressBar, binding.swipeRefreshLayout, binding.list,
+ binding.motionLayout, binding.errorLayout, adapter
+ )
return binding.root
}
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedPostsFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedPostsFragment.kt
index 2d26b4b3..61c71cb5 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedPostsFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedPostsFragment.kt
@@ -85,7 +85,9 @@ class UncachedPostsFragment : UncachedFeedFragment() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let {
- (holder as StatusViewHolder).bind(it, apiHolder, db, lifecycleScope, displayDimensionsInPx)
+ (holder as StatusViewHolder).bind(
+ it, apiHolder, db, lifecycleScope, displayDimensionsInPx, requestPermissionDownloadPic
+ )
}
}
}
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt
index 99ad2239..b26420f0 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt
@@ -2,17 +2,23 @@ package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags
import android.os.Bundle
import org.pixeldroid.app.R
+import org.pixeldroid.app.databinding.ActivityFollowersBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG
-class HashTagActivity : BaseThemedWithBarActivity() {
+class HashTagActivity : BaseActivity() {
private var tagFragment = UncachedPostsFragment()
+ private lateinit var binding: ActivityFollowersBinding
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_followers)
+ binding = ActivityFollowersBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ setSupportActionBar(binding.topBar)
+
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// Get hashtag tag
diff --git a/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt
index a244cdcd..09b2db53 100644
--- a/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt
@@ -13,12 +13,12 @@ import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityCollectionBinding
import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION
import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION_ID
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Collection
import java.lang.Exception
-class CollectionActivity : BaseThemedWithBarActivity() {
+class CollectionActivity : BaseActivity() {
private lateinit var binding: ActivityCollectionBinding
private lateinit var collection: Collection
@@ -37,6 +37,7 @@ class CollectionActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState)
binding = ActivityCollectionBinding.inflate(layoutInflater)
setContentView(binding.root)
+ setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
diff --git a/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt
index 48cb7b95..239f3140 100644
--- a/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt
@@ -6,6 +6,7 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
+import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.widget.doAfterTextChanged
@@ -19,10 +20,10 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityEditProfileBinding
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.openUrl
-class EditProfileActivity : BaseThemedWithBarActivity() {
+class EditProfileActivity : BaseActivity() {
private lateinit var model: EditProfileViewModel
private lateinit var binding: ActivityEditProfileBinding
@@ -31,12 +32,29 @@ class EditProfileActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState)
binding = ActivityEditProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
+ setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setTitle(R.string.edit_profile)
val _model: EditProfileViewModel by viewModels { EditProfileViewModelFactory(application) }
model = _model
+ onBackPressedDispatcher.addCallback(this) {
+ // Handle the back button event
+ if(model.madeChanges()){
+ MaterialAlertDialogBuilder(binding.root.context).apply {
+ setMessage(getString(R.string.profile_save_changes))
+ setNegativeButton(android.R.string.cancel) { _, _ -> }
+ setPositiveButton(android.R.string.ok) { _, _ ->
+ this@addCallback.isEnabled = false
+ super.onBackPressedDispatcher.onBackPressed()
+ }
+ }.show()
+ } else {
+ this.isEnabled = false
+ super.onBackPressedDispatcher.onBackPressed()
+ }
+ }
+
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
@@ -132,18 +150,6 @@ class EditProfileActivity : BaseThemedWithBarActivity() {
return true
}
- @Deprecated("Deprecated in Java")
- override fun onBackPressed() {
- if(model.madeChanges()){
- MaterialAlertDialogBuilder(binding.root.context).apply {
- setMessage(getString(R.string.profile_save_changes))
- setNegativeButton(android.R.string.cancel) { _, _ -> }
- setPositiveButton(android.R.string.ok) { _, _ -> super.onBackPressed()}
- }.show()
- }
- else super.onBackPressed()
- }
-
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId){
R.id.action_apply -> {
diff --git a/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt
index c89b1ced..7f431ca6 100644
--- a/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt
@@ -2,20 +2,25 @@ package org.pixeldroid.app.profile
import android.os.Bundle
import org.pixeldroid.app.R
+import org.pixeldroid.app.databinding.ActivityFollowersBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Account.Companion.ACCOUNT_ID_TAG
import org.pixeldroid.app.utils.api.objects.Account.Companion.ACCOUNT_TAG
import org.pixeldroid.app.utils.api.objects.Account.Companion.FOLLOWERS_TAG
-class FollowsActivity : BaseThemedWithBarActivity() {
+class FollowsActivity : BaseActivity() {
private var followsFragment = AccountListFragment()
+ private lateinit var binding: ActivityFollowersBinding
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_followers)
+ binding = ActivityFollowersBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
diff --git a/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt
index 042e33ce..7283de31 100644
--- a/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt
@@ -17,7 +17,7 @@ import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityProfileBinding
import org.pixeldroid.app.posts.parseHTMLText
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
@@ -25,7 +25,7 @@ import org.pixeldroid.app.utils.setProfileImageFromURL
import retrofit2.HttpException
import java.io.IOException
-class ProfileActivity : BaseThemedWithBarActivity() {
+class ProfileActivity : BaseActivity() {
private lateinit var domain : String
private lateinit var accountId : String
@@ -36,7 +36,10 @@ class ProfileActivity : BaseThemedWithBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater)
+
setContentView(binding.root)
+ setSupportActionBar(binding.topBar)
+
supportActionBar?.setDisplayHomeAsUpEnabled(true)
diff --git a/app/src/main/java/org/pixeldroid/app/profile/ProfileFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/profile/ProfileFeedFragment.kt
index 5e473f24..558a95d4 100644
--- a/app/src/main/java/org/pixeldroid/app/profile/ProfileFeedFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/profile/ProfileFeedFragment.kt
@@ -178,8 +178,10 @@ class ProfileFeedFragment : UncachedFeedFragment() {
deleteFromCollection
)
} else {
- (holder as StatusViewHolder).bind(it as Status, apiHolder, db,
- lifecycleScope, requireContext().displayDimensionsInPx())
+ (holder as StatusViewHolder).bind(
+ it as Status, apiHolder, db, lifecycleScope,
+ requireContext().displayDimensionsInPx(), requestPermissionDownloadPic
+ )
}
}
diff --git a/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchActivity.kt b/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchActivity.kt
index 761abc2b..791b606f 100644
--- a/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchActivity.kt
@@ -9,17 +9,21 @@ import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.pixeldroid.app.R
+import org.pixeldroid.app.databinding.ActivitySearchBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchAccountFragment
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchHashtagFragment
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Results
-class SearchActivity : BaseThemedWithBarActivity() {
+class SearchActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_search)
+ val binding = ActivitySearchBinding.inflate(layoutInflater)
+
+ setContentView(binding.root)
+ setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
var query = ""
diff --git a/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt b/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt
index a1a73e51..b82a0598 100644
--- a/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt
@@ -11,22 +11,24 @@ import androidx.core.content.ContextCompat
import org.pixeldroid.app.databinding.FragmentSearchBinding
import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TRENDING_TAG
import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TrendingType
-import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.BaseFragment
+import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.bindingLifecycleAware
+
/**
* This fragment lets you search and use Pixelfed's Discover feature
*/
class SearchDiscoverFragment : BaseFragment() {
+
private lateinit var api: PixelfedAPI
var binding: FragmentSearchBinding by bindingLifecycleAware()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
+ savedInstanceState: Bundle?,
): View {
binding = FragmentSearchBinding.inflate(inflater, container, false)
@@ -56,4 +58,5 @@ class SearchDiscoverFragment : BaseFragment() {
intent.putExtra(TRENDING_TAG, type)
ContextCompat.startActivity(binding.root.context, intent, null)
}
+
}
diff --git a/app/src/main/java/org/pixeldroid/app/searchDiscover/TrendingActivity.kt b/app/src/main/java/org/pixeldroid/app/searchDiscover/TrendingActivity.kt
index cd064974..edd98f19 100644
--- a/app/src/main/java/org/pixeldroid/app/searchDiscover/TrendingActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/searchDiscover/TrendingActivity.kt
@@ -15,7 +15,7 @@ import org.pixeldroid.app.posts.PostActivity
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountViewHolder
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.HashTagViewHolder
import org.pixeldroid.app.profile.ProfilePostViewHolder
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Attachment
@@ -24,7 +24,7 @@ import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.api.objects.Tag
import org.pixeldroid.app.utils.setSquareImageFromURL
-class TrendingActivity : BaseThemedWithBarActivity() {
+class TrendingActivity : BaseActivity() {
private lateinit var binding: ActivityTrendingBinding
private lateinit var trendingAdapter : TrendingRecyclerViewAdapter
@@ -33,6 +33,7 @@ class TrendingActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState)
binding = ActivityTrendingBinding.inflate(layoutInflater)
setContentView(binding.root)
+ setSupportActionBar(binding.topBar)
val recycler = binding.list
supportActionBar?.setDisplayHomeAsUpEnabled(true)
diff --git a/app/src/main/java/org/pixeldroid/app/settings/AboutActivity.kt b/app/src/main/java/org/pixeldroid/app/settings/AboutActivity.kt
deleted file mode 100644
index ae21ea1e..00000000
--- a/app/src/main/java/org/pixeldroid/app/settings/AboutActivity.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.pixeldroid.app.settings
-
-import android.content.Intent
-import android.os.Bundle
-import org.pixeldroid.app.BuildConfig
-import org.pixeldroid.app.R
-import org.pixeldroid.app.databinding.ActivityAboutBinding
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
-
-class AboutActivity : BaseThemedWithBarActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val binding = ActivityAboutBinding.inflate(layoutInflater)
-
- setContentView(binding.root)
-
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setTitle(R.string.about_pixeldroid)
-
- binding.aboutVersionNumber.text = BuildConfig.VERSION_NAME
- binding.licensesButton.setOnClickListener{
- val intent = Intent(this, LicenseActivity::class.java)
- startActivity(intent)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/settings/LicenseActivity.kt b/app/src/main/java/org/pixeldroid/app/settings/LicenseActivity.kt
deleted file mode 100644
index ead657e0..00000000
--- a/app/src/main/java/org/pixeldroid/app/settings/LicenseActivity.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.pixeldroid.app.settings
-
-import android.os.Bundle
-import com.mikepenz.aboutlibraries.Libs
-import org.pixeldroid.app.R
-import org.pixeldroid.app.databinding.OpenSourceLicenseBinding
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
-
-/**
- * Displays licenses for all app dependencies. JSON is
- * generated by the plugin https://github.com/cookpad/LicenseToolsPlugin.
- */
-class LicenseActivity: BaseThemedWithBarActivity() {
-
- private lateinit var binding: OpenSourceLicenseBinding
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = OpenSourceLicenseBinding.inflate(layoutInflater)
-
- setContentView(binding.root)
-
- supportActionBar?.setTitle(R.string.dependencies_licenses)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setHomeButtonEnabled(true)
-
- setupRecyclerView()
- }
-
- private fun setupRecyclerView() {
- val aboutLibsJson: String = applicationContext.resources.openRawResource(R.raw.aboutlibraries)
- .bufferedReader().use { it.readText() }
-
- val libs = Libs.Builder()
- .withJson(aboutLibsJson)
- .build()
-
- val adapter = OpenSourceLicenseAdapter(libs)
- binding.openSourceLicenseRecyclerView.adapter = adapter
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/settings/OpenSourceLicenseAdapter.kt b/app/src/main/java/org/pixeldroid/app/settings/OpenSourceLicenseAdapter.kt
deleted file mode 100644
index ca485669..00000000
--- a/app/src/main/java/org/pixeldroid/app/settings/OpenSourceLicenseAdapter.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.pixeldroid.app.settings
-
-import android.annotation.SuppressLint
-import android.text.method.LinkMovementMethod
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import androidx.recyclerview.widget.RecyclerView
-import com.mikepenz.aboutlibraries.Libs
-import com.mikepenz.aboutlibraries.entity.Library
-import org.pixeldroid.app.databinding.OpenSourceItemBinding
-
-class OpenSourceLicenseAdapter(private val openSourceItems: Libs) :
- RecyclerView.Adapter() {
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OpenSourceLicenceViewHolder
- {
- val itemBinding = OpenSourceItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- return OpenSourceLicenceViewHolder(itemBinding)
- }
-
- override fun onBindViewHolder(holder: OpenSourceLicenceViewHolder, position: Int) {
- val item = openSourceItems.libraries[position]
- holder.bind(item)
- }
-
- override fun getItemCount(): Int = openSourceItems.libraries.size
-
- class OpenSourceLicenceViewHolder(val binding: OpenSourceItemBinding) :
- RecyclerView.ViewHolder(binding.root) {
- @SuppressLint("SetTextI18n")
- fun bind(item: Library) {
- with(binding) {
- if (item.name.isNotEmpty()) {
- title.isVisible = true
- title.text = item.name
- } else {
- title.isVisible = false
- }
- val license = item.licenses.firstOrNull()
- val licenseName = license?.name ?: ""
- val licenseUrl = license?.url?.let { " (${it} )" } ?: ""
- copyright.isVisible = true
- copyright.apply {
- text = "$licenseName$licenseUrl"
- movementMethod = LinkMovementMethod.getInstance()
- }
- url.isVisible = true
- url.apply {
- text = "${item.developers.firstOrNull()?.name ?: ""} ${item.website}"
- movementMethod = LinkMovementMethod.getInstance()
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt b/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt
index d35b3de3..99ecdf17 100644
--- a/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt
@@ -6,6 +6,7 @@ import android.content.SharedPreferences
import android.content.res.XmlResourceParser
import android.os.Build
import android.os.Bundle
+import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.fragment.app.DialogFragment
@@ -16,23 +17,39 @@ import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R
-import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.databinding.SettingsBinding
+import org.pixeldroid.common.ThemedActivity
import org.pixeldroid.app.utils.setThemeFromPreferences
-class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
+class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
private var restartMainOnExit = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ val binding = SettingsBinding.inflate(layoutInflater)
+
+ setContentView(binding.root)
+ setSupportActionBar(binding.topBar)
- setContentView(R.layout.settings)
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, SettingsFragment())
.commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setTitle(R.string.menu_settings)
+
+ onBackPressedDispatcher.addCallback(this /* lifecycle owner */) {
+ // Handle the back button event
+ // If a setting (for example language or theme) was changed, the main activity should be
+ // started without history so that the change is applied to the whole back stack
+ if (restartMainOnExit) {
+ val intent = Intent(this@SettingsActivity, MainActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ super@SettingsActivity.startActivity(intent)
+ } else {
+ finish()
+ }
+ }
restartMainOnExit = intent.getBooleanExtra("restartMain", false)
}
@@ -51,25 +68,17 @@ class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnShared
)
}
- override fun onBackPressed() {
- // If a setting (for example language or theme) was changed, the main activity should be
- // started without history so that the change is applied to the whole back stack
- if (restartMainOnExit) {
- val intent = Intent(this, MainActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- super.startActivity(intent)
- } else {
- super.onBackPressed()
- }
- }
- override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
- when (key) {
- "theme" -> {
- setThemeFromPreferences(sharedPreferences, resources)
- recreateWithRestartStatus()
- }
- "themeColor" -> {
- recreateWithRestartStatus()
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
+ sharedPreferences?.let {
+ when (key) {
+ "theme" -> {
+ setThemeFromPreferences(it, resources)
+ recreateWithRestartStatus()
+ }
+
+ "themeColor" -> {
+ recreateWithRestartStatus()
+ }
}
}
}
@@ -125,7 +134,8 @@ class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnShared
class LanguageSettingFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val list: MutableList = mutableListOf()
- resources.getXml(R.xml.locales_config).use {
+ // IDE doesn't find it, but compiling works apparently?
+ resources.getXml(R.xml._generated_res_locale_config).use {
var eventType = it.eventType
while (eventType != XmlResourceParser.END_DOCUMENT) {
when (eventType) {
diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt
new file mode 100644
index 00000000..8ca480e5
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt
@@ -0,0 +1,228 @@
+package org.pixeldroid.app.stories
+
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.view.MotionEvent
+import android.view.View.OnClickListener
+import android.view.View.OnTouchListener
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.view.isVisible
+import androidx.core.widget.doAfterTextChanged
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.load.engine.GlideException
+import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.RequestOptions
+import com.bumptech.glide.request.target.Target
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.launch
+import org.pixeldroid.app.R
+import org.pixeldroid.app.databinding.ActivityStoriesBinding
+import org.pixeldroid.app.posts.setTextViewFromISO8601
+import org.pixeldroid.app.utils.BaseActivity
+import org.pixeldroid.app.utils.api.objects.Account
+import org.pixeldroid.app.utils.api.objects.Story
+import org.pixeldroid.app.utils.api.objects.StoryCarousel
+
+
+class StoriesActivity: BaseActivity() {
+
+ companion object {
+ const val STORY_CAROUSEL = "LaunchStoryCarousel"
+ const val STORY_CAROUSEL_SELF = "LaunchStoryCarouselSelf"
+ const val STORY_CAROUSEL_USER_ID = "LaunchStoryUserId"
+ }
+
+
+ private lateinit var binding: ActivityStoriesBinding
+
+ private lateinit var storyProgress: StoryProgress
+
+ private lateinit var model: StoriesViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ //force night mode always
+ delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
+
+ super.onCreate(savedInstanceState)
+
+ val carousel = intent.getSerializableExtra(STORY_CAROUSEL) as? StoryCarousel
+ val userId = intent.getStringExtra(STORY_CAROUSEL_USER_ID)
+ val selfCarousel: Array? = intent.getSerializableExtra(STORY_CAROUSEL_SELF) as? Array
+
+ binding = ActivityStoriesBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ val _model: StoriesViewModel by viewModels {
+ StoriesViewModelFactory(application, carousel, userId, selfCarousel?.asList())
+ }
+ model = _model
+
+ storyProgress = StoryProgress(model.uiState.value.imageList.size)
+ binding.storyProgressImage.setImageDrawable(storyProgress)
+
+ lifecycleScope.launch {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ model.uiState.collect { uiState ->
+ binding.pause.isSelected = uiState.paused
+
+ uiState.age?.let { setTextViewFromISO8601(it, binding.storyAge, false) }
+
+ if (uiState.errorMessage != null) {
+ binding.storyErrorText.setText(uiState.errorMessage)
+ binding.storyErrorCard.isVisible = true
+ } else binding.storyErrorCard.isVisible = false
+
+ if (uiState.snackBar != null) {
+ Snackbar.make(
+ binding.root, uiState.snackBar,
+ Snackbar.LENGTH_SHORT
+ ).setAnchorView(binding.storyReplyField).show()
+ model.shownSnackbar()
+ }
+
+ if (uiState.username != null) {
+ binding.storyReplyField.hint = getString(R.string.replyToStory).format(uiState.username)
+ } else binding.storyReplyField.hint = null
+
+ uiState.profilePicture?.let {
+ Glide.with(binding.storyAuthorProfilePicture)
+ .load(it)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.storyAuthorProfilePicture)
+ }
+
+ binding.storyAuthor.text = uiState.username
+
+ storyProgress.currentStory = uiState.currentImage
+
+ uiState.imageList.getOrNull(uiState.currentImage)?.let {
+ Glide.with(binding.storyImage)
+ .load(it)
+ .listener(object : RequestListener {
+ override fun onLoadFailed(
+ e: GlideException?,
+ model: Any?,
+ target: Target,
+ isFirstResource: Boolean,
+ ): Boolean = false
+
+ override fun onResourceReady(
+ resource: Drawable,
+ model: Any,
+ target: Target?,
+ dataSource: DataSource,
+ isFirstResource: Boolean,
+ ): Boolean {
+ Glide.with(binding.storyImage)
+ .load(uiState.imageList.getOrNull(uiState.currentImage + 1))
+ .preload()
+ return false
+ }
+ })
+ .into(binding.storyImage)
+ }
+ }
+ }
+ }
+
+ //Pause when clicked on text field
+ binding.storyReplyField.editText?.setOnFocusChangeListener { view, isFocused ->
+ if (view.isInTouchMode && isFocused) {
+ view.performClick() // picks up first tap
+ }
+ }
+ binding.storyReplyField.editText?.setOnClickListener {
+ if (!model.uiState.value.paused) {
+ model.pause()
+ }
+ }
+
+ binding.storyReplyField.editText?.doAfterTextChanged {
+ it?.let { text ->
+ val string = text.toString()
+ if(string != model.uiState.value.reply) model.replyChanged(string)
+ }
+ }
+
+ binding.storyReplyField.setEndIconOnClickListener {
+ binding.storyReplyField.editText?.text?.let { text ->
+ model.sendReply(text)
+ }
+ }
+
+ binding.storyErrorCard.setOnClickListener{
+ model.dismissError()
+ }
+
+ model.count.observe(this) { state ->
+ // Render state in UI
+ model.uiState.value.durationList.getOrNull(model.uiState.value.currentImage)?.let {
+ storyProgress.progress = 1 - (state/it.toFloat())
+ binding.storyProgressImage.postInvalidate()
+ }
+ }
+
+ binding.pause.setOnClickListener {
+ //Set the button's appearance
+ it.isSelected = !it.isSelected
+ model.pause()
+ }
+
+ val authorOnClickListener = OnClickListener {
+ if (!model.uiState.value.paused) {
+ model.pause()
+ }
+ model.currentProfileId()?.let {
+ lifecycleScope.launch {
+ Account.openAccountFromId(
+ it,
+ apiHolder.api ?: apiHolder.setToCurrentUser(),
+ this@StoriesActivity
+ )
+ }
+ }
+ }
+ binding.storyAuthorProfilePicture.setOnClickListener(authorOnClickListener)
+ binding.storyAuthor.setOnClickListener(authorOnClickListener)
+
+ val onTouchListener = OnTouchListener { v, event ->
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> if (!model.uiState.value.paused) {
+ model.pause()
+ }
+ MotionEvent.ACTION_UP -> if(event.eventTime - event.downTime < 500) {
+ v.performClick()
+ return@OnTouchListener false
+ } else model.pause()
+ }
+
+ true
+ }
+
+ binding.viewMiddle.setOnTouchListener{ v, event ->
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> model.pause()
+ MotionEvent.ACTION_UP -> if(event.eventTime - event.downTime < 500) {
+ v.performClick()
+ return@setOnTouchListener false
+ } else model.pause()
+ }
+
+ true
+ }
+ binding.viewLeft.setOnTouchListener(onTouchListener)
+ binding.viewRight.setOnTouchListener(onTouchListener)
+
+ binding.viewRight.setOnClickListener {
+ model.goToNext()
+ }
+ binding.viewLeft.setOnClickListener {
+ model.goToPrevious()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt
new file mode 100644
index 00000000..0d42af37
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt
@@ -0,0 +1,229 @@
+package org.pixeldroid.app.stories
+
+import android.app.Application
+import android.os.CountDownTimer
+import android.text.Editable
+import androidx.annotation.StringRes
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.pixeldroid.app.R
+import org.pixeldroid.app.utils.PixelDroidApplication
+import org.pixeldroid.app.utils.api.objects.CarouselUserContainer
+import org.pixeldroid.app.utils.api.objects.Story
+import org.pixeldroid.app.utils.api.objects.StoryCarousel
+import org.pixeldroid.app.utils.db.AppDatabase
+import org.pixeldroid.app.utils.di.PixelfedAPIHolder
+import java.time.Instant
+import javax.inject.Inject
+
+data class StoriesUiState(
+ val profilePicture: String? = null,
+ val username: String? = null,
+ val age: Instant? = null,
+ val currentImage: Int = 0,
+ val imageList: List = emptyList(),
+ val durationList: List = emptyList(),
+ val paused: Boolean = false,
+ @StringRes
+ val errorMessage: Int? = null,
+ @StringRes
+ val snackBar: Int? = null,
+ val reply: String = ""
+)
+
+class StoriesViewModel(
+ application: Application,
+ val carousel: StoryCarousel?,
+ userId: String?,
+ val selfCarousel: List?
+) : AndroidViewModel(application) {
+
+ @Inject
+ lateinit var apiHolder: PixelfedAPIHolder
+ @Inject
+ lateinit var db: AppDatabase
+
+ private var currentAccount: CarouselUserContainer?
+
+ private val _uiState: MutableStateFlow
+
+ val uiState: StateFlow
+
+ val count = MutableLiveData()
+
+ private var timer: CountDownTimer? = null
+
+ init {
+ (application as PixelDroidApplication).getAppComponent().inject(this)
+ currentAccount =
+ if (selfCarousel != null) {
+ db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel) }
+ } else carousel?.nodes?.firstOrNull { it?.user?.id == userId }
+
+ _uiState = MutableStateFlow(newUiStateFromCurrentAccount())
+ uiState = _uiState
+
+ startTimerForCurrent()
+ }
+
+ private fun setTimer(timerLength: Float) {
+ count.value = timerLength
+ timer = object: CountDownTimer((timerLength * 1000).toLong(), 50){
+
+ override fun onTick(millisUntilFinished: Long) {
+ count.value = millisUntilFinished.toFloat() / 1000
+ }
+
+ override fun onFinish() {
+ goToNext()
+ }
+ }
+ }
+
+ private fun newUiStateFromCurrentAccount(): StoriesUiState = StoriesUiState(
+ profilePicture = currentAccount?.user?.avatar,
+ age = currentAccount?.nodes?.getOrNull(0)?.created_at,
+ username = currentAccount?.user?.username, //TODO check if not username_acct, think about falling back on other option?
+ errorMessage = null,
+ currentImage = 0,
+ imageList = currentAccount?.nodes?.mapNotNull { it?.src } ?: emptyList(),
+ durationList = currentAccount?.nodes?.mapNotNull { it?.duration } ?: emptyList()
+ )
+
+ private fun goTo(index: Int){
+ if((0 until uiState.value.imageList.size).contains(index)) {
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ currentImage = index,
+ age = currentAccount?.nodes?.getOrNull(index)?.created_at,
+ paused = false
+ )
+ }
+ } else {
+ if(selfCarousel != null) return
+ val currentUserId = currentAccount?.user?.id
+ val currentAccountIndex = carousel?.nodes?.indexOfFirst { it?.user?.id == currentUserId } ?: return
+ currentAccount = when (index) {
+ uiState.value.imageList.size -> {
+ // Go to next user
+ if(currentAccountIndex + 1 >= carousel.nodes.size) return
+ carousel.nodes.getOrNull(currentAccountIndex + 1)
+
+ }
+
+ -1 -> {
+ // Go to previous user
+ if(currentAccountIndex <= 0) return
+ carousel.nodes.getOrNull(currentAccountIndex - 1)
+ }
+ else -> return // Do nothing, given index does not make sense
+ }
+ _uiState.update { newUiStateFromCurrentAccount() }
+ }
+
+ timer?.cancel()
+ startTimerForCurrent()
+ }
+
+ fun goToNext() {
+ viewModelScope.launch {
+ try {
+ val api = apiHolder.api ?: apiHolder.setToCurrentUser()
+ val story = currentAccount?.nodes?.getOrNull(uiState.value.currentImage)
+
+ if (story?.seen == true){
+ //TODO update seen when marked successfully as seen?
+ story.id?.let { api.storySeen(it) }
+ }
+ } catch (exception: Exception){
+ _uiState.update { currentUiState ->
+ currentUiState.copy(errorMessage = R.string.story_could_not_see)
+ }
+ }
+
+ }
+ goTo(uiState.value.currentImage + 1)
+ }
+
+ fun goToPrevious() = goTo(uiState.value.currentImage - 1)
+
+ private fun startTimerForCurrent(){
+ uiState.value.let {
+ it.durationList.getOrNull(it.currentImage)?.toLong()?.let { time ->
+ setTimer(time.toFloat())
+ timer?.start()
+ }
+ }
+ }
+
+ fun pause() {
+ if(_uiState.value.paused){
+ timer?.start()
+ } else {
+ timer?.cancel()
+ count.value?.let { setTimer(it) }
+ }
+ _uiState.update { currentUiState ->
+ currentUiState.copy(paused = !currentUiState.paused)
+ }
+ }
+
+ fun sendReply(text: Editable) {
+ viewModelScope.launch {
+ try {
+ val api = apiHolder.api ?: apiHolder.setToCurrentUser()
+ currentStoryId()?.let { api.storyComment(it, text.toString()) }
+
+ _uiState.update { currentUiState ->
+ currentUiState.copy(snackBar = R.string.sent_reply_story)
+ }
+ } catch (exception: Exception){
+ _uiState.update { currentUiState ->
+ currentUiState.copy(errorMessage = R.string.story_reply_error)
+ }
+ }
+
+ }
+ }
+
+ private fun currentStoryId(): String? = currentAccount?.nodes?.getOrNull(uiState.value.currentImage)?.id
+
+ fun replyChanged(text: String) {
+ _uiState.update { currentUiState ->
+ currentUiState.copy(reply = text)
+ }
+ }
+
+ fun dismissError() {
+ _uiState.update { currentUiState ->
+ currentUiState.copy(errorMessage = null)
+ }
+ }
+
+ fun shownSnackbar() {
+ _uiState.update { currentUiState ->
+ currentUiState.copy(snackBar = null)
+ }
+ }
+
+ fun currentProfileId(): String? = currentAccount?.user?.id
+
+}
+
+class StoriesViewModelFactory(
+ val application: Application,
+ val carousel: StoryCarousel?,
+ val userId: String?,
+ val selfCarousel: List?
+) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return modelClass.getConstructor(Application::class.java, StoryCarousel::class.java, String::class.java, List::class.java).newInstance(application, carousel, userId, selfCarousel)
+ }
+}
diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt b/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt
new file mode 100644
index 00000000..ba0bcbd9
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt
@@ -0,0 +1,210 @@
+package org.pixeldroid.app.stories
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.RenderEffect
+import android.graphics.Shader
+import android.os.Build
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.LifecycleCoroutineScope
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import kotlinx.coroutines.launch
+import org.pixeldroid.app.R
+import org.pixeldroid.app.databinding.StoryCarouselBinding
+import org.pixeldroid.app.databinding.StoryCarouselItemBinding
+import org.pixeldroid.app.databinding.StoryCarouselSelfBinding
+import org.pixeldroid.app.postCreation.camera.CameraActivity
+import org.pixeldroid.app.postCreation.camera.CameraFragment
+import org.pixeldroid.app.utils.api.objects.CarouselUserContainer
+import org.pixeldroid.app.utils.api.objects.Story
+import org.pixeldroid.app.utils.api.objects.StoryCarousel
+import org.pixeldroid.app.utils.di.PixelfedAPIHolder
+
+
+/**
+ * Adapter that has either 1 or 0 items, to show stories widget or not
+ */
+class StoriesAdapter(val lifecycleScope: LifecycleCoroutineScope, val apiHolder: PixelfedAPIHolder) : RecyclerView.Adapter() {
+ var carousel: StoryCarousel? = null
+
+ /**
+ * Whether to show stories or not.
+ *
+ * Changing this property will immediately notify the Adapter to change the item it's
+ * presenting.
+ */
+ var showStories: Boolean = false
+ set(newValue) {
+ val oldValue = field
+
+ if (oldValue && !newValue) {
+ notifyItemRemoved(0)
+ } else if (newValue && !oldValue) {
+ notifyItemInserted(0)
+ } else if (oldValue && newValue) {
+ notifyItemChanged(0)
+ }
+ field = newValue
+
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StoryCarouselViewHolder {
+ return StoryCarouselViewHolder.create(parent, ::noStories)
+ }
+
+ override fun onBindViewHolder(holder: StoryCarouselViewHolder, position: Int) {
+ holder.bind(carousel)
+ }
+
+ override fun getItemViewType(position: Int): Int = 0
+
+ override fun getItemCount(): Int = if (showStories) 1 else 0
+
+ private fun noStories(){
+ showStories = false
+ }
+
+ private fun gotStories(newCarousel: StoryCarousel) {
+ carousel = newCarousel
+ showStories = true
+ }
+
+ fun refreshStories(){
+ lifecycleScope.launch {
+ try{
+ val api = apiHolder.api ?: apiHolder.setToCurrentUser()
+ val carousel = api.carousel()
+
+ // If there are stories from someone else or our stories to show, show them
+ if (carousel.nodes?.isEmpty() == false || carousel.self?.nodes?.isEmpty() == false) {
+ // Pass carousel to adapter
+ gotStories(carousel)
+ } else {
+ noStories()
+ }
+ } catch (exception: Exception){
+ noStories()
+ }
+ }
+ }
+
+}
+
+class StoryCarouselViewHolder(val binding: StoryCarouselBinding) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(carousel: StoryCarousel?) {
+ val adapter = StoriesListAdapter()
+ binding.storyCarousel.adapter = adapter
+
+ carousel?.let { adapter.initCarousel(it) }
+ }
+
+ companion object {
+ fun create(parent: ViewGroup, noStories: () -> Unit): StoryCarouselViewHolder {
+ val itemBinding = StoryCarouselBinding.inflate(
+ LayoutInflater.from(parent.context), parent, false
+ )
+ return StoryCarouselViewHolder(itemBinding)
+ }
+ }
+}
+
+
+class StoriesListAdapter : RecyclerView.Adapter() {
+
+ private var storyCarousel: StoryCarousel? = null
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ return if(viewType == R.layout.story_carousel_self){
+ val v = StoryCarouselSelfBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ v.myStory.visibility =
+ if (storyCarousel?.self?.nodes?.isEmpty() == false) View.VISIBLE
+ else View.GONE
+
+ AddViewHolder(v)
+ }
+ else {
+ val v = StoryCarouselItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ ViewHolder(v)
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if(position == 0) R.layout.story_carousel_self
+ else R.layout.story_carousel_item
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ if(position > 0) {
+ val carouselPosition = position - 1
+ storyCarousel?.nodes?.get(carouselPosition)?.let { (holder as ViewHolder).bindItem(it) }
+ holder.itemView.setOnClickListener {
+ storyCarousel?.nodes?.get(carouselPosition)?.user?.id?.let { userId ->
+ val intent = Intent(holder.itemView.context, StoriesActivity::class.java)
+ intent.putExtra(StoriesActivity.STORY_CAROUSEL, storyCarousel)
+ intent.putExtra(StoriesActivity.STORY_CAROUSEL_USER_ID, userId)
+ holder.itemView.context.startActivity(intent)
+ }
+ }
+ } else {
+ storyCarousel?.self?.nodes?.let { (holder as? AddViewHolder)?.bindItem(it.filterNotNull()) }
+ }
+ }
+
+ override fun getItemCount(): Int {
+ // If the storyCarousel is not set, the carousel is not shown, so itemCount of 0
+ return (storyCarousel?.nodes?.size?.plus(1)) ?: 0
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun initCarousel(carousel: StoryCarousel){
+ storyCarousel = carousel
+ notifyDataSetChanged()
+ }
+
+ class AddViewHolder(private val itemBinding: StoryCarouselSelfBinding) : RecyclerView.ViewHolder(itemBinding.root) {
+ fun bindItem(nodes: List) {
+ itemBinding.addStory.setOnClickListener {
+ val intent = Intent(itemView.context, CameraActivity::class.java)
+ intent.putExtra(CameraFragment.CAMERA_ACTIVITY_STORY, true)
+ itemView.context.startActivity(intent)
+ }
+ itemBinding.myStory.setOnClickListener {
+ val intent = Intent(itemView.context, StoriesActivity::class.java)
+ intent.putExtra(StoriesActivity.STORY_CAROUSEL_SELF, nodes.toTypedArray())
+ itemView.context.startActivity(intent)
+ }
+
+ // Only show image on new Android versions, because the transformations need it and the
+ // text is not legible without the transformations
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ Glide.with(itemBinding.root).load(nodes.firstOrNull()?.src).into(itemBinding.carouselImageView)
+ val value = 70 * 255 / 100
+ val darkFilterRenderEffect = PorterDuffColorFilter(Color.argb(value, 0, 0, 0), PorterDuff.Mode.SRC_ATOP)
+ val blurRenderEffect =
+ RenderEffect.createBlurEffect(
+ 4f, 4f, Shader.TileMode.MIRROR
+ )
+ val combinedEffect = RenderEffect.createColorFilterEffect(darkFilterRenderEffect, blurRenderEffect)
+ itemBinding.carouselImageView.setRenderEffect(combinedEffect)
+ }
+
+ }
+ }
+
+ class ViewHolder(private val itemBinding: StoryCarouselItemBinding) :
+ RecyclerView.ViewHolder(itemBinding.root) {
+ fun bindItem(user: CarouselUserContainer) {
+ Glide.with(itemBinding.root).load(user.nodes?.firstOrNull()?.src).into(itemBinding.carouselImageView)
+ Glide.with(itemBinding.root).load(user.user?.avatar).circleCrop().into(itemBinding.storyAuthorProfilePicture)
+
+ itemBinding.username.text = user.user?.username ?: "" //TODO check which one to use here!
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoryProgress.kt b/app/src/main/java/org/pixeldroid/app/stories/StoryProgress.kt
new file mode 100644
index 00000000..ebb2e2ba
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/stories/StoryProgress.kt
@@ -0,0 +1,72 @@
+package org.pixeldroid.app.stories
+
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.PixelFormat
+import android.graphics.drawable.Drawable
+
+/**
+ * Copied & adapted from AntennaPod's EchoProgress class because it looked great and is very simple
+ * AntennaPod/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java
+ */
+class StoryProgress(private val numStories: Int) : Drawable() {
+ private val paint: Paint = Paint().apply {
+ flags = Paint.ANTI_ALIAS_FLAG
+ style = Paint.Style.STROKE
+ strokeJoin = Paint.Join.ROUND
+ strokeCap = Paint.Cap.ROUND
+ color = -0x1
+ }
+
+ var progress = 0f
+ var currentStory: Int = 0
+
+ override fun draw(canvas: Canvas) {
+ paint.strokeWidth = 0.5f * bounds.height()
+ val y = 0.5f * bounds.height()
+ val sectionWidth = 1.0f * bounds.width() / numStories
+ val sectionPadding = 0.03f * sectionWidth
+ // Iterate over stories
+ for (i in 0 until numStories) {
+ if (i < currentStory) {
+ // If current drawing position is smaller than current story, the paint we will use
+ // should be opaque: this story is already "seen"
+ paint.alpha = 255
+ } else {
+ // Otherwise it should be somewhat transparent, denoting it is not yet seen
+ paint.alpha = 100
+ }
+ // Draw an entire line with the paint, for now ignoring partial progress within the
+ // current story
+ canvas.drawLine(
+ i * sectionWidth + sectionPadding,
+ y,
+ (i + 1) * sectionWidth - sectionPadding,
+ y,
+ paint
+ )
+ // If current position is equal to progress, we are drawing the current story. Thus we
+ // should account for partial progress and paint the beginning of the line opaquely
+ if (i == currentStory) {
+ paint.alpha = 255
+ canvas.drawLine(
+ currentStory * sectionWidth + sectionPadding,
+ y,
+ currentStory * sectionWidth + sectionPadding + progress * (sectionWidth - 2 * sectionPadding),
+ y,
+ paint
+ )
+ }
+ }
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun getOpacity(): Int {
+ return PixelFormat.TRANSLUCENT
+ }
+
+ override fun setAlpha(alpha: Int) {}
+ override fun setColorFilter(cf: ColorFilter?) {}
+}
+
diff --git a/app/src/main/java/org/pixeldroid/app/utils/BaseActivity.kt b/app/src/main/java/org/pixeldroid/app/utils/BaseActivity.kt
index f47d5e48..4508f14c 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/BaseActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/BaseActivity.kt
@@ -1,12 +1,11 @@
package org.pixeldroid.app.utils
import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import javax.inject.Inject
-open class BaseActivity : AppCompatActivity() {
+open class BaseActivity : org.pixeldroid.common.ThemedActivity() {
@Inject
lateinit var db: AppDatabase
@@ -19,7 +18,7 @@ open class BaseActivity : AppCompatActivity() {
}
override fun onSupportNavigateUp(): Boolean {
- onBackPressed()
+ onBackPressedDispatcher.onBackPressed()
return true
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/utils/BaseFragment.kt b/app/src/main/java/org/pixeldroid/app/utils/BaseFragment.kt
index edac820a..e415ccb7 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/BaseFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/BaseFragment.kt
@@ -1,7 +1,10 @@
package org.pixeldroid.app.utils
import android.os.Bundle
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.pixeldroid.app.R
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import javax.inject.Inject
@@ -22,4 +25,18 @@ open class BaseFragment: Fragment() {
(requireActivity().application as PixelDroidApplication).getAppComponent().inject(this)
}
+ internal val requestPermissionDownloadPic =
+ registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted: Boolean ->
+ if (!isGranted) {
+ context?.let {
+ MaterialAlertDialogBuilder(it)
+ .setMessage(R.string.write_permission_download_pic)
+ .setNegativeButton(android.R.string.ok) { _, _ -> }
+ .show()
+ }
+
+ }
+ }
}
diff --git a/app/src/main/java/org/pixeldroid/app/utils/BaseThemedWithBarActivity.kt b/app/src/main/java/org/pixeldroid/app/utils/BaseThemedWithBarActivity.kt
deleted file mode 100644
index fa07abfc..00000000
--- a/app/src/main/java/org/pixeldroid/app/utils/BaseThemedWithBarActivity.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.pixeldroid.app.utils
-
-import android.os.Bundle
-
-open class BaseThemedWithBarActivity : BaseActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- // Set theme when we chose one
- themeActionBar()?.let { setTheme(it) }
- super.onCreate(savedInstanceState)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/utils/BaseThemedWithoutBarActivity.kt b/app/src/main/java/org/pixeldroid/app/utils/BaseThemedWithoutBarActivity.kt
deleted file mode 100644
index 1c825f18..00000000
--- a/app/src/main/java/org/pixeldroid/app/utils/BaseThemedWithoutBarActivity.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.pixeldroid.app.utils
-
-import android.os.Bundle
-
-open class BaseThemedWithoutBarActivity : BaseActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- // Set theme when we chose one
- themeNoActionBar()?.let { setTheme(it) }
- super.onCreate(savedInstanceState)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/utils/ImageConverter.kt b/app/src/main/java/org/pixeldroid/app/utils/ImageConverter.kt
index afbf7355..cdabe869 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/ImageConverter.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/ImageConverter.kt
@@ -24,6 +24,7 @@ fun setProfileImageFromURL(view : View, url : String?, image : ImageView) {
* @param image, the imageView into which we will load the image
*/
fun setSquareImageFromURL(view : View, url : String?, image : ImageView, blurhash: String? = null) {
+ //TODO performance: placeholder here takes a lot of time to compute and this is not async!
Glide.with(view).load(url).placeholder(
blurhash?.let { BlurHashDecoder.blurHashBitmap(view.resources, it, 32, 32) }
).apply(RequestOptions().centerCrop()).into(image)
diff --git a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt
index 69090468..14981b0d 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt
@@ -161,30 +161,6 @@ fun setThemeFromPreferences(preferences: SharedPreferences, resources: Resources
}
}
-@StyleRes
-fun Context.themeNoActionBar(): Int? {
- return when(PreferenceManager.getDefaultSharedPreferences(this).getInt("themeColor", 0)) {
- // No theme was chosen: the user wants to use the system dynamic color (from wallpaper for example)
- -1 -> null
- 1 -> R.style.AppTheme2_NoActionBar
- 2 -> R.style.AppTheme3_NoActionBar
- 3 -> R.style.AppTheme4_NoActionBar
- else -> R.style.AppTheme5_NoActionBar
- }
-}
-
-@StyleRes
-fun Context.themeActionBar(): Int? {
- return when(PreferenceManager.getDefaultSharedPreferences(this).getInt("themeColor", 0)) {
- // No theme was chosen: the user wants to use the system dynamic color (from wallpaper for example)
- -1 -> null
- 1 -> R.style.AppTheme2
- 2 -> R.style.AppTheme3
- 3 -> R.style.AppTheme4
- else -> R.style.AppTheme5
- }
-}
-
@ColorInt
fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK)
diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt
index 62a96d29..70a79b38 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt
@@ -23,6 +23,7 @@ import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.*
import retrofit2.http.Field
import java.time.Instant
+import java.util.concurrent.TimeUnit
/*
@@ -51,7 +52,9 @@ interface PixelfedAPI {
.client(
OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor)
// Only do secure-ish TLS connections (no HTTP or very old SSL/TLS)
- .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)).build()
+ .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
+ .readTimeout(20, TimeUnit.SECONDS)
+ .build()
)
.build().create(PixelfedAPI::class.java)
}
@@ -74,6 +77,7 @@ interface PixelfedAPI {
OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor)
// Only do secure-ish TLS connections (no HTTP or very old SSL/TLS)
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
+ .readTimeout(20, TimeUnit.SECONDS)
.authenticator(TokenAuthenticator(user, db, pixelfedAPIHolder))
.addInterceptor {
it.request().newBuilder().run {
@@ -161,6 +165,7 @@ interface PixelfedAPI {
@Field("poll[expires_in]") poll_expires: List? = null,
@Field("poll[multiple]") poll_multiple: List? = null,
@Field("poll[hide_totals]") poll_hideTotals: List? = null,
+ //FIXME this should be able to take a boolean or at least "true"/"false" but only "0"/"1" works
@Field("sensitive") sensitive: Int? = null,
@Field("spoiler_text") spoiler_text: String? = null,
@Field("visibility") visibility: String = "public",
@@ -231,6 +236,43 @@ interface PixelfedAPI {
@Query("post_id") post_id: String,
)
+ @GET("/api/pixelfed/v1/stories/self-carousel")
+ suspend fun carousel(): StoryCarousel
+
+ @POST("/api/v1.1/stories/seen")
+ suspend fun storySeen(
+ @Query("id") id: String
+ )
+
+ @POST("/api/v1.1/stories/comment")
+ suspend fun storyComment(
+ @Query("sid") sid: String,
+ @Query("caption") caption: String
+ )
+
+ @Multipart
+ @POST("/api/v1.1/stories/add")
+ fun storyUpload(
+ @Part file: MultipartBody.Part,
+ // The API takes this value but then overwrites it in /api/v1.1/stories/publish, so ignore this
+ @Part duration: MultipartBody.Part? = null,
+ ): Observable
+
+ @POST("/api/v1.1/stories/publish")
+ suspend fun storyPublish(
+ @Query("media_id") media_id: String,
+ //From 0 to 30, duration in seconds of the story
+ @Query("duration") duration: Int = 10,
+ //FIXME this should be able to take a boolean or at least "true"/"false" but only "0"/"1" works. Same issue as sensitive boolean in postStatus
+ @Query("can_reply") can_reply: String,
+ @Query("can_react") can_react: String,
+ )
+
+ @POST("/api/v1.1/stories/self-expire/{id}")
+ suspend fun deleteCarousel(
+ @Path("id") storyId: String
+ )
+
//Used in our case to retrieve comments for a given status
@GET("/api/v1/statuses/{id}/context")
suspend fun statusComments(
diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Account.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Account.kt
index d084799c..402d58dd 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Account.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Account.kt
@@ -57,11 +57,13 @@ data class Account(
suspend fun openAccountFromId(id: String, api : PixelfedAPI, context: Context) {
val account = try {
api.getAccount(id)
- } catch (exception: IOException) {
- Log.e("GET ACCOUNT ERROR", exception.toString())
- return
- } catch (exception: HttpException) {
- Log.e("ERROR CODE", exception.code().toString())
+ } catch (exception: Exception) {
+ val toLog = if (exception is HttpException) {
+ exception.code().toString()
+ } else {
+ exception.toString()
+ }
+ Log.e("GET ACCOUNT ERROR", toLog)
return
}
//Open the account page in a separate activity
diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Attachment.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Attachment.kt
index ad924fff..5b8ab940 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Attachment.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Attachment.kt
@@ -18,6 +18,12 @@ data class Attachment(
//Deprecated attributes
val text_url: String? = null, //URL
+
+ //Pixelfed's Story upload response... TODO make the server return a regular Attachment?
+ val msg: String? = null,
+ val media_id: String? = null,
+ val media_url: String? = null,
+ val media_type: String? = null,
) : Serializable {
enum class AttachmentType: Serializable {
unknown, image, gifv, video, audio
diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Status.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Status.kt
index 5ce0664f..34941389 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Status.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Status.kt
@@ -1,8 +1,10 @@
package org.pixeldroid.app.utils.api.objects
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.app.DownloadManager
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.database.Cursor
import android.net.Uri
import android.os.Environment
@@ -11,6 +13,7 @@ import androidx.core.net.toUri
import com.google.android.material.snackbar.Snackbar
import org.pixeldroid.app.R
import org.pixeldroid.app.posts.getDomain
+import org.pixeldroid.app.utils.getMimeType
import java.io.File
import java.io.Serializable
import java.time.Instant
@@ -148,11 +151,13 @@ open class Status(
)
val file = path.toUri()
+
+
val shareIntent: Intent = Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, file)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
- type = "image/$ext"
+ type = file.getMimeType(context.contentResolver)
}, null)
context.startActivity(shareIntent)
diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt
new file mode 100644
index 00000000..bf3a7395
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt
@@ -0,0 +1,43 @@
+package org.pixeldroid.app.utils.api.objects
+
+import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
+import java.io.Serializable
+import java.time.Instant
+
+data class StoryCarousel(
+ val self: CarouselUserContainer?,
+ val nodes: List?
+): Serializable
+
+data class CarouselUser(
+ val id: String?,
+ val username: String?,
+ val username_acct: String?,
+ val avatar: String?, // URL to account avatar
+ val local: Boolean?, // Is this story from the local instance?
+ val is_author: Boolean?, // Is this me? (seems redundant with id)
+): Serializable
+
+/**
+ * Container with a description of the [user] and a list of stories ([nodes])
+ */
+data class CarouselUserContainer(
+ val user: CarouselUser?,
+ val nodes: List?,
+): Serializable {
+ constructor(user: UserDatabaseEntity, nodes: List?) : this(
+ CarouselUser(user.user_id, user.username, null, user.avatar_static,
+ local = true,
+ is_author = true
+ ), nodes)
+}
+
+data class Story(
+ val id: String?,
+ val pid: String?, // id of author
+ val type: String?, //TODO make enum of this? examples: "photo", ???
+ val src: String?, // URL to photo of story
+ val duration: Int?, //Time in seconds that the Story should be shown
+ val seen: Boolean?, //Indication of whether this story has been seen. Set to true using carouselSeen
+ val created_at: Instant?, //ISO 8601 Datetime
+): Serializable
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt
index cdad0047..11b3f8ca 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt
@@ -10,6 +10,8 @@ import dagger.Component
import org.pixeldroid.app.directMessages.ui.main.ConversationsViewModel
import org.pixeldroid.app.postCreation.PostCreationViewModel
import org.pixeldroid.app.profile.EditProfileViewModel
+import org.pixeldroid.app.stories.StoriesViewModel
+import org.pixeldroid.app.stories.StoryCarouselViewHolder
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
import javax.inject.Singleton
@@ -24,6 +26,7 @@ interface ApplicationComponent {
fun inject(postCreationViewModel: PostCreationViewModel)
fun inject(editProfileViewModel: EditProfileViewModel)
fun inject(editProfileViewModel: ConversationsViewModel)
+ fun inject(storiesViewModel: StoriesViewModel)
val context: Context?
val application: Application?
diff --git a/app/src/main/res/color/selector_story_post.xml b/app/src/main/res/color/selector_story_post.xml
new file mode 100644
index 00000000..68b10156
--- /dev/null
+++ b/app/src/main/res/color/selector_story_post.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/selector_story_post_text.xml b/app/src/main/res/color/selector_story_post_text.xml
new file mode 100644
index 00000000..f864921f
--- /dev/null
+++ b/app/src/main/res/color/selector_story_post_text.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/arrow_forward.xml b/app/src/main/res/drawable/arrow_forward.xml
new file mode 100644
index 00000000..23072282
--- /dev/null
+++ b/app/src/main/res/drawable/arrow_forward.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/bug_report_black_24dp.xml b/app/src/main/res/drawable/bug_report_black_24dp.xml
deleted file mode 100644
index 3632a6af..00000000
--- a/app/src/main/res/drawable/bug_report_black_24dp.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/mascot.xml b/app/src/main/res/drawable/mascot.xml
index 4ce1fea3..55811b0e 100644
--- a/app/src/main/res/drawable/mascot.xml
+++ b/app/src/main/res/drawable/mascot.xml
@@ -1,8 +1,11 @@
+ android:viewportWidth="403.75"
+ android:viewportHeight="437.6"
+ android:width="100dp"
+ android:height="108dp">
+
+
@@ -808,4 +811,5 @@
android:strokeColor="#000000"
android:strokeWidth="1.32292"
android:strokeLineCap="round" />
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/mascot_small.xml b/app/src/main/res/drawable/mascot_small.xml
deleted file mode 100644
index 55811b0e..00000000
--- a/app/src/main/res/drawable/mascot_small.xml
+++ /dev/null
@@ -1,815 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/pause.xml b/app/src/main/res/drawable/pause.xml
new file mode 100644
index 00000000..f701d6f8
--- /dev/null
+++ b/app/src/main/res/drawable/pause.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/play.xml b/app/src/main/res/drawable/play.xml
new file mode 100644
index 00000000..0870be8f
--- /dev/null
+++ b/app/src/main/res/drawable/play.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/play_pause.xml b/app/src/main/res/drawable/play_pause.xml
new file mode 100644
index 00000000..9c956cb2
--- /dev/null
+++ b/app/src/main/res/drawable/play_pause.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/story_play.xml b/app/src/main/res/drawable/story_play.xml
new file mode 100644
index 00000000..476e56f7
--- /dev/null
+++ b/app/src/main/res/drawable/story_play.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/translate_black_24dp.xml b/app/src/main/res/drawable/translate_black_24dp.xml
deleted file mode 100644
index cf62cd35..00000000
--- a/app/src/main/res/drawable/translate_black_24dp.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml
deleted file mode 100644
index 7cd1be30..00000000
--- a/app/src/main/res/layout/activity_about.xml
+++ /dev/null
@@ -1,136 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml
index 4861ad05..3b1cbf9d 100644
--- a/app/src/main/res/layout/activity_camera.xml
+++ b/app/src/main/res/layout/activity_camera.xml
@@ -1,13 +1,34 @@
-
+
+
+
+
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/top_bar" />
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_collection.xml b/app/src/main/res/layout/activity_collection.xml
index 13118b3b..d825b2a0 100644
--- a/app/src/main/res/layout/activity_collection.xml
+++ b/app/src/main/res/layout/activity_collection.xml
@@ -1,14 +1,30 @@
-
+
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml
index 27f23e6c..db8e7bb1 100644
--- a/app/src/main/res/layout/activity_edit_profile.xml
+++ b/app/src/main/res/layout/activity_edit_profile.xml
@@ -1,184 +1,211 @@
-
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
-
-
-
-
+ android:background="?attr/colorSecondaryContainer"
+ android:fitsSystemWindows="true"
+ android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
-
-
+ android:minHeight="?attr/actionBarSize"
+ app:title="@string/edit_profile" />
+
-
+
-
+
+
+
-
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/profilePic">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:ems="10"
+ android:hint="@string/your_name"
+ android:imeOptions="actionDone" />
+
-
+
-
-
+ android:hint="@string/your_bio" />
+
+
+
+
+
+
+ app:layout_constraintTop_toTopOf="parent" />
+
+
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_followers.xml b/app/src/main/res/layout/activity_followers.xml
index e93039d1..3bd91979 100644
--- a/app/src/main/res/layout/activity_followers.xml
+++ b/app/src/main/res/layout/activity_followers.xml
@@ -1,5 +1,30 @@
-
\ No newline at end of file
+ android:fitsSystemWindows="true"
+ android:layout_height="match_parent">
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml
index 04b5c54d..9987fd18 100644
--- a/app/src/main/res/layout/activity_login.xml
+++ b/app/src/main/res/layout/activity_login.xml
@@ -21,7 +21,9 @@
diff --git a/app/src/main/res/layout/activity_post.xml b/app/src/main/res/layout/activity_post.xml
index c0a01051..4c947cb7 100644
--- a/app/src/main/res/layout/activity_post.xml
+++ b/app/src/main/res/layout/activity_post.xml
@@ -3,16 +3,27 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scrollview"
+ android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".posts.PostActivity">
+
+
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml
index 08300107..be52a22c 100644
--- a/app/src/main/res/layout/activity_profile.xml
+++ b/app/src/main/res/layout/activity_profile.xml
@@ -3,16 +3,27 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
+ android:fitsSystemWindows="true"
android:layout_height="match_parent"
tools:context=".profile.ProfileActivity">
+
+
diff --git a/app/src/main/res/layout/activity_report.xml b/app/src/main/res/layout/activity_report.xml
index f0deeac4..61d0cd65 100644
--- a/app/src/main/res/layout/activity_report.xml
+++ b/app/src/main/res/layout/activity_report.xml
@@ -1,11 +1,29 @@
-
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@+id/top_bar"
+ tools:text="Report @user's post" />
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml
index 56e1676f..6783157f 100644
--- a/app/src/main/res/layout/activity_search.xml
+++ b/app/src/main/res/layout/activity_search.xml
@@ -1,13 +1,29 @@
-
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+
+
+
@@ -26,5 +42,4 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_stories.xml b/app/src/main/res/layout/activity_stories.xml
new file mode 100644
index 00000000..a0c58001
--- /dev/null
+++ b/app/src/main/res/layout/activity_stories.xml
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_trending.xml b/app/src/main/res/layout/activity_trending.xml
index 784b355e..eb806dfd 100644
--- a/app/src/main/res/layout/activity_trending.xml
+++ b/app/src/main/res/layout/activity_trending.xml
@@ -1,11 +1,28 @@
-
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@+id/top_bar">
@@ -58,4 +75,5 @@
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/error_layout.xml b/app/src/main/res/layout/error_layout.xml
index 8925f2f2..8c8d19e4 100644
--- a/app/src/main/res/layout/error_layout.xml
+++ b/app/src/main/res/layout/error_layout.xml
@@ -1,17 +1,17 @@
-
+ tools:visibility="visible">
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/top_bar"
+ app:showCaption="true" />
+ android:minHeight="?attr/actionBarSize"
+ android:theme="?attr/actionBarTheme"
+ android:background="?attr/colorSecondaryContainer"
+ app:titleTextColor="?attr/colorOnSecondaryContainer"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
-
+
+
+
+
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/top_bar">
@@ -66,8 +118,8 @@
android:layout_marginStart="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/delete"
- android:tooltipText='@string/delete'
android:src="@drawable/delete_30dp"
+ android:tooltipText='@string/delete'
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/savePhotoButton"
app:layout_constraintTop_toTopOf="parent" />
@@ -79,8 +131,8 @@
android:layout_marginStart="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/edit"
- android:tooltipText='@string/edit'
android:src="@drawable/ic_baseline_edit_30"
+ android:tooltipText='@string/edit'
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/removePhotoButton"
app:layout_constraintTop_toTopOf="parent" />
@@ -92,8 +144,8 @@
android:layout_marginEnd="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/add_photo"
- android:tooltipText='@string/add_photo'
android:src="@drawable/add_photo_button"
+ android:tooltipText='@string/add_photo'
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
diff --git a/app/src/main/res/layout/fragment_post_submission.xml b/app/src/main/res/layout/fragment_post_submission.xml
index 4c418050..60b1201c 100644
--- a/app/src/main/res/layout/fragment_post_submission.xml
+++ b/app/src/main/res/layout/fragment_post_submission.xml
@@ -12,7 +12,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
+ android:background="?attr/colorSecondaryContainer"
android:theme="?attr/actionBarTheme"
+ app:titleTextColor="?attr/colorOnSecondaryContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -76,6 +78,7 @@
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
+
@@ -103,7 +106,7 @@
app:layout_constraintEnd_toEndOf="parent">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml
index 8fbf0e84..2ab4819b 100644
--- a/app/src/main/res/layout/fragment_search.xml
+++ b/app/src/main/res/layout/fragment_search.xml
@@ -1,9 +1,12 @@
-
+ android:layout_height="match_parent">
+
+ android:textColor="?attr/colorOnSecondaryContainer" />
+ android:textColor="?attr/colorOnSecondaryContainer" />
+ app:layout_constraintTop_toBottomOf="@id/hashtagsCardView">
+ android:textColor="?attr/colorOnSecondaryContainer" />
+ app:layout_constraintTop_toBottomOf="@id/accountsCardView">
+ android:textColor="?attr/colorOnSecondaryContainer" />
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/open_source_item.xml b/app/src/main/res/layout/open_source_item.xml
deleted file mode 100644
index d082ffc7..00000000
--- a/app/src/main/res/layout/open_source_item.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/open_source_license.xml b/app/src/main/res/layout/open_source_license.xml
deleted file mode 100644
index 3895cfec..00000000
--- a/app/src/main/res/layout/open_source_license.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/settings.xml b/app/src/main/res/layout/settings.xml
index de6591a2..3fe40912 100644
--- a/app/src/main/res/layout/settings.xml
+++ b/app/src/main/res/layout/settings.xml
@@ -1,9 +1,27 @@
-
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/story_carousel.xml b/app/src/main/res/layout/story_carousel.xml
new file mode 100644
index 00000000..6c3bb87b
--- /dev/null
+++ b/app/src/main/res/layout/story_carousel.xml
@@ -0,0 +1,17 @@
+
+
+
diff --git a/app/src/main/res/layout/story_carousel_item.xml b/app/src/main/res/layout/story_carousel_item.xml
new file mode 100644
index 00000000..5e77571c
--- /dev/null
+++ b/app/src/main/res/layout/story_carousel_item.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/story_carousel_self.xml b/app/src/main/res/layout/story_carousel_self.xml
new file mode 100644
index 00000000..eb963e88
--- /dev/null
+++ b/app/src/main/res/layout/story_carousel_self.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/raw/keep.xml b/app/src/main/res/raw/keep.xml
new file mode 100644
index 00000000..13e36520
--- /dev/null
+++ b/app/src/main/res/raw/keep.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties
new file mode 100644
index 00000000..d5a3ddc9
--- /dev/null
+++ b/app/src/main/res/resources.properties
@@ -0,0 +1 @@
+unqualifiedResLocale=en-US
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 510d9624..2dfec527 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -52,7 +52,6 @@
التعليق: تم نشر%1$s!
خطأ في التعليق!
مشاركة الصورة
- يجب عليك منح تصريح للكتابة قصد مشاركة الصور!
تحتاج إلى منح إذن الكتابة لتنزيل الصور!
لا يجب ان يكون التعليق فارغًا!
نُشِر في %1$s
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 6ecfc72d..a81ee98e 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -60,7 +60,6 @@
Comentari: %1$s publicat!
Error de comentari!
Compartir imatge
- Necessites concedir permís d’escriptura per compartir imatges!
Has de concedir permís d’escriptura per baixar imatges!
El comentari no ha de estar buit!
Publica\'t el %1$s
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index ea85fa7a..88d77011 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -69,7 +69,6 @@
Zveřejněno na %1$s
U tohoto příspěvku nejsou žádné komentáře…
Komentář nesmí být prázdný!
- Pro sdílení obrázků musíte udělit práva k zápisu!
Sdílet obrázek
Chyba komentáře!
Komentář: %1$s zveřejněn!
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 587ac99d..03514ac0 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -79,7 +79,6 @@
Feed konnte nicht geladen werden
Erstellt am %1$s
Für das Herunterladen von Bildern müssen Sie eine Schreibgenehmigung erteilen!
- Für die Teilen von Bildern müssen Sie eine Schreibgenehmigung erteilen!
Konnte den Folgestatus nicht erhalten
Wiederholen
Hier gibt\'s nichts zu sehen!
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 297e113d..7fad8794 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -64,7 +64,6 @@
¡Comentario: %1$s publicado!
¡Error al comentar!
Compartir imagen
- ¡Tienes que dar permiso de escritura para compartir fotos!
¡Tienes que dar permiso de escritura para descargar fotos!
¡Los comentarios no deben estar vacíos!
Publicado en %1$s
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 788e049f..ce7fae28 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -76,7 +76,6 @@
Ezin izan da jarraipen-botoia erakutsi
Ezin izan da segimendu-egoera lortu
Iruzkin
- Idazteko baimena eman behar duzu argazkiak partekatzeko!
%1$s(e)n argitaratua
TRAOLAK
KONTUAK
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 7b201667..f192bce7 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -56,7 +56,6 @@
منتشر شده در %1$s
نظر نمیتواند خالی باشد!
برای بارگیری تصاویر بایستی مجوز نوشتن را بدهید!
- برای همرسانی تصاویر باید مجوز نوشتن را بدهید!
همرسانی تصویر
خطا در درج نظر!
نظر: %1$s منتشر کرد!
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 66eb3f89..3165ff80 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -49,8 +49,7 @@
Téléversement de la publication avec succès
Publié le %1$s
Le commentaire ne doit pas être vide !
- Vous devez accorder une autorisation d\'écriture pour partager des photos !
- -
+ -
\nAbonné·e·s
Le téléversement de la publication a échoué
Une erreur s\'est produite lors du chargement
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index c7364f98..cbc31679 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -61,7 +61,6 @@
Comentario: %1$s publicado!
Fallo ao comentar!
Compartir Imaxe
- Tes que dar permiso de escritura para compartir fotos!
Tes que conceder permiso de escritura para descargar fotos!
O comentario non debe quedar baleiro!
Publicado o %1$s
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index f1937fe4..8e95e3de 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -106,7 +106,6 @@
Hozzászólás: %1$s közzétéve!
Hozzászólási hiba!
Kép megosztása
- Írási engedélyt kell adnia a képek megosztásához!
Írási engedélyt kell adnia a képek letöltéséhez!
A hozzászólás nem lehet üres!
Nincsenek hozzászólások a bejegyzésnél…
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 75ab0e7b..a169bc81 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -53,7 +53,6 @@
Nessuna descrizione
Postato su %1$s
Il commento non deve essere vuoto!
- È necessario concedere i permessi di scrittura per condividere le immagini!
Condividi immagine
Errore nel commento!
Commento: %1$s postato!
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 02c8f4bf..4e880f9c 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -39,7 +39,6 @@
インスタンス情報が取得できませんでした
%1$s の投稿
アカウント
- 画像を共有するためには書き込みの権限を更新する必要があります
画像をダウンロードするには書き込みの権限を更新する必要があります
コメントは空欄にできません
コメント: %1$s が投稿されました
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index 96f33b7f..cc77e097 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -100,7 +100,6 @@
എപിഐ ഈ ഇൻസ്റ്റൻസിൽ സജീവമല്ലാ.ഇത് സജീവമാക്കാൻ നിങ്ങളുടെ അഡ്മിനിസ്ട്രേറ്ററെ ബന്ധപ്പെടുക.
ഫീഡ് ലഭ്യമാക്കാൻ സാധിച്ചില്ല
ചിത്രങ്ങൾ ഡൗൺലോഡ് ചെയ്യാൻ നിങ്ങൾ എഴുത്ത് അനുമതി നൽകണം!
- ചിത്രങ്ങൾ പങ്കിടാൻ നിങ്ങൾ എഴുത്ത് അനുമതി നൽകണം!
- %d
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml
deleted file mode 100644
index 78687208..00000000
--- a/app/src/main/res/values-night/styles.xml
+++ /dev/null
@@ -1,134 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 1f43fe1d..380e0241 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -70,7 +70,6 @@
Commentaar: %1$s gepost!
Fout met commentaar!
Afbeelding delen
- Je moet schrijven toestaan om afbeeldingen te delen!
Je moet schrijven toestaan om afbeeldingen te downloaden!
Kon volgstatus niet ophalen
Kon niet volgen
@@ -142,4 +141,6 @@
- %d
\nVolgers
+ Altijd gevoelig materiaal tonen
+ %1$s heeft gereageerd op je bericht
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 153f7c45..8a7aea9f 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -40,7 +40,6 @@
Bez opisu
Opublikowano %1$s
Komentarz nie może być pusty!
- Musisz nadać uprawnienia do zapisu, aby móc udostępniać obrazki!
Udostępnij
Błąd komentarza!
Komentarz: %1$s opublikowany!
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 65b4b545..2e5c4a66 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -146,7 +146,6 @@
Mudar para visualização em grade
Comentário: %1$s publicado!
Erro no comentário!
- Você precisa conceder permissão de gravação para compartilhar imagens!
Idioma
Excluir esta publicação\?
Algo deu errado…
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 5148923e..62e8b80c 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -84,7 +84,6 @@
Partilhar imagem
Erro ao comentar!
Precisa adicionar a permissão para transferir imagens!
- Precisa adicionar permissão para partilhar imagens!
Comentário: %1$s publicou!
Comentário
Adicionar um comentário
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 334586d5..7397d272 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -1,67 +1,66 @@
- Не удалось авторизоваться
- Ошибка получения токена
+ Не удалось пройти аутентификацию
+ Ошибка при получении токена
Настройки
Тема приложения
Тема
%1$s подписан(а) на вас
%1$s упомянул(а) вас
- %1$s поделился(ась) вашим постом
- пост
+ %1$s поделился(-ась) вашим постом
+ Запостить
Выйти
- Что такое инстанс\?
- РЕДАКТИРОВАТЬ
+ Что такое экземпляр?
+ Править
Сохранить в Галерею…
Загрузка…
Изображение успешно загружено
Переключить камеру
Галерея
- Нет комментариев…
- Домен вашего инстанса
- Подключение к Pixelfed
+ К этому посту нет комментариев …
+ Доменное имя вашего экземпляра
+ Подключить к Pixelfed
PixelDroid
Мой Профиль
Настройки
- Некоректный домен
- Не удалось запустить браузер, есть ли он у вас\?
+ Недопустимый домен
+ Не удалось запустить браузер, есть ли у вас такой?
%1$s оценил(и) ваш пост
Описание…
Загрузка не удалась, попробуйте ещё раз
Поделиться изображением…
- Вам необходимо быть в сети чтобы добавить аккаунт и использовать PixelDroid :(
- CW / NSFW / Медиа для 18+
-\n(кликните что бы показать)
- Не удалось зарегистрировать приложение на этом инстансе
+ Чтобы добавить Ваш первый аккаунт и использовать PixelDroid Вам нужно быть онлайн :(
+ CW / NSFW / Скрытые Медиа
+\n (нажмите, чтобы показать)
+ Не удалось зарегистрировать приложение на этом сервере
Сделать снимок
Добавить другой аккаунт Pixelfed
Добавить аккаунт
- Не удалось получить информацию об инстансе
- Комментарий
+ Не удалось получить информацию об экземпляре
+ Комментировать
Комментарий: %1$s опубликован!
- Ошибка комментария!
+ Ошибка комментирования!
Поделиться изображением
- Вы должны дать разрешение на запись для загрузки изображений!
- Вы должны дать разрешение на запись чтобы делиться фотографиями!
- Комментарий не может быть пустым!
+ Вам необходимо предоставить разрешение на запись чтобы скачивать фотографии!
+ Комментарий не должен быть пустым!
Опубликовано в %1$s
- Нет описания
+ Описание отсутствует
Не удалось загрузить ленту
При загрузке что-то пошло не так
Ошибка загрузки поста
Пост успешно загружен
- Не удалось загрузить пост
+ Загрузка поста не удалась
Ошибка загрузки: некорректный формат запроса
Ошибка загрузки: неверный формат изображения.
Ошибка загрузки изображения!
Изображение успешно сохранено
- Не удалось сохранить изображение
+ Не могу сохранить изображение
Тёмная
Светлая
По умолчанию (как в системе)
Не удалось получить статус подписки
Не удалась подписаться
- Это действие недопустимо
+ Это действие не разрешено
Не удалось отписаться
Токен доступа недействителен
-
@@ -78,49 +77,49 @@
АККАУНТЫ
ХЭШТЕГИ
Не удалось отобразить кнопку подписки
- Изображение, которое будет опубликовано
+ Публикуемое изображение
Загрузка медиа завершена
Повторить
- Ошибка загрузки медиа, попробуйте ещё раз или проверьте состояние сети
- Здесь нечего смотреть!
- Открыть меню навигации
- Панде грустно. Потяните, чтобы обновить.
+ Не удалось загрузить медиа, повторите попытку или проверьте состояние сети
+ Здесь не на что смотреть!
+ Открыть всплывающее меню
+ Панда недовольна. Потяните за кнопку обновления, чтобы попробовать еще раз.
Что-то пошло не так…
- ОБЗОР
+ Откройте для себя
Изображение профиля
Не удалось отправить жалобу
- Отправлено {gmd_check_circle}
+ Жалоба отправлена
Пожаловаться на пост @%1$s
- Дополнительное сообщение для модераторов/администраторов
+ Необязательное сообщение для модеров/админов
Поделиться ссылкой
Пожаловаться
Больше опций
Поисковый запрос не может быть пустым
- %1$s подписок
- %1$s подписчиков
+ подписаны на %1$s
+ подписчики %1$s
Пост %1$s
О приложении
- PixelDroid это свободное ПО с открытым исходным кодом, выпускаемое под лицензией GNU General Public License (версии 3 и выше)
+ PixelDroid - это свободное программное обеспечение с открытым исходным кодом, лицензированное в соответствии с GNU General Public License (версии 3 и выше)
Сайт проекта: https://pixeldroid.org
Зависимости и лицензии
О PixelDroid
Отписаться
Добавить фото
Опрос %1$s завершён
- Отменить вход
- OK, продолжить всё равно
- Это не похоже на инстанс Pixelfed, приложение может работать с ошибками.
+ Отмена входа в систему
+ Ладно, продолжить всё равно
+ Похоже, это не экземпляр Pixelfed, поэтому приложение может сломаться неожиданным образом.
Сохранить описание изображения
Одно из изображений в посте
- Невозможно получить информацию о пользователе
+ Не удалось получить информацию о пользователе
- Описание должно содержать максимум %d символ.
- Описание должно содержать максимум %d символа.
- Описание должно содержать максимум %d символов.
- Описание должно содержать максимум %d символов.
- Изображение красной панды, талисман Pixelfed , использующей телефон
- Сообщайте о проблемах или вносите свой вклад в приложение:
+ Это изображение красной панды, маскота Pixelfed пользующегося телефоном
+ Сообщите о проблемах или внесите свой вклад в работу над приложением:
Помогите перевести PixelDroid на ваш язык:
Язык
Удалить этот пост\?
@@ -155,8 +154,8 @@
- %d
\nПостов
- Этот пост в альбоме
- Предложить комментарий
+ Этот пост представляет собой альбом
+ Отправить комментарий
Добавить комментарий
- %d комментарий
@@ -165,37 +164,37 @@
- %d комментариев
- - %d Репост
- - %d Репоста
- - %d Репостов
- - %d Репостов
+ - %d Поделился(-ась)
+ - %d Поделились
+ - %d Поделились
+ - %d Поделились
- - %d Лайк
- - %d Лайка
- - %d Лайков
- - %d Лайков
+ - %d Понравилось
+ - %d Понравилось
+ - %d Понравилось
+ - %d Понравилось
- Добавьте описание медиа файла здесь…
+ Добавьте описание медиа здесь…
Показывать в режиме «карусель»
- Вас может смутить текстовое поле, запрашивающее доменное имя вашего \'инстанса\'.
+ Вас может смутить текстовое поле запрашивающее доменное имя вашего \"экземпляра\".
\n
-\nPixelfed это федеративная платформа и часть \"федиверса\", что означает, что она может общаться с другими платформами, говорящими на том же языке, как например Mastodon (см. https://joinmastodon.org).
+\nPixelfed - это федеративная платформа и часть \"федеративной вселенной (fediverse)\", что означает, что она может общаться с другими платформами говорящими на одном языке, как например Mastodon (см. https://joinmastodon.org).
\n
-\nЭто также означает, что вы должны выбрать, какой сервер или \'инстанс\' Pixelfed использовать. Если вы еще ничего не знаете об этом, перейдите по ссылке: https://pixelfed.org/join
+\nЭто также означает что Вам придется выбрать какой сервер или \'экземпляр\' Pixelfed использовать. Если вы еще ничего не знаете об этом, перейдите по ссылке: https://pixelfed.org/join
\n
-\nДополнительную информации о Pixelfed вы можете посмотреть здесь: https://pixelfed.org
- Переключить в вид сетки
- Отменить запрос на подписку\?
- Подписаться на запрос
- Вы выбрали большее изображений, чем разрешено вашим сервером (%1$s). Изображения сверх установленного лимита игнорируются.
- На этом инстансе API не активирован. Свяжитесь с вашим администратором для его активации.
- Не удалось удалить пост, проверить подключение\?
- Ошибка при удалении поста %1$d
- Здесь ничего нет :(
+\nБолее подробную информацию о Pixelfed вы можете найти здесь: https://pixelfed.org.
+ Переключить на вид сеткой
+ Отменить запрос на подписку?
+ Запрос на подписку отправлен
+ Вы выбрали количество изображений превышающее позволенное вашим сервером (%1$s). Изображения сверх установленного лимита игнорируются.
+ API не активирован на этом экземпляре. Свяжитесь с вашим администратором для его активации.
+ Не удалось удалить пост, проверьте ваше подключение?
+ Не удалось удалить пост, ошибка %1$d
+ Здесь не на что смотреть :(
Не удалось открыть страницу редактирования
Код ошибки, возвращенный сервером: %1$d
- Размер изображения в альбоме превышает максимальный размер в %1$d разрешённый инстансом (%2$d Кбайт, тогда как лимит установлен в %3$d Кбайт). По всей вероятности вы не сможете загрузить его.
+ Размер изображения в альбоме превышает максимальный размер в %1$d допустимый экземпляром (%2$d Кбайт, однако ограничение установлено в %3$d Кбайт). Вы не сможете загрузить его.
#%1$s
Файл %1$s не найден
@@ -204,24 +203,113 @@
- %d новых уведомлений
- %d новых уведомлений
- Ваш сервер не поддерживает загрузку видео, возможно, вы не сможете загружать видео, включенные в этот пост
- %1$s прокомментировал ваш пост
+ Сервер, который вы используете, не поддерживает загрузку видео, поэтому вы не сможете загрузить видео, включенное в этот пост
+ %1$s прокомментировал(а) ваш пост
Уведомление от %1$s
Новые подписчики
Упоминания
Поделились
- Лайки
+ Понравилось
Комментарии
- Голосования
+ Опросы
Другое
%1$s, %2$s, %3$s и %4$d других
%1$s, %2$s, и %3$s
%1$s и %2$s
Этот пост является видео
Настройки уведомлений
- Управляйте тем, какие уведомления вы хотите получать
+ Отметьте какие уведомления вы хотите получать
Не удалось получить последние уведомления
- Разрешение на использование камеры не предоставлено, выдайте разрешение в настройках, если хотите чтобы PixelDroid использовал камеру
- Разрешение на хранилище не предоставлено, дайте разрешение в настройках, если вы хотите, чтобы PixelDroid показывал миниатюры
+ Разрешение на камеру не предоставлено, дайте разрешение в настройках если хотите чтобы PixelDroid использовал камеру
+ Разрешение на хранилище не предоставлено, дайте разрешение в настройках если хотите чтобы PixelDroid показывал миниатюры
Воспроизвести видео
+ Всегда показывать чувствительное содержимое
+ Заполните описание новых постов следующим образом
+ Сообщение удалено из коллекции
+ Не удалось открыть страницу создания коллекции
+ Предпросмотр изображения в уведомлении об этом посте
+ Просмотр популярных постов за день
+ Что-то пошло не так. Нажмите, чтобы повторить попытку
+ Одно или несколько видео все еще кодируются. Дождитесь их завершения перед загрузкой на сервер
+ Коллекции
+ Популярные учетные записи
+ Вы уверены, что хотите удалить эту коллекцию?
+ Пост добавлен в коллекцию
+ Когда ваш аккаунт становится частным, ваши фотографии и видео на pixelfed смогут видеть только те, кого вы одобрили. На ваших существующих подписчиков это никак не повлияет.
+ Посты NSFW/CW по умолчанию не будут показываться в размытом виде.
+
+ - %d ответить
+ - %d ответить
+ - %d ответить
+ - %d ответить
+
+ Ошибка кодирования
+ Успешное кодирование!
+ Закодировать %1$d%%
+
+ - %d элемент успешно загружен
+ - %d элемента успешно загружено
+ - %d элементов успешно загружены
+ - %d элементов успешно загружены
+
+ Главная
+ Обновления
+ Поиск
+ Создать
+ Общество
+ Цветовой акцент
+ Выберите цветовой акцент
+ Выберите этот цветовой акцент
+ Выбранный цветовой акцент
+ Следующий шаг
+ Добавьте подробностей
+ Неизвестная ошибка, проверьте работает ли сервер: %1$s
+ Коллекция %1$s
+ Закладка
+ Удалить из закладок
+ Переделать
+ Правка этого поста позволит вам скорректировать фотографию и ее описание, но при этом будут удалены все текущие комментарии и лайки. Продолжить?
+ Если вы отмените эту редакцию, первоначальное сообщение больше не будет храниться на вашем аккаунте. Продолжить без повторного публикования?
+ Не удалось переделать пост, ошибка %1$d
+ Не удалось переделать пост, проверьте ваше соединение?
+ Не удалось (снять) закладку с поста, ошибка %1$d
+ Не удалось (снять) закладку, проверьте ваше соединение?
+ Анализ стабилизации %1$d%%
+ Создать новый пост
+ Предварительный просмотр поста
+ Новый пост
+ %1$s запрашивает подписаться на Вас
+ %1$s создал пост
+ Не удалось загрузить профиль
+ от%1$s
+ Ошибка при добавлении изображений
+ Заготовка описания
+ Исследуйте популярные учетные записи на этом экземпляре
+ Исследуйте популярные хэштеги на этом экземпляре
+ Популярные ключевые слова
+ Популярные посты
+ Посмотрите случайные посты за этот день
+ Вид сеткой
+ Вид лентой
+ Закладки
+ Удалить коллекцию
+ Добавить пост
+ Удалить пост
+ Выберите пост для добавления
+ Выберите пост для удаления
+ Не удалось добавить пост в коллекцию
+ Не удалось удалить пост из коллекции
+ Сохранить
+ Используйте динамические цвета из вашей системы
+ Дополнительные настройки профиля
+ Частная учетная запись
+ О себе
+ Ваше имя
+ Вы не сохранили изменения. Выйти?
+ Получаю ваш профиль…
+ Сохранение профиля
+ Изменения сохранены!
+ Измените фотографию профиля
+ Содержит материалы NSFW
+ Сменить аккаунт
\ No newline at end of file
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index b2d31285..1f24c51d 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -64,7 +64,6 @@
Kommentar: %1$s inlagd!
Kommentarsfel!
Dela bild
- Du måste tillåta skrivrättigheter för att dela bilder!
Du måste tillåta skrivrättigheter för att ladda ned bilder!
Kommentaren får inte vara tom!
Inlagt på %1$s
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index e3afb58a..3694f106 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -165,7 +165,6 @@
Поширити зображення
%1$s хвалить ваш допис
Файлу %1$s не знайдено
- Для поширення зображень потрібен дозвіл на запис!
Помилка вивантаження: хибний формат зображення.
Відписатись
Не вдалося вивантажити медіа, повторіть спробу чи перевірте мережне з\'єднання
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index a3da612e..66815d5f 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -64,7 +64,6 @@
评论: %1$s 已发布!
评论错误!
分享图像
- 您需要允许读写权限才能共享图片!
您需要允许读写权限才能下载图片!
评论不能为空!
发表于 %1$s
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 34455d44..ccc257f8 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -7,247 +7,4 @@
#FFFFFF
#b3b3b3
-
-
- #f4a261
- #924C00
- #FFFFFF
- #FFDCC4
- #2F1400
- #745945
- #FFFFFF
- #FFDCC4
- #2A1707
- #5D6136
- #FFFFFF
- #E3E7AF
- #1A1D00
- #BA1A1A
- #FFDAD6
- #FFFFFF
- #410002
- #FFFBFF
- #201A17
- #FFFBFF
- #201A17
- #F3DFD2
- #52443B
- #84746A
- #FBEEE8
- #362F2B
- #FFB780
- #000000
- #924C00
- #924C00
-
- #FFB780
- #4E2600
- #6F3800
- #FFDCC4
- #E4BFA7
- #422B1A
- #5B412F
- #FFDCC4
- #C6CA95
- #2F330C
- #464A20
- #E3E7AF
- #FFB4AB
- #93000A
- #690005
- #FFDAD6
- #201A17
- #ECE0DA
- #201A17
- #ECE0DA
- #52443B
- #D6C3B7
- #9F8D82
- #201A17
- #ECE0DA
- #924C00
- #000000
- #FFB780
- #FFB780
-
-
- #4285F4
- #005AC1
- #FFFFFF
- #D8E2FF
- #001A41
- #535E78
- #FFFFFF
- #D8E2FF
- #0F1B32
- #76517B
- #FFFFFF
- #FED6FF
- #2D0E34
- #BA1A1A
- #FFFFFF
- #FFDAD6
- #410002
- #FEFBFF
- #1B1B1F
- #FEFBFF
- #1B1B1F
- #E1E2EC
- #44474F
- #74777F
- #000000
- #303033
- #F2F0F4
- #ADC6FF
- #005AC1
- #005AC1
- #ADC6FF
- #002E69
- #004494
- #D8E2FF
- #BBC6E4
- #253048
- #3B475F
- #D8E2FF
- #E5B8E8
- #44244A
- #5D3A62
- #FED6FF
- #FFB4AB
- #690005
- #93000A
- #FFB4AB
- #1B1B1F
- #E3E2E6
- #1B1B1F
- #E3E2E6
- #44474F
- #C4C6D0
- #8E9099
- #000000
- #E3E2E6
- #303033
- #005AC1
- #ADC6FF
- #ADC6FF
-
- #86d89e
- #006D3A
- #FFFFFF
- #99F6B5
- #00210E
- #4F6353
- #FFFFFF
- #D2E8D4
- #0D1F13
- #3A646F
- #FFFFFF
- #BEEAF6
- #001F26
- #BA1A1A
- #FFDAD6
- #FFFFFF
- #410002
- #FBFDF8
- #191C19
- #FBFDF8
- #191C19
- #DDE5DB
- #414941
- #717971
- #F0F1EC
- #2E312E
- #7DDA9A
- #000000
- #006D3A
- #006D3A
- #7DDA9A
- #00391C
- #00522B
- #99F6B5
- #B6CCB8
- #223527
- #384B3C
- #D2E8D4
- #A2CEDA
- #02363F
- #214C57
- #BEEAF6
- #FFB4AB
- #93000A
- #690005
- #FFDAD6
- #191C19
- #E1E3DE
- #191C19
- #E1E3DE
- #414941
- #C1C9BF
- #8B938A
- #191C19
- #E1E3DE
- #006D3A
- #000000
- #7DDA9A
- #7DDA9A
-
- #984061
- #984061
- #FFFFFF
- #FFD9E2
- #3E001D
- #74565F
- #FFFFFF
- #FFD9E2
- #2B151C
- #7C5635
- #FFFFFF
- #FFDCC2
- #2E1500
- #BA1A1A
- #FFDAD6
- #FFFFFF
- #410002
- #FFFBFF
- #201A1B
- #FFFBFF
- #201A1B
- #F2DDE2
- #514347
- #837377
- #FAEEEF
- #352F30
- #FFB0C8
- #000000
- #984061
- #984061
- #FFB0C8
- #5E1133
- #7B2949
- #FFD9E2
- #E2BDC6
- #422931
- #5A3F47
- #FFD9E2
- #EFBD94
- #48290C
- #623F20
- #FFDCC2
- #FFB4AB
- #93000A
- #690005
- #FFDAD6
- #201A1B
- #EBE0E1
- #201A1B
- #EBE0E1
- #514347
- #D5C2C6
- #9E8C90
- #201A1B
- #EBE0E1
- #984061
- #000000
- #FFB0C8
- #FFB0C8
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
deleted file mode 100644
index d77d66cc..00000000
--- a/app/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
- 16dp
- 32dp
- 48dp
- 64dp
- 92dp
-
- 4dp
- 8dp
- 16dp
- 24dp
-
- 4dp
- 8dp
- 12dp
-
- 32dp
- 64dp
- 92dp
-
- 12dp
- 80dp
- 24dp
- 80dp
-
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2d56dece..b2eb5ec2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -150,7 +150,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"
No comments on this post…
Comment must not be empty!
You need to grant write permission to download pictures!
- You need to grant write permission to share pictures!
Share Image
Comment error!
"Comment: %1$s posted!"
@@ -330,4 +329,20 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"
Contains NSFW media
Switch accounts
NSFW/CW posts will not be blurred, and will be shown by default.
+ Story image
+ Reply to %1$s
+ Something went wrong sending reply
+ Something went wrong fetching the carousel
+ Sent reply
+ Add Story
+ Error: could not mark story as seen
+ Start or pause the stories
+ My story
+
+ Story
+
+ Post
+ Continue
+ Pictures after the first were removed but can be restored by switching back to creating a Post
+ Story Duration
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 56ca870c..c5030799 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,223 +1,16 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
deleted file mode 100644
index f2f9704d..00000000
--- a/app/src/main/res/xml/locales_config.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index c7df4cde..8b248f33 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -1,4 +1,5 @@
+
@@ -48,7 +49,22 @@
android:summary="@string/about_pixeldroid"
app:icon="@drawable/info_black_24dp">
+ android:targetClass="org.pixeldroid.common.AboutActivity">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
index e3881e5c..5c925d76 100644
--- a/app/src/main/res/xml/shortcuts.xml
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -8,7 +8,7 @@
+ android:targetClass="org.pixeldroid.app.postCreation.camera.CameraActivity" />
diff --git a/build.gradle b/build.gradle
index e19143ad..7eba61e2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,13 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
buildscript {
- ext.kotlin_version = '1.7.20'
+ ext.kotlin_version = '1.9.20'
repositories {
google()
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.0.2'
+ classpath 'com.android.tools.build:gradle:8.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
@@ -15,11 +14,16 @@ buildscript {
}
}
+plugins {
+ id 'com.google.devtools.ksp' version '1.9.20-1.0.14' apply false
+}
+
allprojects {
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
+ //noinspection JcenterRepositoryObsolete
jcenter {
content {
// info.androidhive:imagefilters is only available in JCenter
@@ -31,6 +35,6 @@ allprojects {
}
}
-task clean(type: Delete) {
+tasks.register('clean', Delete) {
delete rootProject.buildDir
}
diff --git a/error_mascots/Red_Panda_Emotions1_vectorize1_client1.svg b/error_mascots/Red_Panda_Emotions1_vectorize1_client1.svg
new file mode 100644
index 00000000..13319543
--- /dev/null
+++ b/error_mascots/Red_Panda_Emotions1_vectorize1_client1.svg
@@ -0,0 +1,1411 @@
+
+
+
+
diff --git a/error_mascots/Red_Panda_Emotions2_vectorize1_client1.svg b/error_mascots/Red_Panda_Emotions2_vectorize1_client1.svg
new file mode 100644
index 00000000..a970c470
--- /dev/null
+++ b/error_mascots/Red_Panda_Emotions2_vectorize1_client1.svg
@@ -0,0 +1,1115 @@
+
+
+
+
diff --git a/error_mascots/Red_Panda_Emotions2_vectorize1_client1_resized.svg b/error_mascots/Red_Panda_Emotions2_vectorize1_client1_resized.svg
new file mode 100644
index 00000000..e7b52db7
--- /dev/null
+++ b/error_mascots/Red_Panda_Emotions2_vectorize1_client1_resized.svg
@@ -0,0 +1,363 @@
+
+
+
+
diff --git a/error_mascots/Red_Panda_version1_vectorize3_client1.svg b/error_mascots/Red_Panda_version1_vectorize3_client1.svg
new file mode 100644
index 00000000..aa6c551b
--- /dev/null
+++ b/error_mascots/Red_Panda_version1_vectorize3_client1.svg
@@ -0,0 +1,2159 @@
+
+
+
+
diff --git a/error_mascots/Red_Panda_version2_emotion1_client1.svg b/error_mascots/Red_Panda_version2_emotion1_client1.svg
new file mode 100644
index 00000000..6601f558
--- /dev/null
+++ b/error_mascots/Red_Panda_version2_emotion1_client1.svg
@@ -0,0 +1,1318 @@
+
+
+
+
diff --git a/error_mascots/Red_Panda_version2_emotion3_client1.svg b/error_mascots/Red_Panda_version2_emotion3_client1.svg
new file mode 100644
index 00000000..353864df
--- /dev/null
+++ b/error_mascots/Red_Panda_version2_emotion3_client1.svg
@@ -0,0 +1,1312 @@
+
+
+
+
diff --git a/error_mascots/Red_Panda_version2_emotion4_client1.svg b/error_mascots/Red_Panda_version2_emotion4_client1.svg
new file mode 100644
index 00000000..4b1db460
--- /dev/null
+++ b/error_mascots/Red_Panda_version2_emotion4_client1.svg
@@ -0,0 +1,872 @@
+
+
+
+
diff --git a/error_mascots/Red_Panda_version2_emotion5_sad_client1.svg b/error_mascots/Red_Panda_version2_emotion5_sad_client1.svg
new file mode 100644
index 00000000..eca61594
--- /dev/null
+++ b/error_mascots/Red_Panda_version2_emotion5_sad_client1.svg
@@ -0,0 +1,1767 @@
+
+
+
+
diff --git a/fastlane/metadata/android/en-US/changelogs/25.txt b/fastlane/metadata/android/en-US/changelogs/25.txt
new file mode 100644
index 00000000..c57f3e38
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/25.txt
@@ -0,0 +1,4 @@
+* Stories support!
+* Dependencies updates
+* Hardware accelerated video encoding
+* Lots of behind-the-scenes work :)
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/26.txt b/fastlane/metadata/android/en-US/changelogs/26.txt
new file mode 100644
index 00000000..35557b33
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/26.txt
@@ -0,0 +1 @@
+Bug fixes & improvements ;)
\ No newline at end of file
diff --git a/fastlane/metadata/android/ru/changelogs/12.txt b/fastlane/metadata/android/ru/changelogs/12.txt
new file mode 100644
index 00000000..6cc9c55e
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/12.txt
@@ -0,0 +1,3 @@
+* Редактирование видео! Отключайте звук, обрезайте видео
+* Открывайте изображения в полноэкранном режиме, масштабируйте и панорамируйте их :)
+* Обновления перевода
diff --git a/fastlane/metadata/android/ru/changelogs/14.txt b/fastlane/metadata/android/ru/changelogs/14.txt
new file mode 100644
index 00000000..d31791c3
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/14.txt
@@ -0,0 +1 @@
+Исправление для сбоев при правке, которые происходили только в режиме релиза
diff --git a/fastlane/metadata/android/ru/changelogs/15.txt b/fastlane/metadata/android/ru/changelogs/15.txt
new file mode 100644
index 00000000..bbf5b128
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/15.txt
@@ -0,0 +1 @@
+Добавили в приложение перевод на венгерский (мадьярский) язык. Спасибо Балажу :)
diff --git a/fastlane/metadata/android/ru/changelogs/16.txt b/fastlane/metadata/android/ru/changelogs/16.txt
new file mode 100644
index 00000000..e13657db
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/16.txt
@@ -0,0 +1,4 @@
+* Добавили proguard правила gson для устранения сбоев на экземплярах Mastodon.
+* Добавление цветовой тематики с 4 различными темами
+* Переход на Material 3
+* Улучшена согласованность пользовательского интерфейса
diff --git a/fastlane/metadata/android/ru/changelogs/17.txt b/fastlane/metadata/android/ru/changelogs/17.txt
new file mode 100644
index 00000000..c8f51f94
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/17.txt
@@ -0,0 +1,8 @@
+* Улучшения в комментариях: Теперь вы можете открыть комментарий, чтобы увидеть ответы, поставить лайк, в комментариях отображается аватар автора и т. д.
+* Обновления перевода. Спасибо переводчикам :). Помогите перевести PixelDroid на ваш язык на weblate.pixeldroid.org
+* Безопасность: проверка зависимостей гарантирует, что зависимости, включенные в приложение, не были подделаны, PixelDroid теперь будет отказываться от любого не-HTTPS соединения
+* PixelDroid теперь использует пользовательский агент "PixelDroid" вместо пользовательского агента библиотеки OkHttp.
+* Все жестко закодированные строки были удалены из приложения, теперь все можно перевести.
+* Некоторые улучшения в коде
+* Исправлено сохранение изображений из библиотеки или общего доступа к приложению
+* Исправлен еще один сбой при использовании экземпляров Mastodon
diff --git a/fastlane/metadata/android/ru/changelogs/18.txt b/fastlane/metadata/android/ru/changelogs/18.txt
new file mode 100644
index 00000000..29a20b29
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/18.txt
@@ -0,0 +1,11 @@
+* Добавили пользовательскую графику ошибок красной панды
+
+* Разрешили произвольную обрезку при правке изображений
+
+* Улучшили метаданные F-Droid
+
+* Обновления перевода
+
+* Обновление зависимостей
+
+* Исправление ошибок
diff --git a/fastlane/metadata/android/ru/changelogs/19.txt b/fastlane/metadata/android/ru/changelogs/19.txt
new file mode 100644
index 00000000..54f42de3
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/19.txt
@@ -0,0 +1,9 @@
+* Удаление метаданных фотографий перед загрузкой
+* Закладки!
+* Просмотр профиля в виде ленты или сетки
+* Установите шаблон для ваших описаний
+* Значок на значке уведомлений, если вы получили новые уведомления
+* Больше функций редактирования видео: обрезка, изменение скорости, добавление стабилизации
+* Реализация динамических цветов: вы можете заставить PixelDroid следовать цвету вашего фона (Android 12 и выше)
+* Исправление ошибок
+* Обновление переводов
diff --git a/fastlane/metadata/android/ru/changelogs/2.txt b/fastlane/metadata/android/ru/changelogs/2.txt
new file mode 100644
index 00000000..0a6c7cfc
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/2.txt
@@ -0,0 +1,7 @@
+* Обновления перевода: новые строки и обновления других строк. Спасибо переводчикам ❤️! Заходите на наш сайт, если хотите помочь с переводом на ваш язык.
+
+* #️⃣ Поддержка хэштегов. Теперь вы можете просматривать хэштеги, а не просто показывать сообщение с тостом ��.
+
+* Нажмите на вкладку, чтобы прокрутить страницу к началу. Больше нет необходимости в бешеной прокрутке, чтобы вернуться к началу. Просто нажмите на вкладку, в которой вы находитесь, и она прокрутится вверх :)
+
+* Попытка исправить некоторые ошибки, из-за которых приложение падало. Большое спасибо всем за сообщения о сбоях! ❤️
diff --git a/fastlane/metadata/android/ru/changelogs/20.txt b/fastlane/metadata/android/ru/changelogs/20.txt
new file mode 100644
index 00000000..ef76d8fd
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/20.txt
@@ -0,0 +1,4 @@
+- Удаление и редактирование существующих сообщений
+- Коллекции собственных постов теперь можно просматривать и редактировать
+- Создание постов теперь происходит в два этапа, с новой поддержкой: NSFW-чувствительность, переключение аккаунтов
+- Много других изменений и улучшений :)
diff --git a/fastlane/metadata/android/ru/changelogs/23.txt b/fastlane/metadata/android/ru/changelogs/23.txt
new file mode 100644
index 00000000..9fb12156
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/23.txt
@@ -0,0 +1,4 @@
+* Менее агрессивные предупреждения при отключении камеры или прав доступа к файлам
+* Обновление переводов
+* Исправление ошибок при загрузке видео
+* Улучшена обработка ошибок
diff --git a/fastlane/metadata/android/ru/changelogs/3.txt b/fastlane/metadata/android/ru/changelogs/3.txt
new file mode 100644
index 00000000..da7dbd4f
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/3.txt
@@ -0,0 +1,7 @@
+* Добавить язык (малаялам)
+
+* Исправление некоторых ошибок в ответах API
+
+* Обновление зависимостей
+
+* Обновить переводы
diff --git a/fastlane/metadata/android/ru/changelogs/4.txt b/fastlane/metadata/android/ru/changelogs/4.txt
new file mode 100644
index 00000000..113ef2b2
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/4.txt
@@ -0,0 +1,9 @@
+- Поддержка уведомлений! Все еще немного рудиментарная, скоро будет доработка :)
+- Исправление проблемы, из-за которой игнорировался поворот EXIF, в результате чего фотографии отображались повернутыми не в ту сторону
+- Исправление #300
+- Исправление ошибки, из-за которой браузеры с веб-просмотром выдавали ошибку при входе, поскольку URL содержал пробелы, которые не были закодированы в URI
+- Исправление проблемы, из-за которой кэш сбрасывался, а лента новостей оставалась пустой при каждом запуске приложения, что приводило к проблемам с производительностью
+- Добавить чешский язык, обновили другие переводы
+- Исправлены фотографии профиля
+- Исправление неработающей камеры после переключения вкладок
+- Обновление зависимостей
diff --git a/fastlane/metadata/android/ru/changelogs/5.txt b/fastlane/metadata/android/ru/changelogs/5.txt
new file mode 100644
index 00000000..0d7661cb
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/5.txt
@@ -0,0 +1,5 @@
+* Обновления перевода
+* Более легкий способ просмотра информации о лицензии
+* Исправление проблем с разрешениями на вкладке камеры
+* Исправление разбора ссылок
+* Исправление просмотра открытий (может потребоваться обновление экземпляра)
diff --git a/fastlane/metadata/android/ru/changelogs/7.txt b/fastlane/metadata/android/ru/changelogs/7.txt
new file mode 100644
index 00000000..efcb3ff3
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/7.txt
@@ -0,0 +1,2 @@
+* Обновление переводов
+* Разрешите удаленные изображения при загрузке (например, Nextcloud)
diff --git a/fastlane/metadata/android/ru/changelogs/8.txt b/fastlane/metadata/android/ru/changelogs/8.txt
new file mode 100644
index 00000000..8b1503e1
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/8.txt
@@ -0,0 +1,4 @@
+* Добавили поддержку воспроизведения и загрузки видео.
+* Улучшение уведомлений
+* Обновление переводов
+* Обновление зависимостей
diff --git a/fastlane/metadata/android/ru/changelogs/9.txt b/fastlane/metadata/android/ru/changelogs/9.txt
new file mode 100644
index 00000000..094e5bc3
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/9.txt
@@ -0,0 +1,3 @@
+* Устранение сбоя при отображении списка лицензий
+* Значительно улучшена производительность при работе с профилем
+* Обновление зависимостей
diff --git a/fastlane/metadata/android/ru/full_description.txt b/fastlane/metadata/android/ru/full_description.txt
index 394d6250..00b9c64b 100644
--- a/fastlane/metadata/android/ru/full_description.txt
+++ b/fastlane/metadata/android/ru/full_description.txt
@@ -1,6 +1,6 @@
-PixelDroid это свободный клиент Pixelfed с открытым исходным кодом для Android.
+PixelDroid - это свободный Android-клиент с открытым исходным кодом для Pixelfed, федеративной платформы для обмена изображениями.
-Просматривайте летны и профили, загружайте новые посты, находите посты, взаимодействуйте с другими пользователями федиверса.
+Просматривайте ленты и профили, загружайте новые посты, находите посты, взаимодействуйте с другими пользователями федиверса.
• Поддержка мульти-аккаунта
• Светлая и тёмная темы
diff --git a/gradle.properties b/gradle.properties
index 560b153a..acce24c4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -25,5 +25,4 @@ org.gradle.parallel=true
kapt.incremental.apt=true
android.enableR8.fullMode=true
android.nonTransitiveRClass=false
-android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 570637ad..8daa1609 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
#Fri Oct 14 13:37:44 GMT 2022
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=1b6b558be93f29438d3df94b7dfee02e794b94d9aca4611a92cdb79b6b88e909
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip
+distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/pixel_common b/pixel_common
new file mode 160000
index 00000000..a390dc06
--- /dev/null
+++ b/pixel_common
@@ -0,0 +1 @@
+Subproject commit a390dc0685eba3dfe18f57fd878f15fda52cc95b
diff --git a/scrambler b/scrambler
index 5b7008b2..7c67b911 160000
--- a/scrambler
+++ b/scrambler
@@ -1 +1 @@
-Subproject commit 5b7008b218399fa3ae2aec80ddab1a840241c3b2
+Subproject commit 7c67b911930b4344a2917f2944493e08fdd04b57
diff --git a/settings.gradle b/settings.gradle
index 706f76d9..389084f8 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,5 @@
rootProject.name='PixelDroid'
include ':app'
include ':scrambler'
-project(':scrambler').projectDir = new File(rootDir, 'scrambler/scrambler/')
\ No newline at end of file
+project(':scrambler').projectDir = new File(rootDir, 'scrambler/scrambler/')
+include ':pixel_common'