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 @@ - - - - - - - - - - - - - - - - - - - - - - - -