feat: Update activity transitions, prepare for predictive-back (#650)
Previous code used a single animation type (slide) when transitioning, the transition was quite slow, and didn't behave appropriately if the device was set to a RTL writing system. In addition, handling the back affordance didn't work well with the new "Predictive Back" feature in Android 14 (https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture). Fix this. ## Transitions To update the transitions the `startActivityWithSlideInAnimation()` implementation (and associated `finishWithoutSlideOutAnimation()`) have been replaced. There are three transitions; `default`, `slide`, and `explode`, represented as an enum passed to the activity intent. The `default` transition is the activity transition from Android 14, from the Android open source project (https://cs.android.com/android/platform/superproject/+/android-14.0.0_r18:frameworks/base/core/res/res/anim/;bpv=1). This is used for most transitions. The `slide` transition is the pre-existing slide transition, with a shorter transition time so it feels more responsive, and an RTL implementation. This is used when there is a strong spatial component to the navigation, for example, when going from: - a status to its thread - a preference menu item to its subscreen - a filter in a list to the "edit filter" screen - viewing your profile to editing your profile The `explode` transition is used when the state of the app changes significantly, such as when switching accounts. Activities are now started with `startActivityWithTransition()` which sets the intent and prepares the transition. `BaseActivity` checks the intent for the transition type and makes further changes to the transition as necessary. ## Predictive back "Predictive back" needs to know what the back button would do before the user interacts with it with an `onBackPressedCallback` that is either enabled or disabled. This required refactoring some code (particularly in `ComposeActivity`) to gather data ahead of time and enable/disable the callback appropriately. ## Fixed bugs - Back button wasn't stepping back through the tabs in AccountActivity - Modifying a filter and pressing back without saving wasn't prompting the user to save the changes - Writing a content warning and then hiding it would still count the text of the content warning toward's the post's length ## Other cleanups - Use `ViewCompat.setTransitionName()` instead of setting the `transitionName` property - Delete the unused `fade_in` and `fade_out` animations. - Use androidx-activity 1.9.0 to get the latest predictive back support library code - Show validation errors when creating / editing filters
This commit is contained in:
parent
2b3cbb6465
commit
93e6b38d43
|
@ -54,7 +54,7 @@
|
||||||
<issue
|
<issue
|
||||||
id="UnusedAttribute"
|
id="UnusedAttribute"
|
||||||
message="Attribute `networkSecurityConfig` is only used in API level 24 and higher (current min is 23)"
|
message="Attribute `networkSecurityConfig` is only used in API level 24 and higher (current min is 23)"
|
||||||
errorLine1=" android:networkSecurityConfig="@xml/network_security_config">"
|
errorLine1=" android:networkSecurityConfig="@xml/network_security_config""
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
|
@ -62,6 +62,17 @@
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="UnusedAttribute"
|
||||||
|
message="Attribute `enableOnBackInvokedCallback` is only used in API level 33 and higher (current min is 23)"
|
||||||
|
errorLine1=" android:enableOnBackInvokedCallback="true">"
|
||||||
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/AndroidManifest.xml"
|
||||||
|
line="26"
|
||||||
|
column="9"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="SelectedPhotoAccess"
|
id="SelectedPhotoAccess"
|
||||||
message="Your app is currently not handling Selected Photos Access introduced in Android 14+"
|
message="Your app is currently not handling Selected Photos Access introduced in Android 14+"
|
||||||
|
@ -883,7 +894,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/app/pachli/components/account/AccountActivity.kt"
|
file="src/main/java/app/pachli/components/account/AccountActivity.kt"
|
||||||
line="493"
|
line="515"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -905,7 +916,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt"
|
file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt"
|
||||||
line="44"
|
line="49"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -916,7 +927,7 @@
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt"
|
file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt"
|
||||||
line="50"
|
line="55"
|
||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,8 @@
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="false"
|
android:usesCleartextTraffic="false"
|
||||||
android:localeConfig="@xml/locales_config"
|
android:localeConfig="@xml/locales_config"
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:enableOnBackInvokedCallback="true">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".SplashActivity"
|
android:name=".SplashActivity"
|
||||||
|
|
|
@ -30,6 +30,7 @@ import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
@ -59,6 +60,7 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@ -75,7 +77,9 @@ class EditProfileActivity : BaseActivity() {
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
|
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
|
||||||
|
|
||||||
private val accountFieldEditAdapter = AccountFieldEditAdapter()
|
private val accountFieldEditAdapter: AccountFieldEditAdapter = AccountFieldEditAdapter {
|
||||||
|
viewModel.onChange(currentProfileData)
|
||||||
|
}
|
||||||
|
|
||||||
private var maxAccountFields = InstanceInfoRepository.DEFAULT_MAX_ACCOUNT_FIELDS
|
private var maxAccountFields = InstanceInfoRepository.DEFAULT_MAX_ACCOUNT_FIELDS
|
||||||
|
|
||||||
|
@ -108,6 +112,12 @@ class EditProfileActivity : BaseActivity() {
|
||||||
fields = accountFieldEditAdapter.getFieldData(),
|
fields = accountFieldEditAdapter.getFieldData(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
showUnsavedChangesDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -216,19 +226,19 @@ class EditProfileActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val onBackCallback = object : OnBackPressedCallback(enabled = true) {
|
lifecycleScope.launch {
|
||||||
override fun handleOnBackPressed() = checkForUnsavedChanges()
|
viewModel.isDirty.collectLatest { onBackPressedCallback.isEnabled = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(this, onBackCallback)
|
binding.displayNameEditText.doAfterTextChanged {
|
||||||
}
|
viewModel.onChange(currentProfileData)
|
||||||
|
|
||||||
fun checkForUnsavedChanges() {
|
|
||||||
if (viewModel.hasUnsavedChanges(currentProfileData)) {
|
|
||||||
showUnsavedChangesDialog()
|
|
||||||
} else {
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.lockedCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||||
|
viewModel.onChange(currentProfileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
|
|
@ -53,6 +53,7 @@ import androidx.core.view.GravityCompat
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewpager2.widget.MarginPageTransformer
|
import androidx.viewpager2.widget.MarginPageTransformer
|
||||||
import app.pachli.appstore.AnnouncementReadEvent
|
import app.pachli.appstore.AnnouncementReadEvent
|
||||||
|
@ -70,6 +71,9 @@ import app.pachli.core.activity.AccountSelectionListener
|
||||||
import app.pachli.core.activity.BottomSheetActivity
|
import app.pachli.core.activity.BottomSheetActivity
|
||||||
import app.pachli.core.activity.PostLookupFallbackBehavior
|
import app.pachli.core.activity.PostLookupFallbackBehavior
|
||||||
import app.pachli.core.activity.emojify
|
import app.pachli.core.activity.emojify
|
||||||
|
import app.pachli.core.activity.extensions.TransitionKind
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithTransition
|
||||||
import app.pachli.core.common.di.ApplicationScope
|
import app.pachli.core.common.di.ApplicationScope
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
|
@ -215,6 +219,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
/** Adapter for the different timeline tabs */
|
/** Adapter for the different timeline tabs */
|
||||||
private lateinit var tabAdapter: MainPagerAdapter
|
private lateinit var tabAdapter: MainPagerAdapter
|
||||||
|
|
||||||
|
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
when {
|
||||||
|
binding.mainDrawerLayout.isOpen -> binding.mainDrawerLayout.close()
|
||||||
|
binding.viewPager.currentItem != 0 -> binding.viewPager.currentItem = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -284,7 +297,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
// otherwise show notification tab
|
// otherwise show notification tab
|
||||||
if (MainActivityIntent.getNotificationType(intent) == Notification.Type.FOLLOW_REQUEST) {
|
if (MainActivityIntent.getNotificationType(intent) == Notification.Type.FOLLOW_REQUEST) {
|
||||||
val intent = AccountListActivityIntent(this, AccountListActivityIntent.Kind.FOLLOW_REQUESTS)
|
val intent = AccountListActivityIntent(this, AccountListActivityIntent.Kind.FOLLOW_REQUESTS)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
} else {
|
} else {
|
||||||
showNotificationTab = true
|
showNotificationTab = true
|
||||||
}
|
}
|
||||||
|
@ -379,24 +392,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
|
|
||||||
selectedEmojiPack = sharedPreferencesRepository.getString(EMOJI_PREFERENCE, "")
|
selectedEmojiPack = sharedPreferencesRepository.getString(EMOJI_PREFERENCE, "")
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(
|
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||||
this,
|
|
||||||
object : OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
when {
|
|
||||||
binding.mainDrawerLayout.isOpen -> {
|
|
||||||
binding.mainDrawerLayout.close()
|
|
||||||
}
|
|
||||||
binding.viewPager.currentItem != 0 -> {
|
|
||||||
binding.viewPager.currentItem = 0
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= TIRAMISU && ActivityCompat.checkSelfPermission(this, POST_NOTIFICATIONS) != PERMISSION_GRANTED) {
|
if (Build.VERSION.SDK_INT >= TIRAMISU && ActivityCompat.checkSelfPermission(this, POST_NOTIFICATIONS) != PERMISSION_GRANTED) {
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(POST_NOTIFICATIONS), 1)
|
ActivityCompat.requestPermissions(this, arrayOf(POST_NOTIFICATIONS), 1)
|
||||||
|
@ -500,7 +496,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_SEARCH -> {
|
KeyEvent.KEYCODE_SEARCH -> {
|
||||||
startActivityWithSlideInAnimation(SearchActivityIntent(this))
|
startActivityWithDefaultTransition(SearchActivityIntent(this))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -603,6 +599,20 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
refreshMainDrawerItems(addSearchButton)
|
refreshMainDrawerItems(addSearchButton)
|
||||||
setSavedInstance(savedInstanceState)
|
setSavedInstance(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.mainDrawerLayout.addDrawerListener(object : DrawerListener {
|
||||||
|
override fun onDrawerSlide(drawerView: View, slideOffset: Float) { }
|
||||||
|
|
||||||
|
override fun onDrawerOpened(drawerView: View) {
|
||||||
|
onBackPressedCallback.isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrawerClosed(drawerView: View) {
|
||||||
|
onBackPressedCallback.isEnabled = binding.tabLayout.selectedTabPosition > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrawerStateChanged(newState: Int) { }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshMainDrawerItems(addSearchButton: Boolean) {
|
private fun refreshMainDrawerItems(addSearchButton: Boolean) {
|
||||||
|
@ -616,7 +626,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
nameText = list.title
|
nameText = list.title
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_list
|
iconicsIcon = GoogleMaterial.Icon.gmd_list
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(
|
startActivityWithDefaultTransition(
|
||||||
TimelineActivityIntent.list(this@MainActivity, list.id, list.title),
|
TimelineActivityIntent.list(this@MainActivity, list.id, list.title),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -635,7 +645,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
nameRes = R.string.title_notifications
|
nameRes = R.string.title_notifications
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_notifications
|
iconicsIcon = GoogleMaterial.Icon.gmd_notifications
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(
|
startActivityWithDefaultTransition(
|
||||||
TimelineActivityIntent.notifications(context),
|
TimelineActivityIntent.notifications(context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -644,7 +654,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
nameRes = R.string.title_public_local
|
nameRes = R.string.title_public_local
|
||||||
iconRes = R.drawable.ic_local_24dp
|
iconRes = R.drawable.ic_local_24dp
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(
|
startActivityWithDefaultTransition(
|
||||||
TimelineActivityIntent.publicLocal(context),
|
TimelineActivityIntent.publicLocal(context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -653,7 +663,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
nameRes = R.string.title_public_federated
|
nameRes = R.string.title_public_federated
|
||||||
iconRes = R.drawable.ic_public_24dp
|
iconRes = R.drawable.ic_public_24dp
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(
|
startActivityWithDefaultTransition(
|
||||||
TimelineActivityIntent.publicFederated(context),
|
TimelineActivityIntent.publicFederated(context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -662,7 +672,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
nameRes = R.string.title_direct_messages
|
nameRes = R.string.title_direct_messages
|
||||||
iconRes = R.drawable.ic_reblog_direct_24dp
|
iconRes = R.drawable.ic_reblog_direct_24dp
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(
|
startActivityWithDefaultTransition(
|
||||||
TimelineActivityIntent.conversations(context),
|
TimelineActivityIntent.conversations(context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -672,7 +682,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_bookmark
|
iconicsIcon = GoogleMaterial.Icon.gmd_bookmark
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = TimelineActivityIntent.bookmarks(context)
|
val intent = TimelineActivityIntent.bookmarks(context)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
|
@ -681,21 +691,21 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_star
|
iconicsIcon = GoogleMaterial.Icon.gmd_star
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = TimelineActivityIntent.favourites(context)
|
val intent = TimelineActivityIntent.favourites(context)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
nameRes = R.string.title_public_trending
|
nameRes = R.string.title_public_trending
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_trending_up
|
iconicsIcon = GoogleMaterial.Icon.gmd_trending_up
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(TrendingActivityIntent(context))
|
startActivityWithDefaultTransition(TrendingActivityIntent(context))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
nameRes = R.string.title_followed_hashtags
|
nameRes = R.string.title_followed_hashtags
|
||||||
iconRes = R.drawable.ic_hashtag
|
iconRes = R.drawable.ic_hashtag
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(FollowedTagsActivityIntent(context))
|
startActivityWithDefaultTransition(FollowedTagsActivityIntent(context))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
|
@ -703,7 +713,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_person_add
|
iconicsIcon = GoogleMaterial.Icon.gmd_person_add
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.FOLLOW_REQUESTS)
|
val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.FOLLOW_REQUESTS)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SectionDrawerItem().apply {
|
SectionDrawerItem().apply {
|
||||||
|
@ -714,7 +724,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
nameRes = R.string.manage_lists
|
nameRes = R.string.manage_lists
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(ListActivityIntent(context))
|
startActivityWithDefaultTransition(ListActivityIntent(context))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DividerDrawerItem(),
|
DividerDrawerItem(),
|
||||||
|
@ -723,14 +733,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
iconRes = R.drawable.ic_notebook
|
iconRes = R.drawable.ic_notebook
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = DraftsActivityIntent(context)
|
val intent = DraftsActivityIntent(context)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
nameRes = R.string.action_access_scheduled_posts
|
nameRes = R.string.action_access_scheduled_posts
|
||||||
iconRes = R.drawable.ic_access_time
|
iconRes = R.drawable.ic_access_time
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(ScheduledStatusActivityIntent(context))
|
startActivityWithDefaultTransition(ScheduledStatusActivityIntent(context))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
|
@ -738,7 +748,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
nameRes = R.string.title_announcements
|
nameRes = R.string.title_announcements
|
||||||
iconRes = R.drawable.ic_bullhorn_24dp
|
iconRes = R.drawable.ic_bullhorn_24dp
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(AnnouncementsActivityIntent(context))
|
startActivityWithDefaultTransition(AnnouncementsActivityIntent(context))
|
||||||
}
|
}
|
||||||
badgeStyle = BadgeStyle().apply {
|
badgeStyle = BadgeStyle().apply {
|
||||||
textColor = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, com.google.android.material.R.attr.colorOnPrimary))
|
textColor = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, com.google.android.material.R.attr.colorOnPrimary))
|
||||||
|
@ -751,7 +761,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
iconRes = R.drawable.ic_account_settings
|
iconRes = R.drawable.ic_account_settings
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = PreferencesActivityIntent(context, PreferenceScreen.ACCOUNT)
|
val intent = PreferencesActivityIntent(context, PreferenceScreen.ACCOUNT)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
secondaryDrawerItem {
|
secondaryDrawerItem {
|
||||||
|
@ -759,7 +769,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = PreferencesActivityIntent(context, PreferenceScreen.GENERAL)
|
val intent = PreferencesActivityIntent(context, PreferenceScreen.GENERAL)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
|
@ -767,7 +777,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_person
|
iconicsIcon = GoogleMaterial.Icon.gmd_person
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = EditProfileActivityIntent(context)
|
val intent = EditProfileActivityIntent(context)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
secondaryDrawerItem {
|
secondaryDrawerItem {
|
||||||
|
@ -775,7 +785,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_info
|
iconicsIcon = GoogleMaterial.Icon.gmd_info
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = AboutActivityIntent(context)
|
val intent = AboutActivityIntent(context)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
secondaryDrawerItem {
|
secondaryDrawerItem {
|
||||||
|
@ -792,7 +802,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
nameRes = R.string.action_search
|
nameRes = R.string.action_search
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_search
|
iconicsIcon = GoogleMaterial.Icon.gmd_search
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(SearchActivityIntent(context))
|
startActivityWithDefaultTransition(SearchActivityIntent(context))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -943,6 +953,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
|
|
||||||
onTabSelectedListener = object : OnTabSelectedListener {
|
onTabSelectedListener = object : OnTabSelectedListener {
|
||||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||||
|
onBackPressedCallback.isEnabled = tab.position > 0 || binding.mainDrawerLayout.isOpen
|
||||||
|
|
||||||
supportActionBar?.title = tabs[tab.position].title(this@MainActivity)
|
supportActionBar?.title = tabs[tab.position].title(this@MainActivity)
|
||||||
|
|
||||||
refreshComposeButtonState(tabs[tab.position])
|
refreshComposeButtonState(tabs[tab.position])
|
||||||
|
@ -952,9 +964,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
|
|
||||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||||
val fragment = tabAdapter.getFragment(tab.position)
|
val fragment = tabAdapter.getFragment(tab.position)
|
||||||
if (fragment is ReselectableFragment) {
|
(fragment as? ReselectableFragment)?.onReselect()
|
||||||
(fragment as ReselectableFragment).onReselect()
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshComposeButtonState(tabs[tab.position])
|
refreshComposeButtonState(tabs[tab.position])
|
||||||
}
|
}
|
||||||
|
@ -987,12 +997,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
// open profile when active image was clicked
|
// open profile when active image was clicked
|
||||||
if (current && activeAccount != null) {
|
if (current && activeAccount != null) {
|
||||||
val intent = AccountActivityIntent(this, activeAccount.accountId)
|
val intent = AccountActivityIntent(this, activeAccount.accountId)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// open LoginActivity to add new account
|
// open LoginActivity to add new account
|
||||||
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||||
startActivityWithSlideInAnimation(
|
startActivityWithDefaultTransition(
|
||||||
LoginActivityIntent(this, LoginMode.ADDITIONAL_LOGIN),
|
LoginActivityIntent(this, LoginMode.ADDITIONAL_LOGIN),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
@ -1012,12 +1022,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
intent.action = forward.action
|
intent.action = forward.action
|
||||||
intent.putExtras(forward)
|
intent.putExtras(forward)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivityWithTransition(intent, TransitionKind.EXPLODE)
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
overridePendingTransition(
|
|
||||||
DR.anim.explode,
|
|
||||||
DR.anim.explode,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logout() {
|
private fun logout() {
|
||||||
|
@ -1040,7 +1046,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
||||||
LoginActivityIntent(this@MainActivity, LoginMode.DEFAULT)
|
LoginActivityIntent(this@MainActivity, LoginMode.DEFAULT)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
|
|
@ -46,6 +46,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import app.pachli.BuildConfig.APPLICATION_ID
|
import app.pachli.BuildConfig.APPLICATION_ID
|
||||||
import app.pachli.core.activity.BaseActivity
|
import app.pachli.core.activity.BaseActivity
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
import app.pachli.core.common.extensions.viewBinding
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
|
@ -250,7 +251,7 @@ class ViewMediaActivity : BaseActivity(), MediaActionsListener {
|
||||||
|
|
||||||
private fun onOpenStatus() {
|
private fun onOpenStatus() {
|
||||||
val attach = attachmentViewData!![binding.viewPager.currentItem]
|
val attach = attachmentViewData!![binding.viewPager.currentItem]
|
||||||
startActivityWithSlideInAnimation(ViewThreadActivityIntent(this, attach.statusId, attach.statusUrl))
|
startActivityWithDefaultTransition(ViewThreadActivityIntent(this, attach.statusId, attach.statusUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyLink() {
|
private fun copyLink() {
|
||||||
|
|
|
@ -25,7 +25,12 @@ import app.pachli.core.ui.BindingHolder
|
||||||
import app.pachli.databinding.ItemEditFieldBinding
|
import app.pachli.databinding.ItemEditFieldBinding
|
||||||
import app.pachli.util.fixTextSelection
|
import app.pachli.util.fixTextSelection
|
||||||
|
|
||||||
class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() {
|
/**
|
||||||
|
* @property onChange Call this whenever data in the UI fields changes
|
||||||
|
*/
|
||||||
|
class AccountFieldEditAdapter(
|
||||||
|
val onChange: () -> Unit,
|
||||||
|
) : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() {
|
||||||
|
|
||||||
private val fieldData = mutableListOf<MutableStringPair>()
|
private val fieldData = mutableListOf<MutableStringPair>()
|
||||||
private var maxNameLength: Int? = null
|
private var maxNameLength: Int? = null
|
||||||
|
@ -84,10 +89,12 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
||||||
|
|
||||||
holder.binding.accountFieldNameText.doAfterTextChanged { newText ->
|
holder.binding.accountFieldNameText.doAfterTextChanged { newText ->
|
||||||
fieldData.getOrNull(holder.bindingAdapterPosition)?.first = newText.toString()
|
fieldData.getOrNull(holder.bindingAdapterPosition)?.first = newText.toString()
|
||||||
|
onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.binding.accountFieldValueText.doAfterTextChanged { newText ->
|
holder.binding.accountFieldValueText.doAfterTextChanged { newText ->
|
||||||
fieldData.getOrNull(holder.bindingAdapterPosition)?.second = newText.toString()
|
fieldData.getOrNull(holder.bindingAdapterPosition)?.second = newText.toString()
|
||||||
|
onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the textview contents are selectable
|
// Ensure the textview contents are selectable
|
||||||
|
|
|
@ -36,6 +36,7 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
@ -58,6 +59,9 @@ import app.pachli.R
|
||||||
import app.pachli.core.activity.AccountSelectionListener
|
import app.pachli.core.activity.AccountSelectionListener
|
||||||
import app.pachli.core.activity.BottomSheetActivity
|
import app.pachli.core.activity.BottomSheetActivity
|
||||||
import app.pachli.core.activity.emojify
|
import app.pachli.core.activity.emojify
|
||||||
|
import app.pachli.core.activity.extensions.TransitionKind
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithTransition
|
||||||
import app.pachli.core.activity.loadAvatar
|
import app.pachli.core.activity.loadAvatar
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
|
@ -180,6 +184,12 @@ class AccountActivity :
|
||||||
|
|
||||||
private var noteWatcher: TextWatcher? = null
|
private var noteWatcher: TextWatcher? = null
|
||||||
|
|
||||||
|
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
binding.accountFragmentViewPager.currentItem = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
loadResources()
|
loadResources()
|
||||||
|
@ -207,6 +217,8 @@ class AccountActivity :
|
||||||
} else {
|
} else {
|
||||||
binding.saveNoteInfo.visibility = View.INVISIBLE
|
binding.saveNoteInfo.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -243,7 +255,7 @@ class AccountActivity :
|
||||||
else -> throw AssertionError()
|
else -> throw AssertionError()
|
||||||
}
|
}
|
||||||
val accountListIntent = AccountListActivityIntent(this, kind, viewModel.accountId)
|
val accountListIntent = AccountListActivityIntent(this, kind, viewModel.accountId)
|
||||||
startActivityWithSlideInAnimation(accountListIntent)
|
startActivityWithDefaultTransition(accountListIntent)
|
||||||
}
|
}
|
||||||
binding.accountFollowers.setOnClickListener(accountListClickListener)
|
binding.accountFollowers.setOnClickListener(accountListClickListener)
|
||||||
binding.accountFollowing.setOnClickListener(accountListClickListener)
|
binding.accountFollowing.setOnClickListener(accountListClickListener)
|
||||||
|
@ -299,7 +311,10 @@ class AccountActivity :
|
||||||
|
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||||
|
|
||||||
override fun onTabSelected(tab: TabLayout.Tab?) {}
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
|
tab?.position ?: return
|
||||||
|
onBackPressedCallback.isEnabled = tab.position > 0
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -561,7 +576,7 @@ class AccountActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun viewImage(view: View, uri: String) {
|
private fun viewImage(view: View, uri: String) {
|
||||||
view.transitionName = uri
|
ViewCompat.setTransitionName(view, uri)
|
||||||
startActivity(
|
startActivity(
|
||||||
ViewMediaActivityIntent(view.context, uri),
|
ViewMediaActivityIntent(view.context, uri),
|
||||||
ActivityOptionsCompat.makeSceneTransitionAnimation(this, view, uri).toBundle(),
|
ActivityOptionsCompat.makeSceneTransitionAnimation(this, view, uri).toBundle(),
|
||||||
|
@ -630,7 +645,7 @@ class AccountActivity :
|
||||||
binding.accountFollowButton.setOnClickListener {
|
binding.accountFollowButton.setOnClickListener {
|
||||||
if (viewModel.isSelf) {
|
if (viewModel.isSelf) {
|
||||||
val intent = EditProfileActivityIntent(this@AccountActivity)
|
val intent = EditProfileActivityIntent(this@AccountActivity)
|
||||||
startActivity(intent)
|
startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -959,12 +974,12 @@ class AccountActivity :
|
||||||
|
|
||||||
override fun onViewTag(tag: String) {
|
override fun onViewTag(tag: String) {
|
||||||
val intent = TimelineActivityIntent.hashtag(this, tag)
|
val intent = TimelineActivityIntent.hashtag(this, tag)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAccount(id: String) {
|
override fun onViewAccount(id: String) {
|
||||||
val intent = AccountActivityIntent(this, id)
|
val intent = AccountActivityIntent(this, id)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewUrl(url: String) {
|
override fun onViewUrl(url: String) {
|
||||||
|
|
|
@ -32,9 +32,9 @@ import app.pachli.components.accountlist.adapter.FollowRequestsAdapter
|
||||||
import app.pachli.components.accountlist.adapter.FollowRequestsHeaderAdapter
|
import app.pachli.components.accountlist.adapter.FollowRequestsHeaderAdapter
|
||||||
import app.pachli.components.accountlist.adapter.MutesAdapter
|
import app.pachli.components.accountlist.adapter.MutesAdapter
|
||||||
import app.pachli.core.accounts.AccountManager
|
import app.pachli.core.accounts.AccountManager
|
||||||
import app.pachli.core.activity.BaseActivity
|
|
||||||
import app.pachli.core.activity.BottomSheetActivity
|
import app.pachli.core.activity.BottomSheetActivity
|
||||||
import app.pachli.core.activity.PostLookupFallbackBehavior
|
import app.pachli.core.activity.PostLookupFallbackBehavior
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
import app.pachli.core.common.extensions.viewBinding
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
|
@ -158,19 +158,15 @@ class AccountListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewTag(tag: String) {
|
override fun onViewTag(tag: String) {
|
||||||
(activity as BaseActivity?)
|
activity?.startActivityWithDefaultTransition(TimelineActivityIntent.hashtag(requireContext(), tag))
|
||||||
?.startActivityWithSlideInAnimation(TimelineActivityIntent.hashtag(requireContext(), tag))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAccount(id: String) {
|
override fun onViewAccount(id: String) {
|
||||||
(activity as BaseActivity?)?.let {
|
activity?.startActivityWithDefaultTransition(AccountActivityIntent(requireContext(), id))
|
||||||
val intent = AccountActivityIntent(it, id)
|
|
||||||
it.startActivityWithSlideInAnimation(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewUrl(url: String) {
|
override fun onViewUrl(url: String) {
|
||||||
(activity as BottomSheetActivity?)?.viewUrl(url, PostLookupFallbackBehavior.OPEN_IN_BROWSER)
|
(activity as? BottomSheetActivity)?.viewUrl(url, PostLookupFallbackBehavior.OPEN_IN_BROWSER)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) {
|
override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) {
|
||||||
|
|
|
@ -29,6 +29,7 @@ import app.pachli.R
|
||||||
import app.pachli.adapter.EmojiAdapter
|
import app.pachli.adapter.EmojiAdapter
|
||||||
import app.pachli.adapter.OnEmojiSelectedListener
|
import app.pachli.adapter.OnEmojiSelectedListener
|
||||||
import app.pachli.core.activity.BottomSheetActivity
|
import app.pachli.core.activity.BottomSheetActivity
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
import app.pachli.core.common.extensions.viewBinding
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
|
@ -187,7 +188,7 @@ class AnnouncementsActivity :
|
||||||
|
|
||||||
override fun onViewTag(tag: String) {
|
override fun onViewTag(tag: String) {
|
||||||
val intent = TimelineActivityIntent.hashtag(this, tag)
|
val intent = TimelineActivityIntent.hashtag(this, tag)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAccount(id: String) {
|
override fun onViewAccount(id: String) {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package app.pachli.components.compose
|
package app.pachli.components.compose
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -30,7 +31,6 @@ import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.style.URLSpan
|
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -94,7 +94,6 @@ import app.pachli.core.network.model.Status
|
||||||
import app.pachli.core.preferences.AppTheme
|
import app.pachli.core.preferences.AppTheme
|
||||||
import app.pachli.core.preferences.PrefKeys
|
import app.pachli.core.preferences.PrefKeys
|
||||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||||
import app.pachli.core.ui.MentionSpan
|
|
||||||
import app.pachli.databinding.ActivityComposeBinding
|
import app.pachli.databinding.ActivityComposeBinding
|
||||||
import app.pachli.util.PickMediaFiles
|
import app.pachli.util.PickMediaFiles
|
||||||
import app.pachli.util.getInitialLanguages
|
import app.pachli.util.getInitialLanguages
|
||||||
|
@ -155,9 +154,9 @@ class ComposeActivity :
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
var maximumTootCharacters = InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT
|
var maximumTootCharacters = InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT
|
||||||
var charactersReservedPerUrl = InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL
|
|
||||||
|
|
||||||
private val viewModel: ComposeViewModel by viewModels()
|
@VisibleForTesting
|
||||||
|
val viewModel: ComposeViewModel by viewModels()
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityComposeBinding::inflate)
|
private val binding by viewBinding(ActivityComposeBinding::inflate)
|
||||||
|
|
||||||
|
@ -206,6 +205,28 @@ class ComposeActivity :
|
||||||
viewModel.cropImageItemOld = null
|
viewModel.cropImageItemOld = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pressing back either (a) closes an open bottom sheet, or (b) goes
|
||||||
|
* back, if no bottom sheets are open.
|
||||||
|
*/
|
||||||
|
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
) {
|
||||||
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -268,7 +289,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
setupLanguageSpinner(getInitialLanguages(composeOptions?.language, activeAccount))
|
setupLanguageSpinner(getInitialLanguages(composeOptions?.language, activeAccount))
|
||||||
setupComposeField(sharedPreferencesRepository, viewModel.startingText, composeOptions)
|
setupComposeField(sharedPreferencesRepository, viewModel.initialContent, composeOptions)
|
||||||
setupContentWarningField(composeOptions?.contentWarning)
|
setupContentWarningField(composeOptions?.contentWarning)
|
||||||
setupPollView()
|
setupPollView()
|
||||||
applyShareIntent(intent, savedInstanceState)
|
applyShareIntent(intent, savedInstanceState)
|
||||||
|
@ -282,7 +303,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
it.getBoolean(CONTENT_WARNING_VISIBLE_KEY).apply {
|
it.getBoolean(CONTENT_WARNING_VISIBLE_KEY).apply {
|
||||||
viewModel.contentWarningChanged(this)
|
viewModel.showContentWarningChanged(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
it.getString(SCHEDULED_TIME_KEY)?.let { time ->
|
it.getString(SCHEDULED_TIME_KEY)?.let { time ->
|
||||||
|
@ -369,7 +390,9 @@ class ComposeActivity :
|
||||||
if (startingContentWarning != null) {
|
if (startingContentWarning != null) {
|
||||||
binding.composeContentWarningField.setText(startingContentWarning)
|
binding.composeContentWarningField.setText(startingContentWarning)
|
||||||
}
|
}
|
||||||
binding.composeContentWarningField.doOnTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() }
|
binding.composeContentWarningField.doOnTextChanged { newContentWarning, _, _, _ ->
|
||||||
|
viewModel.onContentWarningChanged(newContentWarning?.toString() ?: "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupComposeField(
|
private fun setupComposeField(
|
||||||
|
@ -404,7 +427,7 @@ class ComposeActivity :
|
||||||
highlightSpans(binding.composeEditField.text, mentionColour)
|
highlightSpans(binding.composeEditField.text, mentionColour)
|
||||||
binding.composeEditField.doAfterTextChanged { editable ->
|
binding.composeEditField.doAfterTextChanged { editable ->
|
||||||
highlightSpans(editable!!, mentionColour)
|
highlightSpans(editable!!, mentionColour)
|
||||||
updateVisibleCharactersLeft()
|
viewModel.onContentChanged(editable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
|
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
|
||||||
|
@ -419,12 +442,19 @@ class ComposeActivity :
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.instanceInfo.collect { instanceData ->
|
viewModel.instanceInfo.collect { instanceData ->
|
||||||
maximumTootCharacters = instanceData.maxChars
|
maximumTootCharacters = instanceData.maxChars
|
||||||
charactersReservedPerUrl = instanceData.charactersReservedPerUrl
|
|
||||||
maxUploadMediaNumber = instanceData.maxMediaAttachments
|
maxUploadMediaNumber = instanceData.maxMediaAttachments
|
||||||
updateVisibleCharactersLeft()
|
updateVisibleCharactersLeft(viewModel.statusLength.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.statusLength.collect { updateVisibleCharactersLeft(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.closeConfirmation.collect { updateOnBackPressedCallbackState(it, bottomSheetStates()) }
|
||||||
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.emoji.collect(::setEmojiList)
|
viewModel.emoji.collect(::setEmojiList)
|
||||||
}
|
}
|
||||||
|
@ -493,6 +523,23 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return List of states of the different bottomsheets */
|
||||||
|
private fun bottomSheetStates() = listOf(
|
||||||
|
composeOptionsBehavior.state,
|
||||||
|
addMediaBehavior.state,
|
||||||
|
emojiBehavior.state,
|
||||||
|
scheduleBehavior.state,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables [onBackPressedCallback] if a confirmation is required, or any botttom sheet is
|
||||||
|
* open. Otherwise disables.
|
||||||
|
*/
|
||||||
|
private fun updateOnBackPressedCallbackState(confirmationKind: ConfirmationKind, bottomSheetStates: List<Int>) {
|
||||||
|
onBackPressedCallback.isEnabled = confirmationKind != ConfirmationKind.NONE ||
|
||||||
|
bottomSheetStates.any { it != BottomSheetBehavior.STATE_HIDDEN }
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupButtons() {
|
private fun setupButtons() {
|
||||||
binding.composeOptionsBottomSheet.listener = this
|
binding.composeOptionsBottomSheet.listener = this
|
||||||
|
|
||||||
|
@ -501,6 +548,17 @@ class ComposeActivity :
|
||||||
scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView)
|
scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView)
|
||||||
emojiBehavior = BottomSheetBehavior.from(binding.emojiView)
|
emojiBehavior = BottomSheetBehavior.from(binding.emojiView)
|
||||||
|
|
||||||
|
val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||||
|
updateOnBackPressedCallbackState(viewModel.closeConfirmation.value, bottomSheetStates())
|
||||||
|
}
|
||||||
|
override fun onSlide(bottomSheet: View, slideOffset: Float) { }
|
||||||
|
}
|
||||||
|
composeOptionsBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||||
|
addMediaBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||||
|
scheduleBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||||
|
emojiBehavior.addBottomSheetCallback(bottomSheetCallback)
|
||||||
|
|
||||||
enableButton(binding.composeEmojiButton, clickable = false, colorActive = false)
|
enableButton(binding.composeEmojiButton, clickable = false, colorActive = false)
|
||||||
|
|
||||||
// Setup the interface buttons.
|
// Setup the interface buttons.
|
||||||
|
@ -534,26 +592,7 @@ class ComposeActivity :
|
||||||
binding.actionPhotoPick.setOnClickListener { onMediaPick() }
|
binding.actionPhotoPick.setOnClickListener { onMediaPick() }
|
||||||
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
|
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(
|
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||||
this,
|
|
||||||
object : OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
||||||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
||||||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
||||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
) {
|
|
||||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCloseButton()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupLanguageSpinner(initialLanguages: List<String>) {
|
private fun setupLanguageSpinner(initialLanguages: List<String>) {
|
||||||
|
@ -851,7 +890,7 @@ class ComposeActivity :
|
||||||
maxOptionLength = instanceParams.pollMaxLength,
|
maxOptionLength = instanceParams.pollMaxLength,
|
||||||
minDuration = instanceParams.pollMinDuration,
|
minDuration = instanceParams.pollMinDuration,
|
||||||
maxDuration = instanceParams.pollMaxDuration,
|
maxDuration = instanceParams.pollMaxDuration,
|
||||||
onUpdatePoll = viewModel::updatePoll,
|
onUpdatePoll = viewModel::onPollChanged,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -881,30 +920,21 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removePoll() {
|
private fun removePoll() {
|
||||||
viewModel.poll.value = null
|
viewModel.onPollChanged(null)
|
||||||
binding.pollPreview.hide()
|
binding.pollPreview.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVisibilityChanged(visibility: Status.Visibility) {
|
override fun onVisibilityChanged(visibility: Status.Visibility) {
|
||||||
|
viewModel.onStatusVisibilityChanged(visibility)
|
||||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
viewModel.statusVisibility.value = visibility
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
fun calculateTextLength(): Int {
|
|
||||||
return statusLength(
|
|
||||||
binding.composeEditField.text,
|
|
||||||
binding.composeContentWarningField.text,
|
|
||||||
charactersReservedPerUrl,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
val selectedLanguage: String?
|
val selectedLanguage: String?
|
||||||
get() = viewModel.postLanguage
|
get() = viewModel.postLanguage
|
||||||
|
|
||||||
private fun updateVisibleCharactersLeft() {
|
private fun updateVisibleCharactersLeft(textLength: Int) {
|
||||||
val remainingLength = maximumTootCharacters - calculateTextLength()
|
val remainingLength = maximumTootCharacters - textLength
|
||||||
binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength)
|
binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength)
|
||||||
|
|
||||||
val textColor = if (remainingLength < 0) {
|
val textColor = if (remainingLength < 0) {
|
||||||
|
@ -917,8 +947,7 @@ class ComposeActivity :
|
||||||
|
|
||||||
private fun onContentWarningChanged() {
|
private fun onContentWarningChanged() {
|
||||||
val showWarning = binding.composeContentWarningBar.isGone
|
val showWarning = binding.composeContentWarningBar.isGone
|
||||||
viewModel.contentWarningChanged(showWarning)
|
viewModel.showContentWarningChanged(showWarning)
|
||||||
updateVisibleCharactersLeft()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyScheduledTime(): Boolean {
|
private fun verifyScheduledTime(): Boolean {
|
||||||
|
@ -957,11 +986,11 @@ class ComposeActivity :
|
||||||
if (viewModel.showContentWarning.value) {
|
if (viewModel.showContentWarning.value) {
|
||||||
spoilerText = binding.composeContentWarningField.text.toString()
|
spoilerText = binding.composeContentWarningField.text.toString()
|
||||||
}
|
}
|
||||||
val characterCount = calculateTextLength()
|
val statusLength = viewModel.statusLength.value
|
||||||
if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value.isEmpty()) {
|
if ((statusLength <= 0 || contentText.isBlank()) && viewModel.media.value.isEmpty()) {
|
||||||
binding.composeEditField.error = getString(R.string.error_empty)
|
binding.composeEditField.error = getString(R.string.error_empty)
|
||||||
enableButtons(true, viewModel.editing)
|
enableButtons(true, viewModel.editing)
|
||||||
} else if (characterCount <= maximumTootCharacters) {
|
} else if (statusLength <= maximumTootCharacters) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.sendStatus(contentText, spoilerText, activeAccount.id)
|
viewModel.sendStatus(contentText, spoilerText, activeAccount.id)
|
||||||
deleteDraftAndFinish()
|
deleteDraftAndFinish()
|
||||||
|
@ -1008,8 +1037,9 @@ class ComposeActivity :
|
||||||
this,
|
this,
|
||||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
photoFile,
|
photoFile,
|
||||||
)
|
)?.also {
|
||||||
takePicture.launch(photoUploadUri)
|
takePicture.launch(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
|
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
|
||||||
|
@ -1105,6 +1135,7 @@ class ComposeActivity :
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("GestureBackNavigation")
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||||
if (event.isCtrlPressed) {
|
if (event.isCtrlPressed) {
|
||||||
|
@ -1126,10 +1157,10 @@ class ComposeActivity :
|
||||||
private fun handleCloseButton() {
|
private fun handleCloseButton() {
|
||||||
val contentText = binding.composeEditField.text.toString()
|
val contentText = binding.composeEditField.text.toString()
|
||||||
val contentWarning = binding.composeContentWarningField.text.toString()
|
val contentWarning = binding.composeContentWarningField.text.toString()
|
||||||
when (viewModel.handleCloseButton(contentText, contentWarning)) {
|
when (viewModel.closeConfirmation.value) {
|
||||||
ConfirmationKind.NONE -> {
|
ConfirmationKind.NONE -> {
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
ConfirmationKind.SAVE_OR_DISCARD ->
|
ConfirmationKind.SAVE_OR_DISCARD ->
|
||||||
getSaveAsDraftOrDiscardDialog(contentText, contentWarning).show()
|
getSaveAsDraftOrDiscardDialog(contentText, contentWarning).show()
|
||||||
|
@ -1183,7 +1214,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.action_discard) { _, _ ->
|
.setNegativeButton(R.string.action_discard) { _, _ ->
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1193,13 +1224,13 @@ class ComposeActivity :
|
||||||
*/
|
*/
|
||||||
private fun getContinueEditingOrDiscardDialog(): AlertDialog.Builder {
|
private fun getContinueEditingOrDiscardDialog(): AlertDialog.Builder {
|
||||||
return AlertDialog.Builder(this)
|
return AlertDialog.Builder(this)
|
||||||
.setMessage(R.string.compose_unsaved_changes)
|
.setMessage(R.string.unsaved_changes)
|
||||||
.setPositiveButton(R.string.action_continue_edit) { _, _ ->
|
.setPositiveButton(R.string.action_continue_edit) { _, _ ->
|
||||||
// Do nothing, dialog will dismiss, user can continue editing
|
// Do nothing, dialog will dismiss, user can continue editing
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.action_discard) { _, _ ->
|
.setNegativeButton(R.string.action_discard) { _, _ ->
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1213,7 +1244,7 @@ class ComposeActivity :
|
||||||
.setPositiveButton(R.string.action_delete) { _, _ ->
|
.setPositiveButton(R.string.action_delete) { _, _ ->
|
||||||
viewModel.deleteDraft()
|
viewModel.deleteDraft()
|
||||||
viewModel.stopUploads()
|
viewModel.stopUploads()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.action_continue_edit) { _, _ ->
|
.setNegativeButton(R.string.action_continue_edit) { _, _ ->
|
||||||
// Do nothing, dialog will dismiss, user can continue editing
|
// Do nothing, dialog will dismiss, user can continue editing
|
||||||
|
@ -1222,7 +1253,7 @@ class ComposeActivity :
|
||||||
|
|
||||||
private fun deleteDraftAndFinish() {
|
private fun deleteDraftAndFinish() {
|
||||||
viewModel.deleteDraft()
|
viewModel.deleteDraft()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveDraftAndFinish(contentText: String, contentWarning: String) {
|
private fun saveDraftAndFinish(contentText: String, contentWarning: String) {
|
||||||
|
@ -1240,7 +1271,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
viewModel.saveDraft(contentText, contentWarning)
|
viewModel.saveDraft(contentText, contentWarning)
|
||||||
dialog?.cancel()
|
dialog?.cancel()
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1315,54 +1346,6 @@ class ComposeActivity :
|
||||||
return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain")
|
return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the effective status length.
|
|
||||||
*
|
|
||||||
* Some text is counted differently:
|
|
||||||
*
|
|
||||||
* In the status body:
|
|
||||||
*
|
|
||||||
* - URLs always count for [urlLength] characters irrespective of their actual length
|
|
||||||
* (https://docs.joinmastodon.org/user/posting/#links)
|
|
||||||
* - Mentions ("@user@some.instance") only count the "@user" part
|
|
||||||
* (https://docs.joinmastodon.org/user/posting/#mentions)
|
|
||||||
* - Hashtags are always treated as their actual length, including the "#"
|
|
||||||
* (https://docs.joinmastodon.org/user/posting/#hashtags)
|
|
||||||
* - Emojis are treated as a single character
|
|
||||||
*
|
|
||||||
* Content warning text is always treated as its full length, URLs and other entities
|
|
||||||
* are not treated differently.
|
|
||||||
*
|
|
||||||
* @param body status body text
|
|
||||||
* @param contentWarning optional content warning text
|
|
||||||
* @param urlLength the number of characters attributed to URLs
|
|
||||||
* @return the effective status length
|
|
||||||
*/
|
|
||||||
fun statusLength(body: Spanned, contentWarning: Spanned?, urlLength: Int): Int {
|
|
||||||
var length = body.toString().mastodonLength() - body.getSpans(0, body.length, URLSpan::class.java)
|
|
||||||
.fold(0) { acc, span ->
|
|
||||||
// Accumulate a count of characters to be *ignored* in the final length
|
|
||||||
acc + when (span) {
|
|
||||||
is MentionSpan -> {
|
|
||||||
// Ignore everything from the second "@" (if present)
|
|
||||||
span.url.length - (
|
|
||||||
span.url.indexOf("@", 1).takeIf { it >= 0 }
|
|
||||||
?: span.url.length
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// Expected to be negative if the URL length < maxUrlLength
|
|
||||||
span.url.mastodonLength() - urlLength
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content warning text is treated as is, URLs or mentions there are not special
|
|
||||||
contentWarning?.let { length += it.toString().mastodonLength() }
|
|
||||||
|
|
||||||
return length
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [InputFilter] that uses the "Mastodon" length of a string, where emojis always
|
* [InputFilter] that uses the "Mastodon" length of a string, where emojis always
|
||||||
* count as a single character.
|
* count as a single character.
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
package app.pachli.components.compose
|
package app.pachli.components.compose
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.URLSpan
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
@ -25,6 +29,7 @@ import app.pachli.components.compose.ComposeAutoCompleteAdapter.AutocompleteResu
|
||||||
import app.pachli.components.drafts.DraftHelper
|
import app.pachli.components.drafts.DraftHelper
|
||||||
import app.pachli.components.search.SearchType
|
import app.pachli.components.search.SearchType
|
||||||
import app.pachli.core.accounts.AccountManager
|
import app.pachli.core.accounts.AccountManager
|
||||||
|
import app.pachli.core.common.string.mastodonLength
|
||||||
import app.pachli.core.common.string.randomAlphanumericString
|
import app.pachli.core.common.string.randomAlphanumericString
|
||||||
import app.pachli.core.data.model.InstanceInfo
|
import app.pachli.core.data.model.InstanceInfo
|
||||||
import app.pachli.core.data.repository.InstanceInfoRepository
|
import app.pachli.core.data.repository.InstanceInfoRepository
|
||||||
|
@ -35,6 +40,7 @@ import app.pachli.core.network.model.Emoji
|
||||||
import app.pachli.core.network.model.NewPoll
|
import app.pachli.core.network.model.NewPoll
|
||||||
import app.pachli.core.network.model.Status
|
import app.pachli.core.network.model.Status
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
|
import app.pachli.core.ui.MentionSpan
|
||||||
import app.pachli.service.MediaToSend
|
import app.pachli.service.MediaToSend
|
||||||
import app.pachli.service.ServiceClient
|
import app.pachli.service.ServiceClient
|
||||||
import app.pachli.service.StatusToSend
|
import app.pachli.service.StatusToSend
|
||||||
|
@ -49,6 +55,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
@ -66,20 +74,40 @@ class ComposeViewModel @Inject constructor(
|
||||||
instanceInfoRepo: InstanceInfoRepository,
|
instanceInfoRepo: InstanceInfoRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
/** The current content */
|
||||||
|
private var content: Editable = Editable.Factory.getInstance().newEditable("")
|
||||||
|
|
||||||
|
/** The current content warning */
|
||||||
|
private var contentWarning: String = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The effective content warning. Either the real content warning, or the empty string
|
||||||
|
* if the content warning has been hidden
|
||||||
|
*/
|
||||||
|
private val effectiveContentWarning
|
||||||
|
get() = if (showContentWarning.value) contentWarning else ""
|
||||||
|
|
||||||
private var replyingStatusAuthor: String? = null
|
private var replyingStatusAuthor: String? = null
|
||||||
private var replyingStatusContent: String? = null
|
private var replyingStatusContent: String? = null
|
||||||
internal var startingText: String? = null
|
|
||||||
|
/** The initial content for this status, before any edits */
|
||||||
|
internal var initialContent: String = ""
|
||||||
|
|
||||||
|
/** The initial content warning for this status, before any edits */
|
||||||
|
private var initialContentWarning: String = ""
|
||||||
|
|
||||||
internal var postLanguage: String? = null
|
internal var postLanguage: String? = null
|
||||||
|
|
||||||
|
/** If editing a draft then the ID of the draft, otherwise 0 */
|
||||||
private var draftId: Int = 0
|
private var draftId: Int = 0
|
||||||
private var scheduledTootId: String? = null
|
private var scheduledTootId: String? = null
|
||||||
private var startingContentWarning: String = ""
|
|
||||||
private var inReplyToId: String? = null
|
private var inReplyToId: String? = null
|
||||||
private var originalStatusId: String? = null
|
private var originalStatusId: String? = null
|
||||||
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
|
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
|
||||||
|
|
||||||
private var contentWarningStateChanged: Boolean = false
|
private var contentWarningStateChanged: Boolean = false
|
||||||
private var modifiedInitialState: Boolean = false
|
private var modifiedInitialState: Boolean = false
|
||||||
private var hasScheduledTimeChanged: Boolean = false
|
private var scheduledTimeChanged: Boolean = false
|
||||||
|
|
||||||
val instanceInfo: SharedFlow<InstanceInfo> = instanceInfoRepo::getInstanceInfo.asFlow()
|
val instanceInfo: SharedFlow<InstanceInfo> = instanceInfoRepo::getInstanceInfo.asFlow()
|
||||||
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||||
|
@ -87,16 +115,27 @@ class ComposeViewModel @Inject constructor(
|
||||||
val emoji: SharedFlow<List<Emoji>> = instanceInfoRepo::getEmojis.asFlow()
|
val emoji: SharedFlow<List<Emoji>> = instanceInfoRepo::getEmojis.asFlow()
|
||||||
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||||
|
|
||||||
val markMediaAsSensitive: MutableStateFlow<Boolean> =
|
private val _markMediaAsSensitive: MutableStateFlow<Boolean> =
|
||||||
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
||||||
|
val markMediaAsSensitive = _markMediaAsSensitive.asStateFlow()
|
||||||
|
|
||||||
val statusVisibility: MutableStateFlow<Status.Visibility> = MutableStateFlow(Status.Visibility.UNKNOWN)
|
private val _statusVisibility: MutableStateFlow<Status.Visibility> = MutableStateFlow(Status.Visibility.UNKNOWN)
|
||||||
val showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
val statusVisibility = _statusVisibility.asStateFlow()
|
||||||
val poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null)
|
private val _showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
val scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null)
|
val showContentWarning = _showContentWarning.asStateFlow()
|
||||||
|
private val _poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null)
|
||||||
|
val poll = _poll.asStateFlow()
|
||||||
|
private val _scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null)
|
||||||
|
val scheduledAt = _scheduledAt.asStateFlow()
|
||||||
|
|
||||||
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
private val _media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
||||||
val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
val media = _media.asStateFlow()
|
||||||
|
private val _uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
val uploadError = _uploadError.asSharedFlow()
|
||||||
|
private val _closeConfirmation = MutableStateFlow(ConfirmationKind.NONE)
|
||||||
|
val closeConfirmation = _closeConfirmation.asStateFlow()
|
||||||
|
private val _statusLength = MutableStateFlow(0)
|
||||||
|
val statusLength = _statusLength.asStateFlow()
|
||||||
|
|
||||||
private lateinit var composeKind: ComposeKind
|
private lateinit var composeKind: ComposeKind
|
||||||
|
|
||||||
|
@ -133,7 +172,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
): QueuedMedia {
|
): QueuedMedia {
|
||||||
var stashMediaItem: QueuedMedia? = null
|
var stashMediaItem: QueuedMedia? = null
|
||||||
|
|
||||||
media.update { mediaList ->
|
_media.update { mediaList ->
|
||||||
val mediaItem = QueuedMedia(
|
val mediaItem = QueuedMedia(
|
||||||
localId = mediaUploader.getNewLocalMediaId(),
|
localId = mediaUploader.getNewLocalMediaId(),
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
@ -176,12 +215,12 @@ class ComposeViewModel @Inject constructor(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
is UploadEvent.ErrorEvent -> {
|
is UploadEvent.ErrorEvent -> {
|
||||||
media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
|
_media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
|
||||||
uploadError.emit(event.error)
|
_uploadError.emit(event.error)
|
||||||
return@collect
|
return@collect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
media.update { mediaList ->
|
_media.update { mediaList ->
|
||||||
mediaList.map { mediaItem ->
|
mediaList.map { mediaItem ->
|
||||||
if (mediaItem.localId == newMediaItem.localId) {
|
if (mediaItem.localId == newMediaItem.localId) {
|
||||||
newMediaItem
|
newMediaItem
|
||||||
|
@ -192,11 +231,13 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateCloseConfirmation()
|
||||||
return mediaItem
|
return mediaItem
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?, focus: Attachment.Focus?) {
|
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?, focus: Attachment.Focus?) {
|
||||||
media.update { mediaList ->
|
_media.update { mediaList ->
|
||||||
val mediaItem = QueuedMedia(
|
val mediaItem = QueuedMedia(
|
||||||
localId = mediaUploader.getNewLocalMediaId(),
|
localId = mediaUploader.getNewLocalMediaId(),
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
@ -214,22 +255,53 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
fun removeMediaFromQueue(item: QueuedMedia) {
|
fun removeMediaFromQueue(item: QueuedMedia) {
|
||||||
mediaUploader.cancelUploadScope(item.localId)
|
mediaUploader.cancelUploadScope(item.localId)
|
||||||
media.update { mediaList -> mediaList.filter { it.localId != item.localId } }
|
_media.update { mediaList -> mediaList.filter { it.localId != item.localId } }
|
||||||
|
updateCloseConfirmation()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleMarkSensitive() {
|
fun toggleMarkSensitive() {
|
||||||
this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true
|
this._markMediaAsSensitive.value = this._markMediaAsSensitive.value != true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleCloseButton(contentText: String?, contentWarning: String?): ConfirmationKind {
|
/** Call this when the status' primary content changes */
|
||||||
return if (didChange(contentText, contentWarning)) {
|
fun onContentChanged(newContent: Editable) {
|
||||||
|
content = newContent
|
||||||
|
updateStatusLength()
|
||||||
|
updateCloseConfirmation()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call this when the status' content warning changes */
|
||||||
|
fun onContentWarningChanged(newContentWarning: String) {
|
||||||
|
contentWarning = newContentWarning
|
||||||
|
updateStatusLength()
|
||||||
|
updateCloseConfirmation()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call this to attach or clear the status' poll */
|
||||||
|
fun onPollChanged(newPoll: NewPoll?) {
|
||||||
|
_poll.value = newPoll
|
||||||
|
updateCloseConfirmation()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call this to change the status' visibility */
|
||||||
|
fun onStatusVisibilityChanged(newVisibility: Status.Visibility) {
|
||||||
|
_statusVisibility.value = newVisibility
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun updateStatusLength() {
|
||||||
|
_statusLength.value = statusLength(content, effectiveContentWarning, instanceInfo.replayCache.last().charactersReservedPerUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateCloseConfirmation() {
|
||||||
|
_closeConfirmation.value = if (isDirty()) {
|
||||||
when (composeKind) {
|
when (composeKind) {
|
||||||
ComposeKind.NEW -> if (isEmpty(contentText, contentWarning)) {
|
ComposeKind.NEW -> if (isEmpty(content, effectiveContentWarning)) {
|
||||||
ConfirmationKind.NONE
|
ConfirmationKind.NONE
|
||||||
} else {
|
} else {
|
||||||
ConfirmationKind.SAVE_OR_DISCARD
|
ConfirmationKind.SAVE_OR_DISCARD
|
||||||
}
|
}
|
||||||
ComposeKind.EDIT_DRAFT -> if (isEmpty(contentText, contentWarning)) {
|
ComposeKind.EDIT_DRAFT -> if (isEmpty(content, effectiveContentWarning)) {
|
||||||
ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT
|
ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT
|
||||||
} else {
|
} else {
|
||||||
ConfirmationKind.UPDATE_OR_DISCARD
|
ConfirmationKind.UPDATE_OR_DISCARD
|
||||||
|
@ -242,23 +314,30 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun didChange(content: String?, contentWarning: String?): Boolean {
|
/**
|
||||||
val textChanged = content.orEmpty() != startingText.orEmpty()
|
* @return True if content of this status is "dirty", meaning one or more of the
|
||||||
val contentWarningChanged = contentWarning.orEmpty() != startingContentWarning
|
* following have changed since the compose session started: content,
|
||||||
|
* content warning and content warning visibility, media, polls, or the
|
||||||
|
* scheduled time to send.
|
||||||
|
*/
|
||||||
|
private fun isDirty(): Boolean {
|
||||||
|
val contentChanged = !content.contentEquals(initialContent)
|
||||||
|
|
||||||
|
val contentWarningChanged = effectiveContentWarning != initialContentWarning
|
||||||
val mediaChanged = media.value.isNotEmpty()
|
val mediaChanged = media.value.isNotEmpty()
|
||||||
val pollChanged = poll.value != null
|
val pollChanged = poll.value != null
|
||||||
val didScheduledTimeChange = hasScheduledTimeChanged
|
|
||||||
|
|
||||||
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange
|
return modifiedInitialState || contentChanged || contentWarningChanged || mediaChanged || pollChanged || scheduledTimeChanged
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isEmpty(content: String?, contentWarning: String?): Boolean {
|
private fun isEmpty(content: CharSequence, contentWarning: CharSequence): Boolean {
|
||||||
return !modifiedInitialState && (content.isNullOrBlank() && contentWarning.isNullOrBlank() && media.value.isEmpty() && poll.value == null)
|
return !modifiedInitialState && (content.isBlank() && contentWarning.isBlank() && media.value.isEmpty() && poll.value == null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun contentWarningChanged(value: Boolean) {
|
fun showContentWarningChanged(value: Boolean) {
|
||||||
showContentWarning.value = value
|
_showContentWarning.value = value
|
||||||
contentWarningStateChanged = true
|
contentWarningStateChanged = true
|
||||||
|
updateStatusLength()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDraft() {
|
fun deleteDraft() {
|
||||||
|
@ -296,7 +375,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
inReplyToId = inReplyToId,
|
inReplyToId = inReplyToId,
|
||||||
content = content,
|
content = content,
|
||||||
contentWarning = contentWarning,
|
contentWarning = contentWarning,
|
||||||
sensitive = markMediaAsSensitive.value,
|
sensitive = _markMediaAsSensitive.value,
|
||||||
visibility = statusVisibility.value,
|
visibility = statusVisibility.value,
|
||||||
mediaUris = mediaUris,
|
mediaUris = mediaUris,
|
||||||
mediaDescriptions = mediaDescriptions,
|
mediaDescriptions = mediaDescriptions,
|
||||||
|
@ -337,7 +416,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
text = content,
|
text = content,
|
||||||
warningText = spoilerText,
|
warningText = spoilerText,
|
||||||
visibility = statusVisibility.value.serverString(),
|
visibility = statusVisibility.value.serverString(),
|
||||||
sensitive = attachedMedia.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value),
|
sensitive = attachedMedia.isNotEmpty() && (_markMediaAsSensitive.value || showContentWarning.value),
|
||||||
media = attachedMedia,
|
media = attachedMedia,
|
||||||
scheduledAt = scheduledAt.value,
|
scheduledAt = scheduledAt.value,
|
||||||
inReplyToId = inReplyToId,
|
inReplyToId = inReplyToId,
|
||||||
|
@ -356,7 +435,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia) {
|
private fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia) {
|
||||||
media.update { mediaList ->
|
_media.update { mediaList ->
|
||||||
mediaList.map { mediaItem ->
|
mediaList.map { mediaItem ->
|
||||||
if (mediaItem.localId == localId) {
|
if (mediaItem.localId == localId) {
|
||||||
mutator(mediaItem)
|
mutator(mediaItem)
|
||||||
|
@ -438,10 +517,10 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
val contentWarning = composeOptions?.contentWarning
|
val contentWarning = composeOptions?.contentWarning
|
||||||
if (contentWarning != null) {
|
if (contentWarning != null) {
|
||||||
startingContentWarning = contentWarning
|
initialContentWarning = contentWarning
|
||||||
}
|
}
|
||||||
if (!contentWarningStateChanged) {
|
if (!contentWarningStateChanged) {
|
||||||
showContentWarning.value = !contentWarning.isNullOrBlank()
|
_showContentWarning.value = !contentWarning.isNullOrBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
// recreate media list
|
// recreate media list
|
||||||
|
@ -468,14 +547,14 @@ class ComposeViewModel @Inject constructor(
|
||||||
draftId = composeOptions?.draftId ?: 0
|
draftId = composeOptions?.draftId ?: 0
|
||||||
scheduledTootId = composeOptions?.scheduledTootId
|
scheduledTootId = composeOptions?.scheduledTootId
|
||||||
originalStatusId = composeOptions?.statusId
|
originalStatusId = composeOptions?.statusId
|
||||||
startingText = composeOptions?.content
|
initialContent = composeOptions?.content ?: ""
|
||||||
postLanguage = composeOptions?.language
|
postLanguage = composeOptions?.language
|
||||||
|
|
||||||
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
||||||
if (tootVisibility != Status.Visibility.UNKNOWN) {
|
if (tootVisibility != Status.Visibility.UNKNOWN) {
|
||||||
startingVisibility = tootVisibility
|
startingVisibility = tootVisibility
|
||||||
}
|
}
|
||||||
statusVisibility.value = startingVisibility
|
_statusVisibility.value = startingVisibility
|
||||||
val mentionedUsernames = composeOptions?.mentionedUsernames
|
val mentionedUsernames = composeOptions?.mentionedUsernames
|
||||||
if (mentionedUsernames != null) {
|
if (mentionedUsernames != null) {
|
||||||
val builder = StringBuilder()
|
val builder = StringBuilder()
|
||||||
|
@ -484,44 +563,101 @@ class ComposeViewModel @Inject constructor(
|
||||||
builder.append(name)
|
builder.append(name)
|
||||||
builder.append(' ')
|
builder.append(' ')
|
||||||
}
|
}
|
||||||
startingText = builder.toString()
|
initialContent = builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduledAt.value = composeOptions?.scheduledAt
|
_scheduledAt.value = composeOptions?.scheduledAt
|
||||||
|
|
||||||
composeOptions?.sensitive?.let { markMediaAsSensitive.value = it }
|
composeOptions?.sensitive?.let { _markMediaAsSensitive.value = it }
|
||||||
|
|
||||||
val poll = composeOptions?.poll
|
val poll = composeOptions?.poll
|
||||||
if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) {
|
if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) {
|
||||||
this.poll.value = poll
|
_poll.value = poll
|
||||||
}
|
}
|
||||||
replyingStatusContent = composeOptions?.replyingStatusContent
|
replyingStatusContent = composeOptions?.replyingStatusContent
|
||||||
replyingStatusAuthor = composeOptions?.replyingStatusAuthor
|
replyingStatusAuthor = composeOptions?.replyingStatusAuthor
|
||||||
|
|
||||||
|
updateCloseConfirmation()
|
||||||
setupComplete = true
|
setupComplete = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePoll(newPoll: NewPoll) {
|
|
||||||
poll.value = newPoll
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateScheduledAt(newScheduledAt: String?) {
|
fun updateScheduledAt(newScheduledAt: String?) {
|
||||||
if (newScheduledAt != scheduledAt.value) {
|
if (newScheduledAt != scheduledAt.value) {
|
||||||
hasScheduledTimeChanged = true
|
scheduledTimeChanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduledAt.value = newScheduledAt
|
_scheduledAt.value = newScheduledAt
|
||||||
|
updateCloseConfirmation()
|
||||||
}
|
}
|
||||||
|
|
||||||
val editing: Boolean
|
val editing: Boolean
|
||||||
get() = !originalStatusId.isNullOrEmpty()
|
get() = !originalStatusId.isNullOrEmpty()
|
||||||
|
|
||||||
enum class ConfirmationKind {
|
enum class ConfirmationKind {
|
||||||
NONE, // just close
|
/** No confirmation, finish */
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
/** Content has changed and it's an un-posted status, show "save or discard" */
|
||||||
SAVE_OR_DISCARD,
|
SAVE_OR_DISCARD,
|
||||||
|
|
||||||
|
/** Content has changed when editing a draft, show "update draft or discard changes" */
|
||||||
UPDATE_OR_DISCARD,
|
UPDATE_OR_DISCARD,
|
||||||
CONTINUE_EDITING_OR_DISCARD_CHANGES, // editing post
|
|
||||||
CONTINUE_EDITING_OR_DISCARD_DRAFT, // edit draft
|
/** Content has changed when editing a posted status or scheduled status */
|
||||||
|
CONTINUE_EDITING_OR_DISCARD_CHANGES,
|
||||||
|
|
||||||
|
/** Content has been cleared when editing a draft */
|
||||||
|
CONTINUE_EDITING_OR_DISCARD_DRAFT,
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Calculate the effective status length.
|
||||||
|
*
|
||||||
|
* Some text is counted differently:
|
||||||
|
*
|
||||||
|
* In the status body:
|
||||||
|
*
|
||||||
|
* - URLs always count for [urlLength] characters irrespective of their actual length
|
||||||
|
* (https://docs.joinmastodon.org/user/posting/#links)
|
||||||
|
* - Mentions ("@user@some.instance") only count the "@user" part
|
||||||
|
* (https://docs.joinmastodon.org/user/posting/#mentions)
|
||||||
|
* - Hashtags are always treated as their actual length, including the "#"
|
||||||
|
* (https://docs.joinmastodon.org/user/posting/#hashtags)
|
||||||
|
* - Emojis are treated as a single character
|
||||||
|
*
|
||||||
|
* Content warning text is always treated as its full length, URLs and other entities
|
||||||
|
* are not treated differently.
|
||||||
|
*
|
||||||
|
* @param body status body text
|
||||||
|
* @param contentWarning optional content warning text
|
||||||
|
* @param urlLength the number of characters attributed to URLs
|
||||||
|
* @return the effective status length
|
||||||
|
*/
|
||||||
|
fun statusLength(body: Spanned, contentWarning: String, urlLength: Int): Int {
|
||||||
|
var length = body.toString().mastodonLength() - body.getSpans(0, body.length, URLSpan::class.java)
|
||||||
|
.fold(0) { acc, span ->
|
||||||
|
// Accumulate a count of characters to be *ignored* in the final length
|
||||||
|
acc + when (span) {
|
||||||
|
is MentionSpan -> {
|
||||||
|
// Ignore everything from the second "@" (if present)
|
||||||
|
span.url.length - (
|
||||||
|
span.url.indexOf("@", 1).takeIf { it >= 0 }
|
||||||
|
?: span.url.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Expected to be negative if the URL length < maxUrlLength
|
||||||
|
span.url.mastodonLength() - urlLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content warning text is treated as is, URLs or mentions there are not special
|
||||||
|
length += contentWarning.mastodonLength()
|
||||||
|
|
||||||
|
return length
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
package app.pachli.components.filters
|
package app.pachli.components.filters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.DialogInterface.BUTTON_NEGATIVE
|
||||||
import android.content.DialogInterface.BUTTON_POSITIVE
|
import android.content.DialogInterface.BUTTON_POSITIVE
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.size
|
import androidx.core.view.size
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
@ -21,6 +23,7 @@ import app.pachli.core.network.model.Filter
|
||||||
import app.pachli.core.network.model.FilterContext
|
import app.pachli.core.network.model.FilterContext
|
||||||
import app.pachli.core.network.model.FilterKeyword
|
import app.pachli.core.network.model.FilterKeyword
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
|
import app.pachli.core.ui.extensions.await
|
||||||
import app.pachli.databinding.ActivityEditFilterBinding
|
import app.pachli.databinding.ActivityEditFilterBinding
|
||||||
import app.pachli.databinding.DialogFilterBinding
|
import app.pachli.databinding.DialogFilterBinding
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
|
@ -28,8 +31,8 @@ import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.util.Date
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
|
||||||
|
@ -51,11 +54,20 @@ class EditFilterActivity : BaseActivity() {
|
||||||
private var originalFilter: Filter? = null
|
private var originalFilter: Filter? = null
|
||||||
private lateinit var filterContextSwitches: Map<SwitchMaterial, FilterContext>
|
private lateinit var filterContextSwitches: Map<SwitchMaterial, FilterContext>
|
||||||
|
|
||||||
|
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (showUnsavedChangesFilterDialog() == BUTTON_NEGATIVE) finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
||||||
|
|
||||||
originalFilter = EditFilterActivityIntent.getFilter(intent)
|
originalFilter = EditFilterActivityIntent.getFilter(intent)
|
||||||
filter = originalFilter ?: Filter("", "", listOf(), null, Filter.Action.WARN, listOf())
|
filter = originalFilter ?: Filter()
|
||||||
binding.apply {
|
binding.apply {
|
||||||
filterContextSwitches = mapOf(
|
filterContextSwitches = mapOf(
|
||||||
filterContextHome to FilterContext.HOME,
|
filterContextHome to FilterContext.HOME,
|
||||||
|
@ -69,7 +81,6 @@ class EditFilterActivity : BaseActivity() {
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
supportActionBar?.run {
|
supportActionBar?.run {
|
||||||
// Back button
|
|
||||||
setDisplayHomeAsUpEnabled(true)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
}
|
}
|
||||||
|
@ -99,12 +110,10 @@ class EditFilterActivity : BaseActivity() {
|
||||||
} else {
|
} else {
|
||||||
viewModel.removeContext(context)
|
viewModel.removeContext(context)
|
||||||
}
|
}
|
||||||
validateSaveButton()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.filterTitle.doAfterTextChanged { editable ->
|
binding.filterTitle.doAfterTextChanged { editable ->
|
||||||
viewModel.setTitle(editable.toString())
|
viewModel.setTitle(editable.toString())
|
||||||
validateSaveButton()
|
|
||||||
}
|
}
|
||||||
binding.filterActionWarn.setOnCheckedChangeListener { _, checked ->
|
binding.filterActionWarn.setOnCheckedChangeListener { _, checked ->
|
||||||
viewModel.setAction(
|
viewModel.setAction(
|
||||||
|
@ -130,13 +139,8 @@ class EditFilterActivity : BaseActivity() {
|
||||||
viewModel.setDuration(0)
|
viewModel.setDuration(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
validateSaveButton()
|
|
||||||
|
|
||||||
if (originalFilter == null) {
|
loadFilter()
|
||||||
binding.filterActionWarn.isChecked = true
|
|
||||||
} else {
|
|
||||||
loadFilter()
|
|
||||||
}
|
|
||||||
observeModel()
|
observeModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +174,25 @@ class EditFilterActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.isDirty.collectLatest { onBackPressedCallback.isEnabled = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.validationErrors.collectLatest { errors ->
|
||||||
|
binding.filterSaveButton.isEnabled = errors.isEmpty()
|
||||||
|
|
||||||
|
binding.filterTitleWrapper.error = if (errors.contains(FilterValidationError.NO_TITLE)) {
|
||||||
|
getString(R.string.error_filter_missing_title)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.keywordChipsError.isVisible = errors.contains(FilterValidationError.NO_KEYWORDS)
|
||||||
|
binding.filterContextError.isVisible = errors.contains(FilterValidationError.NO_CONTEXT)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the UI from the filter's members
|
// Populate the UI from the filter's members
|
||||||
|
@ -213,7 +236,6 @@ class EditFilterActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
filter = filter.copy(keywords = newKeywords)
|
filter = filter.copy(keywords = newKeywords)
|
||||||
validateSaveButton()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showAddKeywordDialog() {
|
private fun showAddKeywordDialog() {
|
||||||
|
@ -256,9 +278,18 @@ class EditFilterActivity : BaseActivity() {
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateSaveButton() {
|
/**
|
||||||
binding.filterSaveButton.isEnabled = viewModel.validate()
|
* Dialog that warns the user they have unsaved changes, and prompts
|
||||||
}
|
* to continue editing or discard the changes.
|
||||||
|
*
|
||||||
|
* @return [BUTTON_NEGATIVE] if the user chose to discard the changes,
|
||||||
|
* [BUTTON_POSITIVE] if the user chose to continue editing.
|
||||||
|
*/
|
||||||
|
suspend fun showUnsavedChangesFilterDialog() = AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.unsaved_changes)
|
||||||
|
.setCancelable(true)
|
||||||
|
.create()
|
||||||
|
.await(R.string.action_continue_edit, R.string.action_discard)
|
||||||
|
|
||||||
private fun saveChanges() {
|
private fun saveChanges() {
|
||||||
// TODO use a progress bar here (see EditProfileActivity/activity_edit_profile.xml for example)?
|
// TODO use a progress bar here (see EditProfileActivity/activity_edit_profile.xml for example)?
|
||||||
|
@ -297,16 +328,4 @@ class EditFilterActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
// Mastodon *stores* the absolute date in the filter,
|
|
||||||
// but create/edit take a number of seconds (relative to the time the operation is posted)
|
|
||||||
fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): String? {
|
|
||||||
return when (index) {
|
|
||||||
-1 -> default?.let { ((default.time - System.currentTimeMillis()) / 1000).toString() }
|
|
||||||
0 -> ""
|
|
||||||
else -> context?.resources?.getStringArray(R.array.filter_duration_values)?.get(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package app.pachli.components.filters
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.pachli.R
|
||||||
import app.pachli.appstore.EventHub
|
import app.pachli.appstore.EventHub
|
||||||
import app.pachli.appstore.FilterChangedEvent
|
import app.pachli.appstore.FilterChangedEvent
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
|
@ -11,20 +12,38 @@ import app.pachli.core.network.model.FilterKeyword
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() {
|
class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() {
|
||||||
private var originalFilter: Filter? = null
|
private lateinit var originalFilter: Filter
|
||||||
val title = MutableStateFlow("")
|
val title = MutableStateFlow("")
|
||||||
val keywords = MutableStateFlow(listOf<FilterKeyword>())
|
val keywords = MutableStateFlow(listOf<FilterKeyword>())
|
||||||
val action = MutableStateFlow(Filter.Action.WARN)
|
val action = MutableStateFlow(Filter.Action.WARN)
|
||||||
val duration = MutableStateFlow(0)
|
val duration = MutableStateFlow(0)
|
||||||
val contexts = MutableStateFlow(listOf<FilterContext>())
|
val contexts = MutableStateFlow(listOf<FilterContext>())
|
||||||
|
|
||||||
|
/** Track whether the duration has been modified, for use in [onChange] */
|
||||||
|
// TODO: Rethink how duration is shown in the UI.
|
||||||
|
// Could show the actual end time with the date/time widget to set the duration,
|
||||||
|
// along with dropdown for quick settings (1h, etc).
|
||||||
|
private var durationIsDirty = false
|
||||||
|
|
||||||
|
private val _isDirty = MutableStateFlow(false)
|
||||||
|
|
||||||
|
/** True if the user has made unsaved changes to the filter */
|
||||||
|
val isDirty = _isDirty.asStateFlow()
|
||||||
|
|
||||||
|
private val _validationErrors = MutableStateFlow(emptySet<FilterValidationError>())
|
||||||
|
|
||||||
|
/** True if the filter is valid and can be saved */
|
||||||
|
val validationErrors = _validationErrors.asStateFlow()
|
||||||
|
|
||||||
fun load(filter: Filter) {
|
fun load(filter: Filter) {
|
||||||
originalFilter = filter
|
originalFilter = filter
|
||||||
title.value = filter.title
|
title.value = filter.title
|
||||||
|
@ -40,10 +59,12 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
||||||
|
|
||||||
fun addKeyword(keyword: FilterKeyword) {
|
fun addKeyword(keyword: FilterKeyword) {
|
||||||
keywords.value += keyword
|
keywords.value += keyword
|
||||||
|
onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteKeyword(keyword: FilterKeyword) {
|
fun deleteKeyword(keyword: FilterKeyword) {
|
||||||
keywords.value = keywords.value.filterNot { it == keyword }
|
keywords.value = keywords.value.filterNot { it == keyword }
|
||||||
|
onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun modifyKeyword(original: FilterKeyword, updated: FilterKeyword) {
|
fun modifyKeyword(original: FilterKeyword, updated: FilterKeyword) {
|
||||||
|
@ -52,35 +73,66 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
||||||
keywords.value = keywords.value.toMutableList().apply {
|
keywords.value = keywords.value.toMutableList().apply {
|
||||||
set(index, updated)
|
set(index, updated)
|
||||||
}
|
}
|
||||||
|
onChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTitle(title: String) {
|
fun setTitle(title: String) {
|
||||||
this.title.value = title
|
this.title.value = title
|
||||||
|
onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDuration(index: Int) {
|
fun setDuration(index: Int) {
|
||||||
|
if (!durationIsDirty && duration.value != index) durationIsDirty = true
|
||||||
|
|
||||||
duration.value = index
|
duration.value = index
|
||||||
|
onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAction(action: Filter.Action) {
|
fun setAction(action: Filter.Action) {
|
||||||
this.action.value = action
|
this.action.value = action
|
||||||
|
onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addContext(filterContext: FilterContext) {
|
fun addContext(filterContext: FilterContext) {
|
||||||
if (!contexts.value.contains(filterContext)) {
|
if (!contexts.value.contains(filterContext)) {
|
||||||
contexts.value += filterContext
|
contexts.value += filterContext
|
||||||
|
onChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeContext(filterContext: FilterContext) {
|
fun removeContext(filterContext: FilterContext) {
|
||||||
contexts.value = contexts.value.filter { it != filterContext }
|
contexts.value = contexts.value.filter { it != filterContext }
|
||||||
|
onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validate(): Boolean {
|
private fun validate() {
|
||||||
return title.value.isNotBlank() &&
|
_validationErrors.value = buildSet {
|
||||||
keywords.value.isNotEmpty() &&
|
if (title.value.isBlank()) add(FilterValidationError.NO_TITLE)
|
||||||
contexts.value.isNotEmpty()
|
if (keywords.value.isEmpty()) add(FilterValidationError.NO_KEYWORDS)
|
||||||
|
if (contexts.value.isEmpty()) add(FilterValidationError.NO_CONTEXT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call when the contents of the filter change; recalculates validity
|
||||||
|
* and dirty state.
|
||||||
|
*/
|
||||||
|
private fun onChange() {
|
||||||
|
validate()
|
||||||
|
|
||||||
|
if (durationIsDirty) {
|
||||||
|
_isDirty.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDirty.value = when {
|
||||||
|
originalFilter.title != title.value -> true
|
||||||
|
originalFilter.contexts != contexts.value -> true
|
||||||
|
originalFilter.action != action.value -> true
|
||||||
|
originalFilter.keywords.toSet() != keywords.value.toSet() -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveChanges(context: Context): Boolean {
|
suspend fun saveChanges(context: Context): Boolean {
|
||||||
|
@ -90,15 +142,17 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
||||||
val action = action.value
|
val action = action.value
|
||||||
|
|
||||||
return withContext(viewModelScope.coroutineContext) {
|
return withContext(viewModelScope.coroutineContext) {
|
||||||
val success = originalFilter?.let { filter ->
|
val success = if (originalFilter.id == "") {
|
||||||
updateFilter(filter, title, contexts, action, durationIndex, context)
|
createFilter(title, contexts, action, durationIndex, context)
|
||||||
} ?: createFilter(title, contexts, action, durationIndex, context)
|
} else {
|
||||||
|
updateFilter(originalFilter, title, contexts, action, durationIndex, context)
|
||||||
|
}
|
||||||
|
|
||||||
// Send FilterChangedEvent for old and new contexts, to ensure that
|
// Send FilterChangedEvent for old and new contexts, to ensure that
|
||||||
// e.g., removing a filter from "home" still notifies anything showing
|
// e.g., removing a filter from "home" still notifies anything showing
|
||||||
// the home timeline, so the timeline can be refreshed.
|
// the home timeline, so the timeline can be refreshed.
|
||||||
if (success) {
|
if (success) {
|
||||||
val originalContexts = originalFilter?.contexts ?: emptyList()
|
val originalContexts = originalFilter.contexts
|
||||||
val newFilterContexts = contexts
|
val newFilterContexts = contexts
|
||||||
(originalContexts + newFilterContexts).distinct().forEach {
|
(originalContexts + newFilterContexts).distinct().forEach {
|
||||||
eventHub.dispatch(FilterChangedEvent(it))
|
eventHub.dispatch(FilterChangedEvent(it))
|
||||||
|
@ -109,7 +163,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createFilter(title: String, contexts: List<FilterContext>, action: Filter.Action, durationIndex: Int, context: Context): Boolean {
|
private suspend fun createFilter(title: String, contexts: List<FilterContext>, action: Filter.Action, durationIndex: Int, context: Context): Boolean {
|
||||||
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
|
val expiresInSeconds = getSecondsForDurationIndex(durationIndex, context)
|
||||||
api.createFilter(
|
api.createFilter(
|
||||||
title = title,
|
title = title,
|
||||||
context = contexts,
|
context = contexts,
|
||||||
|
@ -133,7 +187,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateFilter(originalFilter: Filter, title: String, contexts: List<FilterContext>, action: Filter.Action, durationIndex: Int, context: Context): Boolean {
|
private suspend fun updateFilter(originalFilter: Filter, title: String, contexts: List<FilterContext>, action: Filter.Action, durationIndex: Int, context: Context): Boolean {
|
||||||
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
|
val expiresInSeconds = getSecondsForDurationIndex(durationIndex, context)
|
||||||
api.updateFilter(
|
api.updateFilter(
|
||||||
id = originalFilter.id,
|
id = originalFilter.id,
|
||||||
title = title,
|
title = title,
|
||||||
|
@ -176,7 +230,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
||||||
|
|
||||||
private suspend fun updateFilterV1(contexts: List<FilterContext>, expiresInSeconds: String?): Boolean {
|
private suspend fun updateFilterV1(contexts: List<FilterContext>, expiresInSeconds: String?): Boolean {
|
||||||
val results = keywords.value.map { keyword ->
|
val results = keywords.value.map { keyword ->
|
||||||
if (originalFilter == null) {
|
if (originalFilter.id == "") {
|
||||||
api.createFilterV1(
|
api.createFilterV1(
|
||||||
phrase = keyword.keyword,
|
phrase = keyword.keyword,
|
||||||
context = contexts,
|
context = contexts,
|
||||||
|
@ -186,7 +240,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
api.updateFilterV1(
|
api.updateFilterV1(
|
||||||
id = originalFilter!!.id,
|
id = originalFilter.id,
|
||||||
phrase = keyword.keyword,
|
phrase = keyword.keyword,
|
||||||
context = contexts,
|
context = contexts,
|
||||||
irreversible = false,
|
irreversible = false,
|
||||||
|
@ -199,4 +253,18 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
||||||
|
|
||||||
return results.none { it.isFailure }
|
return results.none { it.isFailure }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Mastodon *stores* the absolute date in the filter,
|
||||||
|
* but create/edit take a number of seconds (relative to the time the operation is posted)
|
||||||
|
*/
|
||||||
|
fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): String? {
|
||||||
|
return when (index) {
|
||||||
|
-1 -> default?.let { ((default.time - System.currentTimeMillis()) / 1000).toString() }
|
||||||
|
0 -> ""
|
||||||
|
else -> context?.resources?.getStringArray(R.array.filter_duration_values)?.get(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,10 @@
|
||||||
package app.pachli.components.filters
|
package app.pachli.components.filters
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
|
import app.pachli.core.network.model.Filter
|
||||||
import app.pachli.core.ui.extensions.await
|
import app.pachli.core.ui.extensions.await
|
||||||
|
|
||||||
internal suspend fun Activity.showDeleteFilterDialog(filterTitle: String) = AlertDialog.Builder(this)
|
internal suspend fun Activity.showDeleteFilterDialog(filterTitle: String) = AlertDialog.Builder(this)
|
||||||
|
@ -27,3 +29,36 @@ internal suspend fun Activity.showDeleteFilterDialog(filterTitle: String) = Aler
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.create()
|
.create()
|
||||||
.await(R.string.dialog_delete_filter_positive_action, android.R.string.cancel)
|
.await(R.string.dialog_delete_filter_positive_action, android.R.string.cancel)
|
||||||
|
|
||||||
|
/** Reasons why a filter might be invalid */
|
||||||
|
enum class FilterValidationError {
|
||||||
|
/** Filter title is empty or blank */
|
||||||
|
NO_TITLE,
|
||||||
|
|
||||||
|
/** Filter has no keywords */
|
||||||
|
NO_KEYWORDS,
|
||||||
|
|
||||||
|
/** Filter has no contexts */
|
||||||
|
NO_CONTEXT,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Set of validation errors for this filter, empty set if there
|
||||||
|
* are no errors.
|
||||||
|
*/
|
||||||
|
fun Filter.validate() = buildSet {
|
||||||
|
if (title.isBlank()) add(FilterValidationError.NO_TITLE)
|
||||||
|
if (keywords.isEmpty()) add(FilterValidationError.NO_KEYWORDS)
|
||||||
|
if (contexts.isEmpty()) add(FilterValidationError.NO_CONTEXT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return String resource containing an error message for this
|
||||||
|
* validation error.
|
||||||
|
*/
|
||||||
|
@StringRes
|
||||||
|
fun FilterValidationError.stringResource() = when (this) {
|
||||||
|
FilterValidationError.NO_TITLE -> R.string.error_filter_missing_title
|
||||||
|
FilterValidationError.NO_KEYWORDS -> R.string.error_filter_missing_keyword
|
||||||
|
FilterValidationError.NO_CONTEXT -> R.string.error_filter_missing_context
|
||||||
|
}
|
||||||
|
|
|
@ -6,11 +6,12 @@ import androidx.activity.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.core.activity.BaseActivity
|
import app.pachli.core.activity.BaseActivity
|
||||||
|
import app.pachli.core.activity.extensions.TransitionKind
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithTransition
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
import app.pachli.core.common.extensions.viewBinding
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
import app.pachli.core.common.extensions.visible
|
import app.pachli.core.common.extensions.visible
|
||||||
import app.pachli.core.designsystem.R as DR
|
|
||||||
import app.pachli.core.navigation.EditFilterActivityIntent
|
import app.pachli.core.navigation.EditFilterActivityIntent
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
import app.pachli.core.ui.BackgroundMessage
|
import app.pachli.core.ui.BackgroundMessage
|
||||||
|
@ -94,8 +95,7 @@ class FiltersActivity : BaseActivity(), FiltersListener {
|
||||||
|
|
||||||
private fun launchEditFilterActivity(filter: Filter? = null) {
|
private fun launchEditFilterActivity(filter: Filter? = null) {
|
||||||
val intent = EditFilterActivityIntent(this, filter)
|
val intent = EditFilterActivityIntent(this, filter)
|
||||||
startActivity(intent)
|
startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||||
overridePendingTransition(DR.anim.slide_from_right, DR.anim.slide_to_left)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteFilter(filter: Filter) {
|
override fun deleteFilter(filter: Filter) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import app.pachli.core.network.model.Filter
|
||||||
import app.pachli.core.ui.BindingHolder
|
import app.pachli.core.ui.BindingHolder
|
||||||
import app.pachli.databinding.ItemRemovableBinding
|
import app.pachli.databinding.ItemRemovableBinding
|
||||||
import app.pachli.util.getRelativeTimeSpanString
|
import app.pachli.util.getRelativeTimeSpanString
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
|
||||||
class FiltersAdapter(val listener: FiltersListener, val filters: List<Filter>) :
|
class FiltersAdapter(val listener: FiltersListener, val filters: List<Filter>) :
|
||||||
RecyclerView.Adapter<BindingHolder<ItemRemovableBinding>>() {
|
RecyclerView.Adapter<BindingHolder<ItemRemovableBinding>>() {
|
||||||
|
@ -34,11 +35,25 @@ class FiltersAdapter(val listener: FiltersListener, val filters: List<Filter>) :
|
||||||
)
|
)
|
||||||
} ?: filter.title
|
} ?: filter.title
|
||||||
|
|
||||||
binding.textSecondary.text = context.getString(
|
// Secondary row shows filter actions and contexts, or errors if the filter is invalid
|
||||||
R.string.filter_description_format,
|
val errors = filter.validate()
|
||||||
actions.getOrNull(filter.action.ordinal - 1),
|
val secondaryText: String
|
||||||
filter.contexts.map { filterContextNames.getOrNull(it.ordinal) }.joinToString("/"),
|
val secondaryTextColor: Int
|
||||||
)
|
|
||||||
|
if (errors.isEmpty()) {
|
||||||
|
secondaryText = context.getString(
|
||||||
|
R.string.filter_description_format,
|
||||||
|
actions.getOrNull(filter.action.ordinal - 1),
|
||||||
|
filter.contexts.map { filterContextNames.getOrNull(it.ordinal) }.joinToString("/"),
|
||||||
|
)
|
||||||
|
secondaryTextColor = android.R.attr.textColorTertiary
|
||||||
|
} else {
|
||||||
|
secondaryText = context.getString(errors.first().stringResource())
|
||||||
|
secondaryTextColor = androidx.appcompat.R.attr.colorError
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.textSecondary.text = secondaryText
|
||||||
|
binding.textSecondary.setTextColor(MaterialColors.getColor(binding.textSecondary, secondaryTextColor))
|
||||||
|
|
||||||
binding.delete.setOnClickListener {
|
binding.delete.setOnClickListener {
|
||||||
listener.deleteFilter(filter)
|
listener.deleteFilter(filter)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.components.compose.ComposeAutoCompleteAdapter
|
import app.pachli.components.compose.ComposeAutoCompleteAdapter
|
||||||
import app.pachli.core.activity.BaseActivity
|
import app.pachli.core.activity.BaseActivity
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
import app.pachli.core.common.extensions.viewBinding
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
|
@ -172,7 +173,7 @@ class FollowedTagsActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewTag(tag: String) {
|
override fun onViewTag(tag: String) {
|
||||||
startActivityWithSlideInAnimation(TimelineActivityIntent.hashtag(this, tag))
|
startActivityWithDefaultTransition(TimelineActivityIntent.hashtag(this, tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
override suspend fun search(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
||||||
|
|
|
@ -26,7 +26,8 @@ import app.pachli.R
|
||||||
import app.pachli.appstore.EventHub
|
import app.pachli.appstore.EventHub
|
||||||
import app.pachli.components.notifications.currentAccountNeedsMigration
|
import app.pachli.components.notifications.currentAccountNeedsMigration
|
||||||
import app.pachli.core.accounts.AccountManager
|
import app.pachli.core.accounts.AccountManager
|
||||||
import app.pachli.core.activity.BaseActivity
|
import app.pachli.core.activity.extensions.TransitionKind
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithTransition
|
||||||
import app.pachli.core.common.util.unsafeLazy
|
import app.pachli.core.common.util.unsafeLazy
|
||||||
import app.pachli.core.designsystem.R as DR
|
import app.pachli.core.designsystem.R as DR
|
||||||
import app.pachli.core.navigation.AccountListActivityIntent
|
import app.pachli.core.navigation.AccountListActivityIntent
|
||||||
|
@ -104,11 +105,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
setIcon(R.drawable.ic_add_to_tab_24)
|
setIcon(R.drawable.ic_add_to_tab_24)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
val intent = TabPreferenceActivityIntent(context)
|
val intent = TabPreferenceActivityIntent(context)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||||
activity?.overridePendingTransition(
|
|
||||||
DR.anim.slide_from_right,
|
|
||||||
DR.anim.slide_to_left,
|
|
||||||
)
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,11 +115,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
setIcon(R.drawable.ic_hashtag)
|
setIcon(R.drawable.ic_hashtag)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
val intent = FollowedTagsActivityIntent(context)
|
val intent = FollowedTagsActivityIntent(context)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||||
activity?.overridePendingTransition(
|
|
||||||
DR.anim.slide_from_right,
|
|
||||||
DR.anim.slide_to_left,
|
|
||||||
)
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,11 +125,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
setIcon(R.drawable.ic_mute_24dp)
|
setIcon(R.drawable.ic_mute_24dp)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.MUTES)
|
val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.MUTES)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||||
activity?.overridePendingTransition(
|
|
||||||
DR.anim.slide_from_right,
|
|
||||||
DR.anim.slide_to_left,
|
|
||||||
)
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,11 +135,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
icon = makeIcon(GoogleMaterial.Icon.gmd_block)
|
icon = makeIcon(GoogleMaterial.Icon.gmd_block)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.BLOCKS)
|
val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.BLOCKS)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||||
activity?.overridePendingTransition(
|
|
||||||
DR.anim.slide_from_right,
|
|
||||||
DR.anim.slide_to_left,
|
|
||||||
)
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,11 +145,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
setIcon(R.drawable.ic_mute_24dp)
|
setIcon(R.drawable.ic_mute_24dp)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
val intent = InstanceListActivityIntent(context)
|
val intent = InstanceListActivityIntent(context)
|
||||||
activity?.startActivity(intent)
|
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||||
activity?.overridePendingTransition(
|
|
||||||
DR.anim.slide_from_right,
|
|
||||||
DR.anim.slide_to_left,
|
|
||||||
)
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,7 +156,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
setIcon(R.drawable.ic_logout)
|
setIcon(R.drawable.ic_logout)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
val intent = LoginActivityIntent(context, LoginMode.MIGRATION)
|
val intent = LoginActivityIntent(context, LoginMode.MIGRATION)
|
||||||
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
activity?.startActivityWithTransition(intent, TransitionKind.EXPLODE)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,7 +166,8 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
setTitle(R.string.pref_title_timeline_filters)
|
setTitle(R.string.pref_title_timeline_filters)
|
||||||
setIcon(R.drawable.ic_filter_24dp)
|
setIcon(R.drawable.ic_filter_24dp)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
launchFilterActivity()
|
val intent = FiltersActivityIntent(requireContext())
|
||||||
|
activity?.startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
val server = serverRepository.flow.value.getOrElse { null }
|
val server = serverRepository.flow.value.getOrElse { null }
|
||||||
|
@ -298,14 +280,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.action = "android.settings.APP_NOTIFICATION_SETTINGS"
|
intent.action = "android.settings.APP_NOTIFICATION_SETTINGS"
|
||||||
intent.putExtra("android.provider.extra.APP_PACKAGE", BuildConfig.APPLICATION_ID)
|
intent.putExtra("android.provider.extra.APP_PACKAGE", BuildConfig.APPLICATION_ID)
|
||||||
startActivity(intent)
|
requireActivity().startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||||
} else {
|
} else {
|
||||||
activity?.let {
|
val intent = PreferencesActivityIntent(requireContext(), PreferenceScreen.NOTIFICATION)
|
||||||
val intent =
|
requireActivity().startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||||
PreferencesActivityIntent(it, PreferenceScreen.NOTIFICATION)
|
|
||||||
it.startActivity(intent)
|
|
||||||
it.overridePendingTransition(DR.anim.slide_from_right, DR.anim.slide_to_left)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -356,12 +334,6 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchFilterActivity() {
|
|
||||||
val intent = FiltersActivityIntent(requireContext())
|
|
||||||
activity?.startActivity(intent)
|
|
||||||
activity?.overridePendingTransition(DR.anim.slide_from_right, DR.anim.slide_to_left)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance() = AccountPreferencesFragment()
|
fun newInstance() = AccountPreferencesFragment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,8 @@ import androidx.preference.PreferenceFragmentCompat
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.appstore.EventHub
|
import app.pachli.appstore.EventHub
|
||||||
import app.pachli.core.activity.BaseActivity
|
import app.pachli.core.activity.BaseActivity
|
||||||
import app.pachli.core.designsystem.R as DR
|
import app.pachli.core.activity.extensions.TransitionKind
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.navigation.MainActivityIntent
|
import app.pachli.core.navigation.MainActivityIntent
|
||||||
import app.pachli.core.navigation.PreferencesActivityIntent
|
import app.pachli.core.navigation.PreferencesActivityIntent
|
||||||
import app.pachli.core.navigation.PreferencesActivityIntent.PreferenceScreen
|
import app.pachli.core.navigation.PreferencesActivityIntent.PreferenceScreen
|
||||||
|
@ -61,7 +62,7 @@ class PreferencesActivity :
|
||||||
* back stack. */
|
* back stack. */
|
||||||
val intent = MainActivityIntent(this@PreferencesActivity)
|
val intent = MainActivityIntent(this@PreferencesActivity)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,11 +108,11 @@ class PreferencesActivity :
|
||||||
setAppNightMode(theme)
|
setAppNightMode(theme)
|
||||||
|
|
||||||
restartActivitiesOnBackPressedCallback.isEnabled = true
|
restartActivitiesOnBackPressedCallback.isEnabled = true
|
||||||
this@PreferencesActivity.restartCurrentActivity()
|
this@PreferencesActivity.recreate()
|
||||||
}
|
}
|
||||||
PrefKeys.FONT_FAMILY, PrefKeys.UI_TEXT_SCALE_RATIO -> {
|
PrefKeys.FONT_FAMILY, PrefKeys.UI_TEXT_SCALE_RATIO -> {
|
||||||
restartActivitiesOnBackPressedCallback.isEnabled = true
|
restartActivitiesOnBackPressedCallback.isEnabled = true
|
||||||
this@PreferencesActivity.restartCurrentActivity()
|
this@PreferencesActivity.recreate()
|
||||||
}
|
}
|
||||||
PrefKeys.STATUS_TEXT_SIZE, PrefKeys.ABSOLUTE_TIME_VIEW, PrefKeys.SHOW_BOT_OVERLAY, PrefKeys.ANIMATE_GIF_AVATARS, PrefKeys.USE_BLURHASH,
|
PrefKeys.STATUS_TEXT_SIZE, PrefKeys.ABSOLUTE_TIME_VIEW, PrefKeys.SHOW_BOT_OVERLAY, PrefKeys.ANIMATE_GIF_AVATARS, PrefKeys.USE_BLURHASH,
|
||||||
PrefKeys.SHOW_SELF_USERNAME, PrefKeys.SHOW_CARDS_IN_TIMELINES, PrefKeys.CONFIRM_REBLOGS, PrefKeys.CONFIRM_FAVOURITES,
|
PrefKeys.SHOW_SELF_USERNAME, PrefKeys.SHOW_CARDS_IN_TIMELINES, PrefKeys.CONFIRM_REBLOGS, PrefKeys.CONFIRM_FAVOURITES,
|
||||||
|
@ -136,11 +137,13 @@ class PreferencesActivity :
|
||||||
fragment.arguments = args
|
fragment.arguments = args
|
||||||
fragment.setTargetFragment(caller, 0)
|
fragment.setTargetFragment(caller, 0)
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
|
// Slide transition, as sub preference screens are "attached" to the
|
||||||
|
// parent screen.
|
||||||
setCustomAnimations(
|
setCustomAnimations(
|
||||||
DR.anim.slide_from_right,
|
TransitionKind.SLIDE_FROM_END.openEnter,
|
||||||
DR.anim.slide_to_left,
|
TransitionKind.SLIDE_FROM_END.openExit,
|
||||||
DR.anim.slide_from_left,
|
TransitionKind.SLIDE_FROM_END.closeEnter,
|
||||||
DR.anim.slide_to_right,
|
TransitionKind.SLIDE_FROM_END.closeExit,
|
||||||
)
|
)
|
||||||
replace(R.id.fragment_container, fragment)
|
replace(R.id.fragment_container, fragment)
|
||||||
addToBackStack(null)
|
addToBackStack(null)
|
||||||
|
@ -148,25 +151,11 @@ class PreferencesActivity :
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveInstanceState(outState: Bundle) {
|
|
||||||
outState.putBoolean(EXTRA_RESTART_ON_BACK, restartActivitiesOnBackPressedCallback.isEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putBoolean(EXTRA_RESTART_ON_BACK, restartActivitiesOnBackPressedCallback.isEnabled)
|
outState.putBoolean(EXTRA_RESTART_ON_BACK, restartActivitiesOnBackPressedCallback.isEnabled)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restartCurrentActivity() {
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
val savedInstanceState = Bundle()
|
|
||||||
saveInstanceState(savedInstanceState)
|
|
||||||
intent.putExtras(savedInstanceState)
|
|
||||||
startActivityWithSlideInAnimation(intent)
|
|
||||||
finish()
|
|
||||||
overridePendingTransition(DR.anim.fade_in, DR.anim.fade_out)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val EXTRA_RESTART_ON_BACK = "restart"
|
private const val EXTRA_RESTART_ON_BACK = "restart"
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,10 +88,6 @@ class SearchActivity : BottomSheetActivity(), MenuProvider, SearchView.OnQueryTe
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finish() {
|
|
||||||
super.finishWithoutSlideOutAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPageTitle(position: Int): CharSequence {
|
private fun getPageTitle(position: Int): CharSequence {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> getString(R.string.title_posts)
|
0 -> getString(R.string.title_posts)
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.components.search.SearchViewModel
|
import app.pachli.components.search.SearchViewModel
|
||||||
import app.pachli.core.activity.BottomSheetActivity
|
import app.pachli.core.activity.BottomSheetActivity
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.common.extensions.viewBinding
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
import app.pachli.core.common.extensions.visible
|
import app.pachli.core.common.extensions.visible
|
||||||
import app.pachli.core.navigation.AccountActivityIntent
|
import app.pachli.core.navigation.AccountActivityIntent
|
||||||
|
@ -140,11 +141,11 @@ abstract class SearchFragment<T : Any> :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAccount(id: String) {
|
override fun onViewAccount(id: String) {
|
||||||
bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivityIntent(requireContext(), id))
|
bottomSheetActivity?.startActivityWithDefaultTransition(AccountActivityIntent(requireContext(), id))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewTag(tag: String) {
|
override fun onViewTag(tag: String) {
|
||||||
bottomSheetActivity?.startActivityWithSlideInAnimation(TimelineActivityIntent.hashtag(requireContext(), tag))
|
bottomSheetActivity?.startActivityWithDefaultTransition(TimelineActivityIntent.hashtag(requireContext(), tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewUrl(url: String) {
|
override fun onViewUrl(url: String) {
|
||||||
|
|
|
@ -40,6 +40,7 @@ import app.pachli.R
|
||||||
import app.pachli.components.search.adapter.SearchStatusesAdapter
|
import app.pachli.components.search.adapter.SearchStatusesAdapter
|
||||||
import app.pachli.core.activity.AccountSelectionListener
|
import app.pachli.core.activity.AccountSelectionListener
|
||||||
import app.pachli.core.activity.BaseActivity
|
import app.pachli.core.activity.BaseActivity
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.activity.openLink
|
import app.pachli.core.activity.openLink
|
||||||
import app.pachli.core.database.model.AccountEntity
|
import app.pachli.core.database.model.AccountEntity
|
||||||
import app.pachli.core.navigation.AttachmentViewData
|
import app.pachli.core.navigation.AttachmentViewData
|
||||||
|
@ -187,7 +188,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData>(), StatusActionLis
|
||||||
kind = ComposeOptions.ComposeKind.NEW,
|
kind = ComposeOptions.ComposeKind.NEW,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
bottomSheetActivity?.startActivityWithSlideInAnimation(intent)
|
bottomSheetActivity?.startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun more(statusViewData: StatusViewData, view: View) {
|
private fun more(statusViewData: StatusViewData, view: View) {
|
||||||
|
|
|
@ -49,8 +49,8 @@ import app.pachli.components.timeline.viewmodel.StatusAction
|
||||||
import app.pachli.components.timeline.viewmodel.StatusActionSuccess
|
import app.pachli.components.timeline.viewmodel.StatusActionSuccess
|
||||||
import app.pachli.components.timeline.viewmodel.TimelineViewModel
|
import app.pachli.components.timeline.viewmodel.TimelineViewModel
|
||||||
import app.pachli.components.timeline.viewmodel.UiSuccess
|
import app.pachli.components.timeline.viewmodel.UiSuccess
|
||||||
import app.pachli.core.activity.BaseActivity
|
|
||||||
import app.pachli.core.activity.RefreshableFragment
|
import app.pachli.core.activity.RefreshableFragment
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
import app.pachli.core.common.extensions.viewBinding
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
|
@ -637,12 +637,12 @@ class TimelineFragment :
|
||||||
|
|
||||||
override fun onShowReblogs(statusId: String) {
|
override fun onShowReblogs(statusId: String) {
|
||||||
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.REBLOGGED, statusId)
|
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.REBLOGGED, statusId)
|
||||||
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
activity?.startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowFavs(statusId: String) {
|
override fun onShowFavs(statusId: String) {
|
||||||
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.FAVOURITED, statusId)
|
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.FAVOURITED, statusId)
|
||||||
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
activity?.startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentCollapsedChange(viewData: StatusViewData, isCollapsed: Boolean) {
|
override fun onContentCollapsedChange(viewData: StatusViewData, isCollapsed: Boolean) {
|
||||||
|
|
|
@ -35,8 +35,11 @@ import app.pachli.core.model.Timeline
|
||||||
import app.pachli.core.ui.extensions.reduceSwipeSensitivity
|
import app.pachli.core.ui.extensions.reduceSwipeSensitivity
|
||||||
import app.pachli.databinding.ActivityTrendingBinding
|
import app.pachli.databinding.ActivityTrendingBinding
|
||||||
import app.pachli.interfaces.AppBarLayoutHost
|
import app.pachli.interfaces.AppBarLayoutHost
|
||||||
|
import app.pachli.interfaces.ReselectableFragment
|
||||||
import app.pachli.pager.MainPagerAdapter
|
import app.pachli.pager.MainPagerAdapter
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -55,6 +58,12 @@ class TrendingActivity : BottomSheetActivity(), AppBarLayoutHost, MenuProvider {
|
||||||
|
|
||||||
private lateinit var adapter: MainPagerAdapter
|
private lateinit var adapter: MainPagerAdapter
|
||||||
|
|
||||||
|
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
binding.pager.currentItem = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
@ -90,14 +99,19 @@ class TrendingActivity : BottomSheetActivity(), AppBarLayoutHost, MenuProvider {
|
||||||
}
|
}
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(
|
binding.tabLayout.addOnTabSelectedListener(object : OnTabSelectedListener {
|
||||||
this,
|
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||||
object : OnBackPressedCallback(true) {
|
onBackPressedCallback.isEnabled = tab.position > 0
|
||||||
override fun handleOnBackPressed() {
|
}
|
||||||
if (binding.pager.currentItem != 0) binding.pager.currentItem = 0 else finish()
|
|
||||||
}
|
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||||
},
|
|
||||||
)
|
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||||
|
(adapter.getFragment(tab.position) as? ReselectableFragment)?.onReselect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
|
|
@ -37,8 +37,8 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.components.trending.viewmodel.TrendingTagsViewModel
|
import app.pachli.components.trending.viewmodel.TrendingTagsViewModel
|
||||||
import app.pachli.core.activity.BaseActivity
|
|
||||||
import app.pachli.core.activity.RefreshableFragment
|
import app.pachli.core.activity.RefreshableFragment
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
import app.pachli.core.common.extensions.viewBinding
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
|
@ -173,7 +173,7 @@ class TrendingTagsFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onViewTag(tag: String) {
|
fun onViewTag(tag: String) {
|
||||||
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(
|
activity?.startActivityWithDefaultTransition(
|
||||||
TimelineActivityIntent.hashtag(
|
TimelineActivityIntent.hashtag(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
tag,
|
tag,
|
||||||
|
|
|
@ -33,7 +33,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.components.viewthread.edits.ViewEditsFragment
|
import app.pachli.components.viewthread.edits.ViewEditsFragment
|
||||||
import app.pachli.core.activity.BaseActivity
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.activity.openLink
|
import app.pachli.core.activity.openLink
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
|
@ -337,12 +337,12 @@ class ViewThreadFragment :
|
||||||
|
|
||||||
override fun onShowReblogs(statusId: String) {
|
override fun onShowReblogs(statusId: String) {
|
||||||
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.REBLOGGED, statusId)
|
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.REBLOGGED, statusId)
|
||||||
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent)
|
activity?.startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowFavs(statusId: String) {
|
override fun onShowFavs(statusId: String) {
|
||||||
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.FAVOURITED, statusId)
|
val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.FAVOURITED, statusId)
|
||||||
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent)
|
activity?.startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentCollapsedChange(viewData: StatusViewData, isCollapsed: Boolean) {
|
override fun onContentCollapsedChange(viewData: StatusViewData, isCollapsed: Boolean) {
|
||||||
|
@ -374,7 +374,12 @@ class ViewThreadFragment :
|
||||||
val viewEditsFragment = ViewEditsFragment.newInstance(statusId)
|
val viewEditsFragment = ViewEditsFragment.newInstance(statusId)
|
||||||
|
|
||||||
parentFragmentManager.commit {
|
parentFragmentManager.commit {
|
||||||
setCustomAnimations(DR.anim.slide_from_right, DR.anim.slide_to_left, DR.anim.slide_from_left, DR.anim.slide_to_right)
|
setCustomAnimations(
|
||||||
|
DR.anim.activity_open_enter,
|
||||||
|
DR.anim.activity_open_exit,
|
||||||
|
DR.anim.activity_close_enter,
|
||||||
|
DR.anim.activity_close_exit,
|
||||||
|
)
|
||||||
replace(R.id.fragment_container, viewEditsFragment, "ViewEditsFragment_$id")
|
replace(R.id.fragment_container, viewEditsFragment, "ViewEditsFragment_$id")
|
||||||
addToBackStack(null)
|
addToBackStack(null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.core.activity.BottomSheetActivity
|
import app.pachli.core.activity.BottomSheetActivity
|
||||||
import app.pachli.core.activity.emojify
|
import app.pachli.core.activity.emojify
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.activity.loadAvatar
|
import app.pachli.core.activity.loadAvatar
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
import app.pachli.core.common.extensions.show
|
import app.pachli.core.common.extensions.show
|
||||||
|
@ -183,11 +184,11 @@ class ViewEditsFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAccount(id: String) {
|
override fun onViewAccount(id: String) {
|
||||||
bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivityIntent(requireContext(), id))
|
bottomSheetActivity?.startActivityWithDefaultTransition(AccountActivityIntent(requireContext(), id))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewTag(tag: String) {
|
override fun onViewTag(tag: String) {
|
||||||
bottomSheetActivity?.startActivityWithSlideInAnimation(TimelineActivityIntent.hashtag(requireContext(), tag))
|
bottomSheetActivity?.startActivityWithDefaultTransition(TimelineActivityIntent.hashtag(requireContext(), tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewUrl(url: String) {
|
override fun onViewUrl(url: String) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ import androidx.annotation.CallSuper
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
@ -44,10 +45,10 @@ import app.pachli.core.activity.AccountSelectionListener
|
||||||
import app.pachli.core.activity.BaseActivity
|
import app.pachli.core.activity.BaseActivity
|
||||||
import app.pachli.core.activity.BottomSheetActivity
|
import app.pachli.core.activity.BottomSheetActivity
|
||||||
import app.pachli.core.activity.PostLookupFallbackBehavior
|
import app.pachli.core.activity.PostLookupFallbackBehavior
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.activity.openLink
|
import app.pachli.core.activity.openLink
|
||||||
import app.pachli.core.database.model.AccountEntity
|
import app.pachli.core.database.model.AccountEntity
|
||||||
import app.pachli.core.database.model.TranslationState
|
import app.pachli.core.database.model.TranslationState
|
||||||
import app.pachli.core.designsystem.R as DR
|
|
||||||
import app.pachli.core.navigation.AttachmentViewData
|
import app.pachli.core.navigation.AttachmentViewData
|
||||||
import app.pachli.core.navigation.ComposeActivityIntent
|
import app.pachli.core.navigation.ComposeActivityIntent
|
||||||
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
|
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
|
||||||
|
@ -94,8 +95,7 @@ abstract class SFragment<T : IStatusViewData> : Fragment(), StatusActionListener
|
||||||
private var serverCanTranslate = false
|
private var serverCanTranslate = false
|
||||||
|
|
||||||
override fun startActivity(intent: Intent) {
|
override fun startActivity(intent: Intent) {
|
||||||
super.startActivity(intent)
|
requireActivity().startActivityWithDefaultTransition(intent)
|
||||||
requireActivity().overridePendingTransition(DR.anim.slide_from_right, DR.anim.slide_to_left)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
|
@ -400,7 +400,7 @@ abstract class SFragment<T : IStatusViewData> : Fragment(), StatusActionListener
|
||||||
val intent = ViewMediaActivityIntent(requireContext(), attachments, urlIndex)
|
val intent = ViewMediaActivityIntent(requireContext(), attachments, urlIndex)
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
val url = attachment.url
|
val url = attachment.url
|
||||||
view.transitionName = url
|
ViewCompat.setTransitionName(view, url)
|
||||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
view,
|
view,
|
||||||
|
|
|
@ -30,6 +30,7 @@ import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.GestureDetectorCompat
|
import androidx.core.view.GestureDetectorCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
@ -62,7 +63,7 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
private var scheduleToolbarHide = false
|
private var scheduleToolbarHide = false
|
||||||
|
|
||||||
override fun setupMediaView(showingDescription: Boolean) {
|
override fun setupMediaView(showingDescription: Boolean) {
|
||||||
binding.photoView.transitionName = attachment.url
|
ViewCompat.setTransitionName(binding.photoView, attachment.url)
|
||||||
binding.mediaDescription.text = attachment.description
|
binding.mediaDescription.text = attachment.description
|
||||||
binding.captionSheet.visible(showingDescription)
|
binding.captionSheet.visible(showingDescription)
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import android.widget.FrameLayout
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.view.GestureDetectorCompat
|
import androidx.core.view.GestureDetectorCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
|
@ -359,7 +360,7 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
// Ensure the description is visible over the video
|
// Ensure the description is visible over the video
|
||||||
binding.mediaDescription.elevation = binding.videoView.elevation + 1
|
binding.mediaDescription.elevation = binding.videoView.elevation + 1
|
||||||
|
|
||||||
binding.videoView.transitionName = attachment.url
|
ViewCompat.setTransitionName(binding.videoView, attachment.url)
|
||||||
|
|
||||||
if (!startedTransition && shouldCallMediaReady) {
|
if (!startedTransition && shouldCallMediaReady) {
|
||||||
startedTransition = true
|
startedTransition = true
|
||||||
|
|
|
@ -16,14 +16,10 @@
|
||||||
|
|
||||||
package app.pachli.service
|
package app.pachli.service
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import app.pachli.components.notifications.pendingIntentFlags
|
|
||||||
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
|
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
|
||||||
import app.pachli.core.navigation.MainActivityIntent
|
import app.pachli.core.navigation.MainActivityIntent
|
||||||
|
|
||||||
|
@ -33,21 +29,14 @@ import app.pachli.core.navigation.MainActivityIntent
|
||||||
*/
|
*/
|
||||||
@TargetApi(24)
|
@TargetApi(24)
|
||||||
class PachliTileService : TileService() {
|
class PachliTileService : TileService() {
|
||||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
val intent = MainActivityIntent.openCompose(this, ComposeOptions())
|
val intent = MainActivityIntent.openCompose(this, ComposeOptions())
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
startActivityAndCollapse(getActivityPendingIntent(this, 0, intent))
|
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
startActivityAndCollapse(pendingIntent)
|
||||||
} else {
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
startActivityAndCollapse(intent)
|
startActivityAndCollapse(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getActivityPendingIntent(context: Context, requestCode: Int, intent: Intent): PendingIntent {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
PendingIntent.getActivity(context, requestCode, intent, pendingIntentFlags(false))
|
|
||||||
} else {
|
|
||||||
PendingIntent.getActivity(context, requestCode, intent, pendingIntentFlags(false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
@ -77,6 +79,11 @@ class EditProfileViewModel @Inject constructor(
|
||||||
|
|
||||||
private var apiProfileAccount: Account? = null
|
private var apiProfileAccount: Account? = null
|
||||||
|
|
||||||
|
private val _isDirty = MutableStateFlow(false)
|
||||||
|
|
||||||
|
/** True if the user has made unsaved changes to the profile */
|
||||||
|
val isDirty = _isDirty.asStateFlow()
|
||||||
|
|
||||||
fun obtainProfile() = viewModelScope.launch {
|
fun obtainProfile() = viewModelScope.launch {
|
||||||
if (profileData.value == null || profileData.value is Error) {
|
if (profileData.value == null || profileData.value is Error) {
|
||||||
profileData.postValue(Loading())
|
profileData.postValue(Loading())
|
||||||
|
@ -170,10 +177,8 @@ class EditProfileViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun hasUnsavedChanges(newProfileData: ProfileDataInUi): Boolean {
|
internal fun onChange(newProfileData: ProfileDataInUi) {
|
||||||
val diff = getProfileDiff(apiProfileAccount, newProfileData)
|
_isDirty.value = getProfileDiff(apiProfileAccount, newProfileData).hasChanges()
|
||||||
|
|
||||||
return diff.hasChanges()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getProfileDiff(oldProfileAccount: Account?, newProfileData: ProfileDataInUi): DiffProfileData {
|
private fun getProfileDiff(oldProfileAccount: Account?, newProfileData: ProfileDataInUi): DiffProfileData {
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
|
app:errorEnabled="true"
|
||||||
android:hint="@string/label_filter_title">
|
android:hint="@string/label_filter_title">
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/filterTitle"
|
android:id="@+id/filterTitle"
|
||||||
|
@ -43,6 +44,15 @@
|
||||||
style="@style/TextAppearance.Material3.TitleSmall"
|
style="@style/TextAppearance.Material3.TitleSmall"
|
||||||
android:textColor="?attr/colorAccent" />
|
android:textColor="?attr/colorAccent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/keywordChipsError"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/error_filter_missing_keyword"
|
||||||
|
style="@style/TextAppearance.Material3.BodySmall"
|
||||||
|
android:textColor="?attr/colorError" />
|
||||||
|
|
||||||
<com.google.android.material.chip.ChipGroup
|
<com.google.android.material.chip.ChipGroup
|
||||||
android:id="@+id/keywordChips"
|
android:id="@+id/keywordChips"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -100,8 +110,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:entries="@array/filter_duration_names"
|
android:entries="@array/filter_duration_names" />
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -146,6 +155,15 @@
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:text="@string/pref_title_account_filter_keywords" />
|
android:text="@string/pref_title_account_filter_keywords" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/filterContextError"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/error_filter_missing_context"
|
||||||
|
style="@style/TextAppearance.Material3.BodySmall"
|
||||||
|
android:textColor="?attr/colorError" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -503,7 +503,7 @@
|
||||||
<string name="notification_report_description">ارسال إشعار عن شكاوى المدراء</string>
|
<string name="notification_report_description">ارسال إشعار عن شكاوى المدراء</string>
|
||||||
<string name="set_focus_description">اضغط على الدائرة أو اسحبها لاختيار النقطة المحورية التي ستكون مرئية دائمًا في الصور المصغرة.</string>
|
<string name="set_focus_description">اضغط على الدائرة أو اسحبها لاختيار النقطة المحورية التي ستكون مرئية دائمًا في الصور المصغرة.</string>
|
||||||
<string name="compose_save_draft_loses_media">حفظ المسودة؟ (سيتم رفع المرفقات مرة أخرى عند استعادة المسودة.)</string>
|
<string name="compose_save_draft_loses_media">حفظ المسودة؟ (سيتم رفع المرفقات مرة أخرى عند استعادة المسودة.)</string>
|
||||||
<string name="compose_unsaved_changes">لديك تعديلات لم تحفظ.</string>
|
<string name="unsaved_changes">لديك تعديلات لم تحفظ.</string>
|
||||||
<string name="post_edited">عدَّلَ %s</string>
|
<string name="post_edited">عدَّلَ %s</string>
|
||||||
<string name="notification_report_format">شكوى جديدة عن %s</string>
|
<string name="notification_report_format">شكوى جديدة عن %s</string>
|
||||||
<string name="status_edit_info">%1$s :عدّله</string>
|
<string name="status_edit_info">%1$s :عدّله</string>
|
||||||
|
|
|
@ -374,7 +374,7 @@
|
||||||
<item quantity="many"><b>%s</b> пашыраных</item>
|
<item quantity="many"><b>%s</b> пашыраных</item>
|
||||||
<item quantity="other"><b>%s</b> пашыраных</item>
|
<item quantity="other"><b>%s</b> пашыраных</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="compose_unsaved_changes">У Вас засталіся незахаваныя змены.</string>
|
<string name="unsaved_changes">У Вас засталіся незахаваныя змены.</string>
|
||||||
<string name="expand_collapse_all_posts">Разгарнуць/згарнуць допісы</string>
|
<string name="expand_collapse_all_posts">Разгарнуць/згарнуць допісы</string>
|
||||||
<string name="action_open_post">Адкрыць допіс</string>
|
<string name="action_open_post">Адкрыць допіс</string>
|
||||||
<string name="restart_required">Патрэбна перазапусціць праграму</string>
|
<string name="restart_required">Патрэбна перазапусціць праграму</string>
|
||||||
|
|
|
@ -508,7 +508,7 @@
|
||||||
<string name="pachli_compose_post_quicksetting_label">Redacta la publicació</string>
|
<string name="pachli_compose_post_quicksetting_label">Redacta la publicació</string>
|
||||||
<string name="pref_title_confirm_favourites">Mostra el diàleg de confirmació abans de marcar com a preferit</string>
|
<string name="pref_title_confirm_favourites">Mostra el diàleg de confirmació abans de marcar com a preferit</string>
|
||||||
<string name="action_unfollow_hashtag_format">Deixar de seguir #%s\?</string>
|
<string name="action_unfollow_hashtag_format">Deixar de seguir #%s\?</string>
|
||||||
<string name="compose_unsaved_changes">Tens canvis no desats.</string>
|
<string name="unsaved_changes">Tens canvis no desats.</string>
|
||||||
<string name="set_focus_description">Toqueu o arrossegueu el cercle per triar el punt focal que sempre serà visible a les miniatures.</string>
|
<string name="set_focus_description">Toqueu o arrossegueu el cercle per triar el punt focal que sempre serà visible a les miniatures.</string>
|
||||||
<string name="url_domain_notifier">\u0020(🔗 %s)</string>
|
<string name="url_domain_notifier">\u0020(🔗 %s)</string>
|
||||||
<string name="pref_title_show_self_username">Mostra el nom d\'usuari a les barres d\'eines</string>
|
<string name="pref_title_show_self_username">Mostra el nom d\'usuari a les barres d\'eines</string>
|
||||||
|
|
|
@ -563,7 +563,7 @@
|
||||||
<string name="title_edits">Golygiadau</string>
|
<string name="title_edits">Golygiadau</string>
|
||||||
<string name="post_media_alt">AMGEN</string>
|
<string name="post_media_alt">AMGEN</string>
|
||||||
<string name="action_discard">Hepgor newidiadau</string>
|
<string name="action_discard">Hepgor newidiadau</string>
|
||||||
<string name="compose_unsaved_changes">Mae gennych newidiadau heb eu cadw.</string>
|
<string name="unsaved_changes">Mae gennych newidiadau heb eu cadw.</string>
|
||||||
<string name="mute_notifications_switch">Tewi hysbysiadau</string>
|
<string name="mute_notifications_switch">Tewi hysbysiadau</string>
|
||||||
<string name="a11y_label_loading_thread">Llwytho trywydd</string>
|
<string name="a11y_label_loading_thread">Llwytho trywydd</string>
|
||||||
<string name="action_share_account_link">Rhannu ddolen i gyfrif</string>
|
<string name="action_share_account_link">Rhannu ddolen i gyfrif</string>
|
||||||
|
|
|
@ -515,7 +515,7 @@
|
||||||
<string name="pref_summary_http_proxy_disabled">Deaktiviert</string>
|
<string name="pref_summary_http_proxy_disabled">Deaktiviert</string>
|
||||||
<string name="pref_summary_http_proxy_missing"><nicht gesetzt></string>
|
<string name="pref_summary_http_proxy_missing"><nicht gesetzt></string>
|
||||||
<string name="pref_summary_http_proxy_invalid"><ungültig></string>
|
<string name="pref_summary_http_proxy_invalid"><ungültig></string>
|
||||||
<string name="compose_unsaved_changes">Du hast nicht gespeicherte Änderungen.</string>
|
<string name="unsaved_changes">Du hast nicht gespeicherte Änderungen.</string>
|
||||||
<string name="pref_title_http_proxy_port_message">Port sollte zwischen %d und %d liegen</string>
|
<string name="pref_title_http_proxy_port_message">Port sollte zwischen %d und %d liegen</string>
|
||||||
<string name="error_muting_hashtag_format">Fehler beim Stummschalten von #%s</string>
|
<string name="error_muting_hashtag_format">Fehler beim Stummschalten von #%s</string>
|
||||||
<string name="action_post_failed">Hochladen fehlgeschlagen</string>
|
<string name="action_post_failed">Hochladen fehlgeschlagen</string>
|
||||||
|
|
|
@ -526,7 +526,7 @@
|
||||||
<string name="pref_title_notification_filter_reports">Hay una nueva denuncia</string>
|
<string name="pref_title_notification_filter_reports">Hay una nueva denuncia</string>
|
||||||
<string name="action_discard">Descartar cambios</string>
|
<string name="action_discard">Descartar cambios</string>
|
||||||
<string name="action_continue_edit">Continuar editando</string>
|
<string name="action_continue_edit">Continuar editando</string>
|
||||||
<string name="compose_unsaved_changes">Tienes cambios sin guardar.</string>
|
<string name="unsaved_changes">Tienes cambios sin guardar.</string>
|
||||||
<string name="title_edits">Ediciones</string>
|
<string name="title_edits">Ediciones</string>
|
||||||
<string name="action_post_failed">La subida falló</string>
|
<string name="action_post_failed">La subida falló</string>
|
||||||
<string name="action_post_failed_do_nothing">Descartar</string>
|
<string name="action_post_failed_do_nothing">Descartar</string>
|
||||||
|
@ -683,4 +683,4 @@
|
||||||
<string name="manage_lists">Gestionar listas</string>
|
<string name="manage_lists">Gestionar listas</string>
|
||||||
<string name="title_lists_loading">Listas - cargando…</string>
|
<string name="title_lists_loading">Listas - cargando…</string>
|
||||||
<string name="title_lists_failed">Listas - falló en cargar</string>
|
<string name="title_lists_failed">Listas - falló en cargar</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -534,7 +534,7 @@
|
||||||
<string name="description_browser_login">ممکن است از روشهای تأیید خویت اضافی پشتیبانی کند؛ ولی نیازمند مرورگری پشتیبانی شده است.</string>
|
<string name="description_browser_login">ممکن است از روشهای تأیید خویت اضافی پشتیبانی کند؛ ولی نیازمند مرورگری پشتیبانی شده است.</string>
|
||||||
<string name="action_discard">دور انداختن تغییرات</string>
|
<string name="action_discard">دور انداختن تغییرات</string>
|
||||||
<string name="action_continue_edit">ادامهٔ ویرایش</string>
|
<string name="action_continue_edit">ادامهٔ ویرایش</string>
|
||||||
<string name="compose_unsaved_changes">تغییراتی ذخیره نشده دارید.</string>
|
<string name="unsaved_changes">تغییراتی ذخیره نشده دارید.</string>
|
||||||
<string name="action_share_account_link">همرسانی پیوند به حساب</string>
|
<string name="action_share_account_link">همرسانی پیوند به حساب</string>
|
||||||
<string name="action_share_account_username">همرسانی نام کاربری حساب</string>
|
<string name="action_share_account_username">همرسانی نام کاربری حساب</string>
|
||||||
<string name="send_account_link_to">همرسانی نشانی حساب به…</string>
|
<string name="send_account_link_to">همرسانی نشانی حساب به…</string>
|
||||||
|
|
|
@ -376,7 +376,7 @@
|
||||||
<string name="pref_title_notification_filter_poll">päättyneet äänestykset</string>
|
<string name="pref_title_notification_filter_poll">päättyneet äänestykset</string>
|
||||||
<string name="abbreviated_in_seconds">%ds</string>
|
<string name="abbreviated_in_seconds">%ds</string>
|
||||||
<string name="failed_to_unpin">Irrottaminen epäonnistui</string>
|
<string name="failed_to_unpin">Irrottaminen epäonnistui</string>
|
||||||
<string name="compose_unsaved_changes">Sinulla on tallentamattomia muutoksia.</string>
|
<string name="unsaved_changes">Sinulla on tallentamattomia muutoksia.</string>
|
||||||
<string name="state_follow_requested">Seuraamista pyydetty</string>
|
<string name="state_follow_requested">Seuraamista pyydetty</string>
|
||||||
<string name="compose_delete_draft">Poista luonnos?</string>
|
<string name="compose_delete_draft">Poista luonnos?</string>
|
||||||
<string name="notification_notification_worker">Ilmoituksia haetaan…</string>
|
<string name="notification_notification_worker">Ilmoituksia haetaan…</string>
|
||||||
|
|
|
@ -568,7 +568,7 @@
|
||||||
<string name="description_login">Fonctionne dans la plupart des cas. Aucune autre application n\'aura accès à vos données.</string>
|
<string name="description_login">Fonctionne dans la plupart des cas. Aucune autre application n\'aura accès à vos données.</string>
|
||||||
<string name="description_browser_login">Peut permettre des méthodes d\'authentification supplémentaires, mais un navigateur compatible est nécessaire.</string>
|
<string name="description_browser_login">Peut permettre des méthodes d\'authentification supplémentaires, mais un navigateur compatible est nécessaire.</string>
|
||||||
<string name="mute_notifications_switch">Masquer les notifications</string>
|
<string name="mute_notifications_switch">Masquer les notifications</string>
|
||||||
<string name="compose_unsaved_changes">Il y a des modifications non enregistrées.</string>
|
<string name="unsaved_changes">Il y a des modifications non enregistrées.</string>
|
||||||
<string name="description_post_edited">Modifié</string>
|
<string name="description_post_edited">Modifié</string>
|
||||||
<string name="select_list_empty">Vous n\'avez pas encore de liste</string>
|
<string name="select_list_empty">Vous n\'avez pas encore de liste</string>
|
||||||
<string name="select_list_manage">Gérer les listes</string>
|
<string name="select_list_manage">Gérer les listes</string>
|
||||||
|
|
|
@ -548,7 +548,7 @@
|
||||||
<string name="notification_summary_report_format">%s · Tha postaichean ris, %d dhiubh</string>
|
<string name="notification_summary_report_format">%s · Tha postaichean ris, %d dhiubh</string>
|
||||||
<string name="action_share_account_link">Co-roinn ceangal dhan chunntas</string>
|
<string name="action_share_account_link">Co-roinn ceangal dhan chunntas</string>
|
||||||
<string name="account_username_copied">Chaidh lethbhreac a dhèanamh dhen ainm-chleachdaiche</string>
|
<string name="account_username_copied">Chaidh lethbhreac a dhèanamh dhen ainm-chleachdaiche</string>
|
||||||
<string name="compose_unsaved_changes">Tha atharraichean gun sàbhaladh agad.</string>
|
<string name="unsaved_changes">Tha atharraichean gun sàbhaladh agad.</string>
|
||||||
<string name="error_status_source_load">Dh’fhàillig luchdadh bun-tùs a’ phuist on fhrithealaiche.</string>
|
<string name="error_status_source_load">Dh’fhàillig luchdadh bun-tùs a’ phuist on fhrithealaiche.</string>
|
||||||
<string name="action_post_failed_detail">Dh’fhàillig luchdadh suas a’ phuist agad is chaidh a shàbhaladh ’na dhreachd.
|
<string name="action_post_failed_detail">Dh’fhàillig luchdadh suas a’ phuist agad is chaidh a shàbhaladh ’na dhreachd.
|
||||||
\n
|
\n
|
||||||
|
|
|
@ -514,7 +514,7 @@
|
||||||
<string name="status_created_info">Creado por %1$s</string>
|
<string name="status_created_info">Creado por %1$s</string>
|
||||||
<string name="action_discard">Desbotar cambios</string>
|
<string name="action_discard">Desbotar cambios</string>
|
||||||
<string name="action_continue_edit">Continuar a edición</string>
|
<string name="action_continue_edit">Continuar a edición</string>
|
||||||
<string name="compose_unsaved_changes">Hai cambios non gardados.</string>
|
<string name="unsaved_changes">Hai cambios non gardados.</string>
|
||||||
<string name="action_share_account_link">Comparte ligazón da conta</string>
|
<string name="action_share_account_link">Comparte ligazón da conta</string>
|
||||||
<string name="action_share_account_username">Comparte identificador da conta</string>
|
<string name="action_share_account_username">Comparte identificador da conta</string>
|
||||||
<string name="send_account_link_to">Compartir URL da conta en…</string>
|
<string name="send_account_link_to">Compartir URL da conta en…</string>
|
||||||
|
|
|
@ -524,7 +524,7 @@
|
||||||
<string name="post_media_alt">ALT</string>
|
<string name="post_media_alt">ALT</string>
|
||||||
<string name="action_discard">Változtatások elvetése</string>
|
<string name="action_discard">Változtatások elvetése</string>
|
||||||
<string name="action_continue_edit">Szerkesztés folytatása</string>
|
<string name="action_continue_edit">Szerkesztés folytatása</string>
|
||||||
<string name="compose_unsaved_changes">Elmentetlen változtatásaid vannak.</string>
|
<string name="unsaved_changes">Elmentetlen változtatásaid vannak.</string>
|
||||||
<string name="action_share_account_link">Fiókra történő hivatkozás megosztása</string>
|
<string name="action_share_account_link">Fiókra történő hivatkozás megosztása</string>
|
||||||
<string name="action_share_account_username">Fiók felhasználói nevének megosztása</string>
|
<string name="action_share_account_username">Fiók felhasználói nevének megosztása</string>
|
||||||
<string name="send_account_link_to">Fiók URL megosztása vele…</string>
|
<string name="send_account_link_to">Fiók URL megosztása vele…</string>
|
||||||
|
|
|
@ -507,7 +507,7 @@
|
||||||
<string name="pref_title_http_proxy_port_message">Gáttin ætti að vera á milli %d og %d</string>
|
<string name="pref_title_http_proxy_port_message">Gáttin ætti að vera á milli %d og %d</string>
|
||||||
<string name="error_status_source_load">Mistókst að hlaða inn uppruna stöðufærslu af netþjóninum.</string>
|
<string name="error_status_source_load">Mistókst að hlaða inn uppruna stöðufærslu af netþjóninum.</string>
|
||||||
<string name="a11y_label_loading_thread">Hleð inn þræði</string>
|
<string name="a11y_label_loading_thread">Hleð inn þræði</string>
|
||||||
<string name="compose_unsaved_changes">Þú ert með óvistaðar breytingar.</string>
|
<string name="unsaved_changes">Þú ert með óvistaðar breytingar.</string>
|
||||||
<string name="pref_summary_http_proxy_disabled">Óvirkt</string>
|
<string name="pref_summary_http_proxy_disabled">Óvirkt</string>
|
||||||
<string name="pref_summary_http_proxy_missing"><ekki stillt></string>
|
<string name="pref_summary_http_proxy_missing"><ekki stillt></string>
|
||||||
<string name="pref_summary_http_proxy_invalid"><ógilt></string>
|
<string name="pref_summary_http_proxy_invalid"><ógilt></string>
|
||||||
|
|
|
@ -542,7 +542,7 @@
|
||||||
<string name="notification_header_report_format">%s ha segnalato %s</string>
|
<string name="notification_header_report_format">%s ha segnalato %s</string>
|
||||||
<string name="notification_summary_report_format">%s · %d post allegati</string>
|
<string name="notification_summary_report_format">%s · %d post allegati</string>
|
||||||
<string name="action_add_reaction">aggiungi reazione</string>
|
<string name="action_add_reaction">aggiungi reazione</string>
|
||||||
<string name="compose_unsaved_changes">Hai delle modifiche non salvate.</string>
|
<string name="unsaved_changes">Hai delle modifiche non salvate.</string>
|
||||||
<string name="confirmation_hashtag_unfollowed">Non segui più #%s</string>
|
<string name="confirmation_hashtag_unfollowed">Non segui più #%s</string>
|
||||||
<string name="error_status_source_load">Caricamento dello status della sorgente dal server fallito.</string>
|
<string name="error_status_source_load">Caricamento dello status della sorgente dal server fallito.</string>
|
||||||
<string name="post_edited">Modificato %s</string>
|
<string name="post_edited">Modificato %s</string>
|
||||||
|
|
|
@ -512,7 +512,7 @@
|
||||||
<string name="status_created_info">%1$s の投稿</string>
|
<string name="status_created_info">%1$s の投稿</string>
|
||||||
<string name="follow_requests_info">アカウントがロックされていなかったとしても、%1$s のスタッフは以下のアカウントのフォローリクエストを確認した方がいいと判断しました。</string>
|
<string name="follow_requests_info">アカウントがロックされていなかったとしても、%1$s のスタッフは以下のアカウントのフォローリクエストを確認した方がいいと判断しました。</string>
|
||||||
<string name="action_set_focus">中心点の設定</string>
|
<string name="action_set_focus">中心点の設定</string>
|
||||||
<string name="compose_unsaved_changes">保存していない変更があります。</string>
|
<string name="unsaved_changes">保存していない変更があります。</string>
|
||||||
<string name="error_status_source_load">サーバーからステータスの元情報を取得できませんでした。</string>
|
<string name="error_status_source_load">サーバーからステータスの元情報を取得できませんでした。</string>
|
||||||
<string name="pref_summary_http_proxy_disabled">無効</string>
|
<string name="pref_summary_http_proxy_disabled">無効</string>
|
||||||
<string name="pref_summary_http_proxy_missing"><設定なし></string>
|
<string name="pref_summary_http_proxy_missing"><設定なし></string>
|
||||||
|
|
|
@ -331,7 +331,7 @@
|
||||||
<string name="notification_summary_large">%1$s, %2$s, %3$s un %4$d citi</string>
|
<string name="notification_summary_large">%1$s, %2$s, %3$s un %4$d citi</string>
|
||||||
<string name="title_media">Multivide</string>
|
<string name="title_media">Multivide</string>
|
||||||
<string name="pref_title_alway_show_sensitive_media">Vienmēr rādīt sensitīvu saturu</string>
|
<string name="pref_title_alway_show_sensitive_media">Vienmēr rādīt sensitīvu saturu</string>
|
||||||
<string name="compose_unsaved_changes">Tev ir nesaglabātas izmaiņas.</string>
|
<string name="unsaved_changes">Tev ir nesaglabātas izmaiņas.</string>
|
||||||
<string name="description_post_media">Multivide: %s</string>
|
<string name="description_post_media">Multivide: %s</string>
|
||||||
<string name="description_poll">Aptauja ar izvēlēm: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
<string name="description_poll">Aptauja ar izvēlēm: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
||||||
<string name="poll_info_format"><!-- 15 balsis • atlikusi 1 stunda -->%1$s • %2$s</string>
|
<string name="poll_info_format"><!-- 15 balsis • atlikusi 1 stunda -->%1$s • %2$s</string>
|
||||||
|
|
|
@ -554,7 +554,7 @@
|
||||||
<string name="ui_success_accepted_follow_request">Følgeforespørsel akseptert</string>
|
<string name="ui_success_accepted_follow_request">Følgeforespørsel akseptert</string>
|
||||||
<string name="action_discard">Forkast endringer</string>
|
<string name="action_discard">Forkast endringer</string>
|
||||||
<string name="action_continue_edit">Fortsett endring</string>
|
<string name="action_continue_edit">Fortsett endring</string>
|
||||||
<string name="compose_unsaved_changes">Du har ulagrede endringer.</string>
|
<string name="unsaved_changes">Du har ulagrede endringer.</string>
|
||||||
<string name="select_list_empty">Du har ingen lister, enda</string>
|
<string name="select_list_empty">Du har ingen lister, enda</string>
|
||||||
<string name="error_list_load">Feil under lading av lister</string>
|
<string name="error_list_load">Feil under lading av lister</string>
|
||||||
<string name="select_list_manage">Forvalte lister</string>
|
<string name="select_list_manage">Forvalte lister</string>
|
||||||
|
|
|
@ -488,7 +488,7 @@
|
||||||
<string name="delete_scheduled_post_warning">Dit ingeplande bericht verwijderen\?</string>
|
<string name="delete_scheduled_post_warning">Dit ingeplande bericht verwijderen\?</string>
|
||||||
<string name="failed_to_pin">Kan niet vastmaken</string>
|
<string name="failed_to_pin">Kan niet vastmaken</string>
|
||||||
<string name="pref_summary_http_proxy_disabled">Uitgeschakeld</string>
|
<string name="pref_summary_http_proxy_disabled">Uitgeschakeld</string>
|
||||||
<string name="compose_unsaved_changes">Er zijn niet opgeslagen wijzigingen.</string>
|
<string name="unsaved_changes">Er zijn niet opgeslagen wijzigingen.</string>
|
||||||
<string name="mute_notifications_switch">Meldingen negeren</string>
|
<string name="mute_notifications_switch">Meldingen negeren</string>
|
||||||
<string name="title_edits">Bewerkingen</string>
|
<string name="title_edits">Bewerkingen</string>
|
||||||
<string name="pref_default_post_language">Standaardtaal van berichten</string>
|
<string name="pref_default_post_language">Standaardtaal van berichten</string>
|
||||||
|
|
|
@ -520,7 +520,7 @@
|
||||||
<string name="post_media_alt">ALT</string>
|
<string name="post_media_alt">ALT</string>
|
||||||
<string name="action_discard">Ignorar las modificacions</string>
|
<string name="action_discard">Ignorar las modificacions</string>
|
||||||
<string name="action_continue_edit">Téner de modificar</string>
|
<string name="action_continue_edit">Téner de modificar</string>
|
||||||
<string name="compose_unsaved_changes">Avètz de modificacions pas salvadas.</string>
|
<string name="unsaved_changes">Avètz de modificacions pas salvadas.</string>
|
||||||
<string name="a11y_label_loading_thread">Cargament del fil</string>
|
<string name="a11y_label_loading_thread">Cargament del fil</string>
|
||||||
<string name="pref_summary_http_proxy_disabled">Desactivat</string>
|
<string name="pref_summary_http_proxy_disabled">Desactivat</string>
|
||||||
<string name="pref_summary_http_proxy_missing"><pas definit></string>
|
<string name="pref_summary_http_proxy_missing"><pas definit></string>
|
||||||
|
|
|
@ -526,7 +526,7 @@
|
||||||
<string name="status_edit_info">%1$s edytował</string>
|
<string name="status_edit_info">%1$s edytował</string>
|
||||||
<string name="status_created_info">%1$s stworzył</string>
|
<string name="status_created_info">%1$s stworzył</string>
|
||||||
<string name="title_edits">Edycje</string>
|
<string name="title_edits">Edycje</string>
|
||||||
<string name="compose_unsaved_changes">Masz niezapisane zmiany.</string>
|
<string name="unsaved_changes">Masz niezapisane zmiany.</string>
|
||||||
<string name="action_post_failed">Błąd wysyłania</string>
|
<string name="action_post_failed">Błąd wysyłania</string>
|
||||||
<string name="action_post_failed_show_drafts">Pokaż szkice</string>
|
<string name="action_post_failed_show_drafts">Pokaż szkice</string>
|
||||||
<string name="action_post_failed_do_nothing">Odrzuć</string>
|
<string name="action_post_failed_do_nothing">Odrzuć</string>
|
||||||
|
|
|
@ -510,7 +510,7 @@
|
||||||
<string name="description_browser_login">Pode oferecer suporte a métodos de autenticação adicionais, mas requer um navegador compatível.</string>
|
<string name="description_browser_login">Pode oferecer suporte a métodos de autenticação adicionais, mas requer um navegador compatível.</string>
|
||||||
<string name="status_edit_info">%1$s editou</string>
|
<string name="status_edit_info">%1$s editou</string>
|
||||||
<string name="action_continue_edit">Continuar editando</string>
|
<string name="action_continue_edit">Continuar editando</string>
|
||||||
<string name="compose_unsaved_changes">Você tem alterações não salvas.</string>
|
<string name="unsaved_changes">Você tem alterações não salvas.</string>
|
||||||
<string name="description_post_edited">Editado</string>
|
<string name="description_post_edited">Editado</string>
|
||||||
<string name="delete_scheduled_post_warning">Excluir este Toot agendado?</string>
|
<string name="delete_scheduled_post_warning">Excluir este Toot agendado?</string>
|
||||||
<string name="action_unfollow_hashtag_format">Deixar de seguir #%s\?</string>
|
<string name="action_unfollow_hashtag_format">Deixar de seguir #%s\?</string>
|
||||||
|
|
|
@ -481,7 +481,7 @@
|
||||||
<string name="dialog_follow_hashtag_title">व्यक्तित्वविवरणलेखा अनुसरतु</string>
|
<string name="dialog_follow_hashtag_title">व्यक्तित्वविवरणलेखा अनुसरतु</string>
|
||||||
<string name="limit_notifications">कालानुक्रमपङ्क्त्याः सूचनाः परिमिताः कुरुताम्</string>
|
<string name="limit_notifications">कालानुक्रमपङ्क्त्याः सूचनाः परिमिताः कुरुताम्</string>
|
||||||
<string name="notification_report_description">परिमितावेदनानि प्रति ज्ञापनसूचनाः</string>
|
<string name="notification_report_description">परिमितावेदनानि प्रति ज्ञापनसूचनाः</string>
|
||||||
<string name="compose_unsaved_changes">भवतः अरक्षितानि परिवर्तनानि सन्ति।</string>
|
<string name="unsaved_changes">भवतः अरक्षितानि परिवर्तनानि सन्ति।</string>
|
||||||
<string name="pref_title_notification_filter_reports">नूतनम् आवेदनमस्ति</string>
|
<string name="pref_title_notification_filter_reports">नूतनम् आवेदनमस्ति</string>
|
||||||
<string name="pref_title_wellbeing_mode">सुस्थितिः</string>
|
<string name="pref_title_wellbeing_mode">सुस्थितिः</string>
|
||||||
<string name="delete_scheduled_post_warning">इदं कालबद्धदौत्यं विनश्येत् किम् \?</string>
|
<string name="delete_scheduled_post_warning">इदं कालबद्धदौत्यं विनश्येत् किम् \?</string>
|
||||||
|
|
|
@ -532,7 +532,7 @@
|
||||||
<string name="status_created_info">%1$s skapade</string>
|
<string name="status_created_info">%1$s skapade</string>
|
||||||
<string name="action_discard">Förkasta ändringar</string>
|
<string name="action_discard">Förkasta ändringar</string>
|
||||||
<string name="action_continue_edit">Fortsätt redigera</string>
|
<string name="action_continue_edit">Fortsätt redigera</string>
|
||||||
<string name="compose_unsaved_changes">Du har ändringar som inte sparats.</string>
|
<string name="unsaved_changes">Du har ändringar som inte sparats.</string>
|
||||||
<string name="action_post_failed">Uppladdning misslyckades</string>
|
<string name="action_post_failed">Uppladdning misslyckades</string>
|
||||||
<string name="action_post_failed_detail">Ett fel inträffade när inlägget skulle laddas upp och har sparats till utkast.
|
<string name="action_post_failed_detail">Ett fel inträffade när inlägget skulle laddas upp och har sparats till utkast.
|
||||||
\n
|
\n
|
||||||
|
|
|
@ -524,7 +524,7 @@
|
||||||
<string name="status_created_info">%1$s oluşturdu</string>
|
<string name="status_created_info">%1$s oluşturdu</string>
|
||||||
<string name="dialog_follow_hashtag_title">Etiketi takip et</string>
|
<string name="dialog_follow_hashtag_title">Etiketi takip et</string>
|
||||||
<string name="dialog_follow_hashtag_hint">#etiket</string>
|
<string name="dialog_follow_hashtag_hint">#etiket</string>
|
||||||
<string name="compose_unsaved_changes">Kaydedilmemiş değişikliklerin var.</string>
|
<string name="unsaved_changes">Kaydedilmemiş değişikliklerin var.</string>
|
||||||
<string name="status_edit_info">%1$s düzenledi</string>
|
<string name="status_edit_info">%1$s düzenledi</string>
|
||||||
<string name="title_edits">Düzenlemeler</string>
|
<string name="title_edits">Düzenlemeler</string>
|
||||||
<string name="hint_description">Açıklama</string>
|
<string name="hint_description">Açıklama</string>
|
||||||
|
|
|
@ -530,7 +530,7 @@
|
||||||
<string name="error_status_source_load">Не вдалося завантажити джерело стану з сервера.</string>
|
<string name="error_status_source_load">Не вдалося завантажити джерело стану з сервера.</string>
|
||||||
<string name="post_media_alt">ALT</string>
|
<string name="post_media_alt">ALT</string>
|
||||||
<string name="action_discard">Відкинути зміни</string>
|
<string name="action_discard">Відкинути зміни</string>
|
||||||
<string name="compose_unsaved_changes">У вас є незбережені зміни.</string>
|
<string name="unsaved_changes">У вас є незбережені зміни.</string>
|
||||||
<string name="action_continue_edit">Продовжити редагування</string>
|
<string name="action_continue_edit">Продовжити редагування</string>
|
||||||
<string name="mute_notifications_switch">Беззвучні сповіщення</string>
|
<string name="mute_notifications_switch">Беззвучні сповіщення</string>
|
||||||
<string name="title_edits">Редагування</string>
|
<string name="title_edits">Редагування</string>
|
||||||
|
|
|
@ -499,7 +499,7 @@
|
||||||
<string name="post_media_alt">✦</string>
|
<string name="post_media_alt">✦</string>
|
||||||
<string name="action_discard">Hủy bỏ thay đổi</string>
|
<string name="action_discard">Hủy bỏ thay đổi</string>
|
||||||
<string name="action_continue_edit">Tiếp tục sửa</string>
|
<string name="action_continue_edit">Tiếp tục sửa</string>
|
||||||
<string name="compose_unsaved_changes">Thay đổi chưa được lưu.</string>
|
<string name="unsaved_changes">Thay đổi chưa được lưu.</string>
|
||||||
<string name="mute_notifications_switch">Ẩn thông báo</string>
|
<string name="mute_notifications_switch">Ẩn thông báo</string>
|
||||||
<string name="status_edit_info">Sửa %1$s</string>
|
<string name="status_edit_info">Sửa %1$s</string>
|
||||||
<string name="status_created_info">Đăng %1$s</string>
|
<string name="status_created_info">Đăng %1$s</string>
|
||||||
|
|
|
@ -513,7 +513,7 @@
|
||||||
<string name="post_media_alt">ALT</string>
|
<string name="post_media_alt">ALT</string>
|
||||||
<string name="action_discard">放弃更改</string>
|
<string name="action_discard">放弃更改</string>
|
||||||
<string name="action_continue_edit">继续编辑</string>
|
<string name="action_continue_edit">继续编辑</string>
|
||||||
<string name="compose_unsaved_changes">你有未保存的更改。</string>
|
<string name="unsaved_changes">你有未保存的更改。</string>
|
||||||
<string name="status_created_info">%1$s 创建了</string>
|
<string name="status_created_info">%1$s 创建了</string>
|
||||||
<string name="mute_notifications_switch">将通知静音</string>
|
<string name="mute_notifications_switch">将通知静音</string>
|
||||||
<string name="title_edits">编辑</string>
|
<string name="title_edits">编辑</string>
|
||||||
|
|
|
@ -412,7 +412,7 @@
|
||||||
<string name="compose_delete_draft">Delete draft?</string>
|
<string name="compose_delete_draft">Delete draft?</string>
|
||||||
<string name="compose_save_draft">Save draft?</string>
|
<string name="compose_save_draft">Save draft?</string>
|
||||||
<string name="compose_save_draft_loses_media">Save draft? (Attachments will be uploaded again when you restore the draft.)</string>
|
<string name="compose_save_draft_loses_media">Save draft? (Attachments will be uploaded again when you restore the draft.)</string>
|
||||||
<string name="compose_unsaved_changes">You have unsaved changes.</string>
|
<string name="unsaved_changes">You have unsaved changes.</string>
|
||||||
<string name="send_post_notification_title">Sending post…</string>
|
<string name="send_post_notification_title">Sending post…</string>
|
||||||
<string name="send_post_notification_error_title">Error sending post</string>
|
<string name="send_post_notification_error_title">Error sending post</string>
|
||||||
<string name="send_post_notification_channel_name">Sending Posts</string>
|
<string name="send_post_notification_channel_name">Sending Posts</string>
|
||||||
|
@ -703,5 +703,8 @@
|
||||||
<string name="action_add_to_tab_success">Added \'%1$s\' to tabs</string>
|
<string name="action_add_to_tab_success">Added \'%1$s\' to tabs</string>
|
||||||
<string name="action_remove_tab">Remove tab</string>
|
<string name="action_remove_tab">Remove tab</string>
|
||||||
<string name="action_manage_tabs">Manage tabs</string>
|
<string name="action_manage_tabs">Manage tabs</string>
|
||||||
|
<string name="error_filter_missing_keyword">At least one keyword or phrase is required</string>
|
||||||
|
<string name="error_filter_missing_context">At least one filter context is required</string>
|
||||||
|
<string name="error_filter_missing_title">Title is required</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
package app.pachli
|
package app.pachli
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import app.pachli.components.filters.EditFilterActivity
|
import app.pachli.components.filters.EditFilterViewModel.Companion.getSecondsForDurationIndex
|
||||||
import app.pachli.core.network.model.Attachment
|
import app.pachli.core.network.model.Attachment
|
||||||
import app.pachli.core.network.model.Filter
|
import app.pachli.core.network.model.Filter
|
||||||
import app.pachli.core.network.model.FilterContext
|
import app.pachli.core.network.model.FilterContext
|
||||||
|
@ -259,7 +259,7 @@ class FilterV1Test {
|
||||||
fun unchangedExpiration_shouldBeNegative_whenFilterIsExpired() {
|
fun unchangedExpiration_shouldBeNegative_whenFilterIsExpired() {
|
||||||
val expiredBySeconds = 3600
|
val expiredBySeconds = 3600
|
||||||
val expiredDate = Date.from(Instant.now().minusSeconds(expiredBySeconds.toLong()))
|
val expiredDate = Date.from(Instant.now().minusSeconds(expiredBySeconds.toLong()))
|
||||||
val updatedDuration = EditFilterActivity.getSecondsForDurationIndex(-1, null, expiredDate)
|
val updatedDuration = getSecondsForDurationIndex(-1, null, expiredDate)
|
||||||
assert(updatedDuration != null && updatedDuration.toInt() <= -expiredBySeconds)
|
assert(updatedDuration != null && updatedDuration.toInt() <= -expiredBySeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,7 +267,7 @@ class FilterV1Test {
|
||||||
fun unchangedExpiration_shouldBePositive_whenFilterIsUnexpired() {
|
fun unchangedExpiration_shouldBePositive_whenFilterIsUnexpired() {
|
||||||
val expiresInSeconds = 3600
|
val expiresInSeconds = 3600
|
||||||
val expiredDate = Date.from(Instant.now().plusSeconds(expiresInSeconds.toLong()))
|
val expiredDate = Date.from(Instant.now().plusSeconds(expiresInSeconds.toLong()))
|
||||||
val updatedDuration = EditFilterActivity.getSecondsForDurationIndex(-1, null, expiredDate)
|
val updatedDuration = getSecondsForDurationIndex(-1, null, expiredDate)
|
||||||
assert(updatedDuration != null && updatedDuration.toInt() > (expiresInSeconds - 60))
|
assert(updatedDuration != null && updatedDuration.toInt() > (expiresInSeconds - 60))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -239,7 +239,7 @@ class ComposeActivityTest {
|
||||||
rule.launch()
|
rule.launch()
|
||||||
rule.getScenario().onActivity {
|
rule.getScenario().onActivity {
|
||||||
insertSomeTextInContent(it, content)
|
insertSomeTextInContent(it, content)
|
||||||
assertEquals(content.length, it.calculateTextLength())
|
assertEquals(content.length, it.viewModel.statusLength.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,7 +249,7 @@ class ComposeActivityTest {
|
||||||
rule.launch()
|
rule.launch()
|
||||||
rule.getScenario().onActivity {
|
rule.getScenario().onActivity {
|
||||||
insertSomeTextInContent(it, content)
|
insertSomeTextInContent(it, content)
|
||||||
assertEquals(6, it.calculateTextLength())
|
assertEquals(6, it.viewModel.statusLength.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,7 +259,7 @@ class ComposeActivityTest {
|
||||||
rule.launch()
|
rule.launch()
|
||||||
rule.getScenario().onActivity {
|
rule.getScenario().onActivity {
|
||||||
insertSomeTextInContent(it, content)
|
insertSomeTextInContent(it, content)
|
||||||
assertEquals(7, it.calculateTextLength())
|
assertEquals(7, it.viewModel.statusLength.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,7 +271,7 @@ class ComposeActivityTest {
|
||||||
insertSomeTextInContent(it, content)
|
insertSomeTextInContent(it, content)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL,
|
InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL,
|
||||||
it.calculateTextLength(),
|
it.viewModel.statusLength.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -282,7 +282,7 @@ class ComposeActivityTest {
|
||||||
rule.launch()
|
rule.launch()
|
||||||
rule.getScenario().onActivity {
|
rule.getScenario().onActivity {
|
||||||
insertSomeTextInContent(it, content)
|
insertSomeTextInContent(it, content)
|
||||||
assertEquals(21, it.calculateTextLength())
|
assertEquals(21, it.viewModel.statusLength.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,7 +295,7 @@ class ComposeActivityTest {
|
||||||
insertSomeTextInContent(it, additionalContent + url)
|
insertSomeTextInContent(it, additionalContent + url)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
additionalContent.length + InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL,
|
additionalContent.length + InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL,
|
||||||
it.calculateTextLength(),
|
it.viewModel.statusLength.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -310,7 +310,7 @@ class ComposeActivityTest {
|
||||||
insertSomeTextInContent(it, shortUrl + additionalContent + url)
|
insertSomeTextInContent(it, shortUrl + additionalContent + url)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2),
|
additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2),
|
||||||
it.calculateTextLength(),
|
it.viewModel.statusLength.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -324,7 +324,7 @@ class ComposeActivityTest {
|
||||||
insertSomeTextInContent(it, url + additionalContent + url)
|
insertSomeTextInContent(it, url + additionalContent + url)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2),
|
additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2),
|
||||||
it.calculateTextLength(),
|
it.viewModel.statusLength.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -340,7 +340,7 @@ class ComposeActivityTest {
|
||||||
insertSomeTextInContent(it, additionalContent + url)
|
insertSomeTextInContent(it, additionalContent + url)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
additionalContent.length + customUrlLength,
|
additionalContent.length + customUrlLength,
|
||||||
it.calculateTextLength(),
|
it.viewModel.statusLength.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -357,7 +357,7 @@ class ComposeActivityTest {
|
||||||
insertSomeTextInContent(it, shortUrl + additionalContent + url)
|
insertSomeTextInContent(it, shortUrl + additionalContent + url)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
additionalContent.length + (customUrlLength * 2),
|
additionalContent.length + (customUrlLength * 2),
|
||||||
it.calculateTextLength(),
|
it.viewModel.statusLength.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -373,7 +373,7 @@ class ComposeActivityTest {
|
||||||
insertSomeTextInContent(it, url + additionalContent + url)
|
insertSomeTextInContent(it, url + additionalContent + url)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
additionalContent.length + (customUrlLength * 2),
|
additionalContent.length + (customUrlLength * 2),
|
||||||
it.calculateTextLength(),
|
it.viewModel.statusLength.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,15 +22,15 @@ import app.pachli.util.highlightSpans
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.Parameterized
|
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||||
|
|
||||||
@RunWith(Parameterized::class)
|
@RunWith(ParameterizedRobolectricTestRunner::class)
|
||||||
class StatusLengthTest(
|
class StatusLengthTest(
|
||||||
private val text: String,
|
private val text: String,
|
||||||
private val expectedLength: Int,
|
private val expectedLength: Int,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
@Parameterized.Parameters(name = "{0}")
|
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun data(): Iterable<Any> {
|
fun data(): Iterable<Any> {
|
||||||
return listOf(
|
return listOf(
|
||||||
|
@ -61,7 +61,7 @@ class StatusLengthTest(
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
expectedLength,
|
expectedLength,
|
||||||
ComposeActivity.statusLength(spannedText, null, 23),
|
ComposeViewModel.statusLength(spannedText, "", 23),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ class StatusLengthTest(
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
expectedLength + cwText.length,
|
expectedLength + cwText.length,
|
||||||
ComposeActivity.statusLength(spannedText, cwText, 23),
|
ComposeViewModel.statusLength(spannedText, cwText.toString(), 23),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,9 @@ import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import app.pachli.core.accounts.AccountManager
|
import app.pachli.core.accounts.AccountManager
|
||||||
|
import app.pachli.core.activity.extensions.canOverrideActivityTransitions
|
||||||
|
import app.pachli.core.activity.extensions.getTransitionKind
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
import app.pachli.core.database.model.AccountEntity
|
import app.pachli.core.database.model.AccountEntity
|
||||||
import app.pachli.core.designsystem.EmbeddedFontFamily
|
import app.pachli.core.designsystem.EmbeddedFontFamily
|
||||||
import app.pachli.core.designsystem.R as DR
|
import app.pachli.core.designsystem.R as DR
|
||||||
|
@ -82,6 +85,13 @@ abstract class BaseActivity : AppCompatActivity(), MenuProvider {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (canOverrideActivityTransitions()) {
|
||||||
|
intent.getTransitionKind()?.let {
|
||||||
|
overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, it.openEnter, it.openExit)
|
||||||
|
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, it.closeEnter, it.closeExit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set the theme from preferences
|
// Set the theme from preferences
|
||||||
val theme = AppTheme.from(sharedPreferencesRepository)
|
val theme = AppTheme.from(sharedPreferencesRepository)
|
||||||
Timber.d("activeTheme: %s", theme)
|
Timber.d("activeTheme: %s", theme)
|
||||||
|
@ -156,11 +166,6 @@ abstract class BaseActivity : AppCompatActivity(), MenuProvider {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startActivityWithSlideInAnimation(intent: Intent) {
|
|
||||||
super.startActivity(intent)
|
|
||||||
overridePendingTransition(DR.anim.slide_from_right, DR.anim.slide_to_left)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
if (item.itemId == android.R.id.home) {
|
if (item.itemId == android.R.id.home) {
|
||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
@ -171,11 +176,13 @@ abstract class BaseActivity : AppCompatActivity(), MenuProvider {
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
super.finish()
|
super.finish()
|
||||||
overridePendingTransition(DR.anim.slide_from_left, DR.anim.slide_to_right)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun finishWithoutSlideOutAnimation() {
|
if (!canOverrideActivityTransitions()) {
|
||||||
super.finish()
|
intent.getTransitionKind()?.let {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
overridePendingTransition(it.closeEnter, it.closeExit)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun redirectIfNotLoggedIn() {
|
private fun redirectIfNotLoggedIn() {
|
||||||
|
@ -183,7 +190,7 @@ abstract class BaseActivity : AppCompatActivity(), MenuProvider {
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
val intent = LoginActivityIntent(this)
|
val intent = LoginActivityIntent(this)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,7 +266,7 @@ abstract class BaseActivity : AppCompatActivity(), MenuProvider {
|
||||||
accountManager.setActiveAccount(account.id)
|
accountManager.setActiveAccount(account.id)
|
||||||
val intent = MainActivityIntent.redirect(this, account.id, url)
|
val intent = MainActivityIntent.redirect(this, account.id, url)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finishWithoutSlideOutAnimation()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(
|
override fun onRequestPermissionsResult(
|
||||||
|
|
|
@ -24,6 +24,9 @@ import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import app.pachli.core.activity.extensions.TransitionKind
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
|
||||||
|
import app.pachli.core.activity.extensions.startActivityWithTransition
|
||||||
import app.pachli.core.navigation.AccountActivityIntent
|
import app.pachli.core.navigation.AccountActivityIntent
|
||||||
import app.pachli.core.navigation.ViewThreadActivityIntent
|
import app.pachli.core.navigation.ViewThreadActivityIntent
|
||||||
import app.pachli.core.network.retrofit.MastodonApi
|
import app.pachli.core.network.retrofit.MastodonApi
|
||||||
|
@ -105,13 +108,13 @@ abstract class BottomSheetActivity : BaseActivity() {
|
||||||
open fun viewThread(statusId: String, url: String?) {
|
open fun viewThread(statusId: String, url: String?) {
|
||||||
if (!isSearching()) {
|
if (!isSearching()) {
|
||||||
val intent = ViewThreadActivityIntent(this, statusId, url)
|
val intent = ViewThreadActivityIntent(this, statusId, url)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithTransition(intent, TransitionKind.SLIDE_FROM_END)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun viewAccount(id: String) {
|
open fun viewAccount(id: String) {
|
||||||
val intent = AccountActivityIntent(this, id)
|
val intent = AccountActivityIntent(this, id)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithDefaultTransition(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) {
|
protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) {
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Pachli Association
|
||||||
|
*
|
||||||
|
* This file is a part of Pachli.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package app.pachli.core.activity.extensions
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Activity.OVERRIDE_TRANSITION_CLOSE
|
||||||
|
import android.app.Activity.OVERRIDE_TRANSITION_OPEN
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
|
import app.pachli.core.activity.BaseActivity
|
||||||
|
import app.pachli.core.activity.BuildConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the activity in [intent] (which must be a subclass of [BaseActivity])
|
||||||
|
* using [transitionKind] as the open/close transition.
|
||||||
|
*/
|
||||||
|
fun Activity.startActivityWithTransition(intent: Intent, transitionKind: TransitionKind) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
if (this !is BaseActivity) {
|
||||||
|
throw IllegalStateException("startActivityWithTransition must be used with BaseActivity subclass")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
intent.putExtra(EXTRA_TRANSITION_KIND, transitionKind)
|
||||||
|
startActivity(intent)
|
||||||
|
|
||||||
|
if (canOverrideActivityTransitions()) {
|
||||||
|
overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, transitionKind.openEnter, transitionKind.openExit)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
overridePendingTransition(transitionKind.openEnter, transitionKind.openExit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See [Activity.startActivityWithTransition] */
|
||||||
|
fun Activity.startActivityWithDefaultTransition(intent: Intent) = startActivityWithTransition(intent, TransitionKind.DEFAULT)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides any "close" transition already set for this activity and
|
||||||
|
* replaces them with [transitionKind].
|
||||||
|
*
|
||||||
|
* Call this after calling [Activity.finish]
|
||||||
|
*/
|
||||||
|
fun Activity.setCloseTransition(transitionKind: TransitionKind) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
if (this !is BaseActivity) {
|
||||||
|
throw IllegalStateException("startActivityWithTransition must be used with BaseActivity subclass")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canOverrideActivityTransitions()) {
|
||||||
|
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, transitionKind.closeEnter, transitionKind.closeExit)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
overridePendingTransition(transitionKind.closeEnter, transitionKind.closeExit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return True if the Android version supports [Activity.overrideActivityTransition],
|
||||||
|
* false if [Activity.overridePendingTransition] must be used.
|
||||||
|
*/
|
||||||
|
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun canOverrideActivityTransitions() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Pachli Association
|
||||||
|
*
|
||||||
|
* This file is a part of Pachli.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package app.pachli.core.activity.extensions
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.AnimRes
|
||||||
|
import app.pachli.core.designsystem.R as DR
|
||||||
|
|
||||||
|
const val EXTRA_TRANSITION_KIND = "transition_kind"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of transition and animation resources to use when opening and closing
|
||||||
|
* an activity.
|
||||||
|
*
|
||||||
|
* @property openEnter When opening an activity, a resource ID of the animation resource to
|
||||||
|
* use for the incoming activity. Use 0 for no animation.
|
||||||
|
* @property openExit When opening an activity, a resource ID of the animation resource to
|
||||||
|
* use for the outgoing activity. Use 0 for no animation
|
||||||
|
* @property closeEnter When closing an activity, a resource ID of the animation resource to
|
||||||
|
* use for the incoming activity. Use 0 for no animation.
|
||||||
|
* @property closeExit When closing an activity, a resource ID of the animation resource to
|
||||||
|
* use for the outgoing activity. Use 0 for no animation
|
||||||
|
*/
|
||||||
|
enum class TransitionKind(
|
||||||
|
@AnimRes val openEnter: Int,
|
||||||
|
@AnimRes val openExit: Int,
|
||||||
|
@AnimRes val closeEnter: Int,
|
||||||
|
@AnimRes val closeExit: Int,
|
||||||
|
) {
|
||||||
|
/** Default transition */
|
||||||
|
DEFAULT(DR.anim.activity_open_enter, DR.anim.activity_open_exit, DR.anim.activity_close_enter, DR.anim.activity_close_exit),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slide from the user's "end" perspective (right side for LTR text, left side for RTL text).
|
||||||
|
* Use when a spatial relationship makes sense, such as transitioning from a single status
|
||||||
|
* to the thread that contains that status.
|
||||||
|
*/
|
||||||
|
SLIDE_FROM_END(DR.anim.slide_from_end, DR.anim.slide_to_start, DR.anim.slide_from_start, DR.anim.slide_to_end),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explode out from the centre of the screen. Use to indicate a significant change in
|
||||||
|
* application state (e.g., changing accounts).
|
||||||
|
*/
|
||||||
|
EXPLODE(DR.anim.explode, DR.anim.activity_open_exit, 0, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return The [TransitionKind] included in this intent, or null */
|
||||||
|
fun Intent.getTransitionKind(): TransitionKind? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
getSerializableExtra(EXTRA_TRANSITION_KIND, TransitionKind::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
getSerializableExtra(EXTRA_TRANSITION_KIND) as? TransitionKind
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2024 Pachli Association
|
||||||
|
~
|
||||||
|
~ This file is a part of Pachli.
|
||||||
|
~
|
||||||
|
~ This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
~ License, or (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
~ Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||||
|
~ see <http://www.gnu.org/licenses>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="450" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="96dp"
|
||||||
|
android:toXDelta="0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
|
||||||
|
<extend
|
||||||
|
android:fromExtendLeft="96dp"
|
||||||
|
android:fromExtendTop="0"
|
||||||
|
android:fromExtendRight="0"
|
||||||
|
android:fromExtendBottom="0"
|
||||||
|
android:toExtendLeft="96dp"
|
||||||
|
android:toExtendTop="0"
|
||||||
|
android:toExtendRight="0"
|
||||||
|
android:toExtendBottom="0"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2024 Pachli Association
|
||||||
|
~
|
||||||
|
~ This file is a part of Pachli.
|
||||||
|
~
|
||||||
|
~ This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
~ License, or (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
~ Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||||
|
~ see <http://www.gnu.org/licenses>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="35"
|
||||||
|
android:duration="83" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="0"
|
||||||
|
android:toXDelta="-96dp"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
|
||||||
|
<extend
|
||||||
|
android:fromExtendLeft="0"
|
||||||
|
android:fromExtendTop="0"
|
||||||
|
android:fromExtendRight="96dp"
|
||||||
|
android:fromExtendBottom="0"
|
||||||
|
android:toExtendLeft="0"
|
||||||
|
android:toExtendTop="0"
|
||||||
|
android:toExtendRight="96dp"
|
||||||
|
android:toExtendBottom="0"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
/*
|
||||||
|
** Copyright 2022, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="50"
|
||||||
|
android:duration="83" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="-96dp"
|
||||||
|
android:toXDelta="0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
|
||||||
|
<extend
|
||||||
|
android:fromExtendLeft="0"
|
||||||
|
android:fromExtendTop="0"
|
||||||
|
android:fromExtendRight="96dp"
|
||||||
|
android:fromExtendBottom="0"
|
||||||
|
android:toExtendLeft="0"
|
||||||
|
android:toExtendTop="0"
|
||||||
|
android:toExtendRight="96dp"
|
||||||
|
android:toExtendBottom="0"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
/*
|
||||||
|
** Copyright 2022, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/standard_accelerate"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="450" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="0"
|
||||||
|
android:toXDelta="96dp"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
|
||||||
|
<extend
|
||||||
|
android:fromExtendLeft="9dp"
|
||||||
|
android:fromExtendTop="0"
|
||||||
|
android:fromExtendRight="0"
|
||||||
|
android:fromExtendBottom="0"
|
||||||
|
android:toExtendLeft="96dp"
|
||||||
|
android:toExtendTop="0"
|
||||||
|
android:toExtendRight="0"
|
||||||
|
android:toExtendBottom="0"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
/*
|
||||||
|
** Copyright 2022, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="450" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="96dp"
|
||||||
|
android:toXDelta="0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
/*
|
||||||
|
** Copyright 2022, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="35"
|
||||||
|
android:duration="83" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="0"
|
||||||
|
android:toXDelta="-96dp"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
/*
|
||||||
|
** Copyright 2022, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="50"
|
||||||
|
android:duration="83" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="-96dp"
|
||||||
|
android:toXDelta="0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
/*
|
||||||
|
** Copyright 2022, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/standard_accelerate"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="450" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="0"
|
||||||
|
android:toXDelta="96dp"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -2,5 +2,5 @@
|
||||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<translate android:fromXDelta="0" android:toXDelta="100%p"
|
<translate android:fromXDelta="0" android:toXDelta="100%p"
|
||||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||||
android:duration="300"/>
|
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||||
</set>
|
</set>
|
|
@ -2,5 +2,5 @@
|
||||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<translate android:fromXDelta="0" android:toXDelta="-100%p"
|
<translate android:fromXDelta="0" android:toXDelta="-100%p"
|
||||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||||
android:duration="300"/>
|
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||||
</set>
|
</set>
|
|
@ -2,5 +2,5 @@
|
||||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<translate android:fromXDelta="100%p" android:toXDelta="0"
|
<translate android:fromXDelta="100%p" android:toXDelta="0"
|
||||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||||
android:duration="300"/>
|
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||||
</set>
|
</set>
|
|
@ -2,5 +2,5 @@
|
||||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<translate android:fromXDelta="-100%p" android:toXDelta="0"
|
<translate android:fromXDelta="-100%p" android:toXDelta="0"
|
||||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||||
android:duration="300"/>
|
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||||
</set>
|
</set>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
/*
|
||||||
|
** Copyright 2009, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="-96dp"
|
||||||
|
android:toXDelta="0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
|
||||||
|
<extend
|
||||||
|
android:fromExtendLeft="0"
|
||||||
|
android:fromExtendTop="0"
|
||||||
|
android:fromExtendRight="96dp"
|
||||||
|
android:fromExtendBottom="0"
|
||||||
|
android:toExtendLeft="0"
|
||||||
|
android:toExtendTop="0"
|
||||||
|
android:toExtendRight="96dp"
|
||||||
|
android:toExtendBottom="0"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
/*
|
||||||
|
** Copyright 2009, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="35"
|
||||||
|
android:duration="83" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="0"
|
||||||
|
android:toXDelta="96dp"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
|
||||||
|
<extend
|
||||||
|
android:fromExtendLeft="96dp"
|
||||||
|
android:fromExtendTop="0"
|
||||||
|
android:fromExtendRight="0"
|
||||||
|
android:fromExtendBottom="0"
|
||||||
|
android:toExtendLeft="96dp"
|
||||||
|
android:toExtendTop="0"
|
||||||
|
android:toExtendRight="0"
|
||||||
|
android:toExtendBottom="0"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
/*
|
||||||
|
** Copyright 2009, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="50"
|
||||||
|
android:duration="83" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="96dp"
|
||||||
|
android:toXDelta="0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
|
||||||
|
<extend
|
||||||
|
android:fromExtendLeft="96dp"
|
||||||
|
android:fromExtendTop="0"
|
||||||
|
android:fromExtendRight="0"
|
||||||
|
android:fromExtendBottom="0"
|
||||||
|
android:toExtendLeft="96dp"
|
||||||
|
android:toExtendTop="0"
|
||||||
|
android:toExtendRight="0"
|
||||||
|
android:toExtendBottom="0"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
/*
|
||||||
|
** Copyright 2009, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/standard_accelerate"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="450" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="0"
|
||||||
|
android:toXDelta="-96dp"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
|
||||||
|
<extend
|
||||||
|
android:fromExtendLeft="0"
|
||||||
|
android:fromExtendTop="0"
|
||||||
|
android:fromExtendRight="96dp"
|
||||||
|
android:fromExtendBottom="0"
|
||||||
|
android:toExtendLeft="0"
|
||||||
|
android:toExtendTop="0"
|
||||||
|
android:toExtendRight="96dp"
|
||||||
|
android:toExtendBottom="0"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
/*
|
||||||
|
** Copyright 2009, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="450" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="-96dp"
|
||||||
|
android:toXDelta="0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
/*
|
||||||
|
** Copyright 2009, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false"
|
||||||
|
android:zAdjustment="top">
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="35"
|
||||||
|
android:duration="83" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="0"
|
||||||
|
android:toXDelta="96dp"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
/*
|
||||||
|
** Copyright 2009, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/linear_interpolator"
|
||||||
|
android:startOffset="50"
|
||||||
|
android:duration="83" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="96dp"
|
||||||
|
android:toXDelta="0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
/*
|
||||||
|
** Copyright 2009, The Android Open Source Project
|
||||||
|
**
|
||||||
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
** you may not use this file except in compliance with the License.
|
||||||
|
** You may obtain a copy of the License at
|
||||||
|
**
|
||||||
|
** http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
**
|
||||||
|
** Unless required by applicable law or agreed to in writing, software
|
||||||
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
** See the License for the specific language governing permissions and
|
||||||
|
** limitations under the License.
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shareInterpolator="false">
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/standard_accelerate"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="0"
|
||||||
|
android:toXDelta="-96dp"
|
||||||
|
android:fillEnabled="true"
|
||||||
|
android:fillBefore="true"
|
||||||
|
android:fillAfter="true"
|
||||||
|
android:interpolator="@anim/fast_out_extra_slow_in"
|
||||||
|
android:startOffset="0"
|
||||||
|
android:duration="@integer/activity_transition_duration_ms" />
|
||||||
|
</set>
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:interpolator="@android:anim/linear_interpolator"
|
|
||||||
android:fromAlpha="0"
|
|
||||||
android:toAlpha="1"
|
|
||||||
android:duration="300" />
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:interpolator="@android:anim/linear_interpolator"
|
|
||||||
android:fromAlpha="1"
|
|
||||||
android:toAlpha="0"
|
|
||||||
android:duration="300" />
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (C) 2017 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License
|
||||||
|
-->
|
||||||
|
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1"/>
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2024 Pachli Association
|
||||||
|
~
|
||||||
|
~ This file is a part of Pachli.
|
||||||
|
~
|
||||||
|
~ This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
~ License, or (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
~ Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||||
|
~ see <http://www.gnu.org/licenses>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<linearInterpolator />
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate android:fromXDelta="100%p" android:toXDelta="0"
|
||||||
|
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||||
|
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||||
|
</set>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate android:fromXDelta="-100%p" android:toXDelta="0"
|
||||||
|
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||||
|
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||||
|
</set>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate android:fromXDelta="0" android:toXDelta="100%p"
|
||||||
|
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||||
|
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||||
|
</set>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate android:fromXDelta="0" android:toXDelta="-100%p"
|
||||||
|
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||||
|
android:duration="@integer/activity_slide_transition_duration_ms"/>
|
||||||
|
</set>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (C) 2021 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:controlX1="0.3"
|
||||||
|
android:controlY1="0"
|
||||||
|
android:controlX2="1"
|
||||||
|
android:controlY2="1"/>
|
|
@ -3,4 +3,10 @@
|
||||||
<integer name="profile_media_column_count">3</integer>
|
<integer name="profile_media_column_count">3</integer>
|
||||||
|
|
||||||
<integer name="trending_column_count">1</integer>
|
<integer name="trending_column_count">1</integer>
|
||||||
|
|
||||||
|
<!-- Duration for default activity transition, in ms -->
|
||||||
|
<integer name="activity_transition_duration_ms">450</integer>
|
||||||
|
|
||||||
|
<!-- Duration for slide transition, in ms -->
|
||||||
|
<integer name="activity_slide_transition_duration_ms">200</integer>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -11,11 +11,11 @@ import kotlinx.parcelize.Parcelize
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class Filter(
|
data class Filter(
|
||||||
val id: String,
|
val id: String = "",
|
||||||
val title: String,
|
val title: String = "",
|
||||||
@Json(name = "context") val contexts: List<FilterContext>,
|
@Json(name = "context") val contexts: List<FilterContext> = emptyList(),
|
||||||
@Json(name = "expires_at") val expiresAt: Date?,
|
@Json(name = "expires_at") val expiresAt: Date? = null,
|
||||||
@Json(name = "filter_action") val action: Action,
|
@Json(name = "filter_action") val action: Action = Action.WARN,
|
||||||
// This should not normally be empty. However, Mastodon does not include
|
// This should not normally be empty. However, Mastodon does not include
|
||||||
// this in a status' `filtered.filter` property (it's not null or empty,
|
// this in a status' `filtered.filter` property (it's not null or empty,
|
||||||
// it's missing) which breaks deserialisation. Patch this by ensuring it's
|
// it's missing) which breaks deserialisation. Patch this by ensuring it's
|
||||||
|
|
|
@ -25,20 +25,30 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import app.pachli.core.activity.BottomSheetActivity
|
import app.pachli.core.activity.BottomSheetActivity
|
||||||
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
import app.pachli.core.designsystem.R as DR
|
import app.pachli.core.designsystem.R as DR
|
||||||
import app.pachli.core.ui.extensions.reduceSwipeSensitivity
|
import app.pachli.core.ui.extensions.reduceSwipeSensitivity
|
||||||
import app.pachli.feature.about.databinding.ActivityAboutBinding
|
import app.pachli.feature.about.databinding.ActivityAboutBinding
|
||||||
import com.bumptech.glide.request.target.FixedSizeDrawable
|
import com.bumptech.glide.request.target.FixedSizeDrawable
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AboutActivity : BottomSheetActivity(), MenuProvider {
|
class AboutActivity : BottomSheetActivity(), MenuProvider {
|
||||||
|
|
||||||
|
private val binding: ActivityAboutBinding by viewBinding(ActivityAboutBinding::inflate)
|
||||||
|
|
||||||
|
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
binding.pager.currentItem = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val binding = ActivityAboutBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
|
@ -65,14 +75,17 @@ class AboutActivity : BottomSheetActivity(), MenuProvider {
|
||||||
tab.text = adapter.title(position)
|
tab.text = adapter.title(position)
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(
|
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
this,
|
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||||
object : OnBackPressedCallback(true) {
|
onBackPressedCallback.isEnabled = tab.position > 0
|
||||||
override fun handleOnBackPressed() {
|
}
|
||||||
if (binding.pager.currentItem != 0) binding.pager.currentItem = 0 else finish()
|
|
||||||
}
|
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||||
},
|
|
||||||
)
|
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue