From 41c702fc1b3905d9b075a9af28c61377134025bb Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Tue, 6 Feb 2024 00:43:26 +0100 Subject: [PATCH] change: Display "About" information in tabs (#420) Previously, `AboutActivity` had buttons and links to show the privacy policy and licenses of dependencies. Change this to a selection of fragments in tabs, one tab each for: - General "About" information - Licenses - Privacy Policy The information shown hasn't changed, but this lays the groundwork for including additional tabs in the future for information like server rules, detected capabilities, or troubleshooting information. --- app/src/main/AndroidManifest.xml | 2 - app/src/main/java/app/pachli/MainActivity.kt | 2 +- .../components/account/AccountActivity.kt | 2 +- .../components/search/SearchActivity.kt | 2 +- .../components/trending/TrendingActivity.kt | 2 +- .../java/app/pachli/util/ViewExtensions.kt | 37 --- .../app/pachli/core/navigation/Navigation.kt | 12 - .../pachli/core/ui/ViewPager2Extensions.kt | 56 ++++ feature/about/build.gradle.kts | 4 + .../app/pachli/feature/about/AboutActivity.kt | 164 ++++-------- .../app/pachli/feature/about/AboutFragment.kt | 135 ++++++++++ .../feature/about/AboutFragmentViewModel.kt | 56 ++++ .../pachli/feature/about/LicenseActivity.kt | 51 ---- ...cyActivity.kt => PrivacyPolicyFragment.kt} | 20 +- .../src/main/res/layout/activity_about.xml | 242 +++--------------- .../src/main/res/layout/activity_license.xml | 25 -- .../src/main/res/layout/fragment_about.xml | 160 ++++++++++++ ...policy.xml => fragment_privacy_policy.xml} | 4 +- 18 files changed, 524 insertions(+), 452 deletions(-) create mode 100644 core/ui/src/main/kotlin/app/pachli/core/ui/ViewPager2Extensions.kt create mode 100644 feature/about/src/main/kotlin/app/pachli/feature/about/AboutFragment.kt create mode 100644 feature/about/src/main/kotlin/app/pachli/feature/about/AboutFragmentViewModel.kt delete mode 100644 feature/about/src/main/kotlin/app/pachli/feature/about/LicenseActivity.kt rename feature/about/src/main/kotlin/app/pachli/feature/about/{PrivacyPolicyActivity.kt => PrivacyPolicyFragment.kt} (65%) delete mode 100644 feature/about/src/main/res/layout/activity_license.xml create mode 100644 feature/about/src/main/res/layout/fragment_about.xml rename feature/about/src/main/res/layout/{activity_privacy_policy.xml => fragment_privacy_policy.xml} (84%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ed559231b..fa344b86b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -144,8 +144,6 @@ android:resource="@xml/searchable" /> - - diff --git a/app/src/main/java/app/pachli/MainActivity.kt b/app/src/main/java/app/pachli/MainActivity.kt index 1dc018bd0..72bd30c87 100644 --- a/app/src/main/java/app/pachli/MainActivity.kt +++ b/app/src/main/java/app/pachli/MainActivity.kt @@ -97,6 +97,7 @@ import app.pachli.core.navigation.TrendingActivityIntent import app.pachli.core.network.model.Account import app.pachli.core.network.model.Notification import app.pachli.core.preferences.PrefKeys +import app.pachli.core.ui.reduceSwipeSensitivity import app.pachli.databinding.ActivityMainBinding import app.pachli.db.DraftsAlert import app.pachli.interfaces.ActionButtonActivity @@ -110,7 +111,6 @@ import app.pachli.usecase.LogoutUsecase import app.pachli.util.await import app.pachli.util.deleteStaleCachedMedia import app.pachli.util.getDimension -import app.pachli.util.reduceSwipeSensitivity import app.pachli.util.unsafeLazy import app.pachli.util.updateShortcut import at.connyduck.calladapter.networkresult.fold diff --git a/app/src/main/java/app/pachli/components/account/AccountActivity.kt b/app/src/main/java/app/pachli/components/account/AccountActivity.kt index 6bca184ad..99786ec02 100644 --- a/app/src/main/java/app/pachli/components/account/AccountActivity.kt +++ b/app/src/main/java/app/pachli/components/account/AccountActivity.kt @@ -77,6 +77,7 @@ import app.pachli.core.network.model.Relationship import app.pachli.core.network.parseAsMastodonHtml import app.pachli.core.preferences.AppTheme import app.pachli.core.preferences.PrefKeys +import app.pachli.core.ui.reduceSwipeSensitivity import app.pachli.databinding.ActivityAccountBinding import app.pachli.db.DraftsAlert import app.pachli.interfaces.ActionButtonActivity @@ -86,7 +87,6 @@ import app.pachli.util.Error import app.pachli.util.Loading import app.pachli.util.Success import app.pachli.util.getDomain -import app.pachli.util.reduceSwipeSensitivity import app.pachli.util.setClickableText import app.pachli.view.showMuteAccountDialog import com.bumptech.glide.Glide diff --git a/app/src/main/java/app/pachli/components/search/SearchActivity.kt b/app/src/main/java/app/pachli/components/search/SearchActivity.kt index 483a6b383..dfc21dad6 100644 --- a/app/src/main/java/app/pachli/components/search/SearchActivity.kt +++ b/app/src/main/java/app/pachli/components/search/SearchActivity.kt @@ -31,8 +31,8 @@ import app.pachli.components.search.adapter.SearchPagerAdapter import app.pachli.core.activity.BottomSheetActivity import app.pachli.core.common.extensions.viewBinding import app.pachli.core.preferences.PrefKeys +import app.pachli.core.ui.reduceSwipeSensitivity import app.pachli.databinding.ActivitySearchBinding -import app.pachli.util.reduceSwipeSensitivity import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint diff --git a/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt b/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt index 905d65961..1e2d5dedf 100644 --- a/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt +++ b/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt @@ -32,9 +32,9 @@ import app.pachli.components.timeline.TimelineFragment import app.pachli.core.activity.BottomSheetActivity import app.pachli.core.common.extensions.viewBinding import app.pachli.core.network.model.TimelineKind +import app.pachli.core.ui.reduceSwipeSensitivity import app.pachli.databinding.ActivityTrendingBinding import app.pachli.interfaces.AppBarLayoutHost -import app.pachli.util.reduceSwipeSensitivity import com.google.android.material.appbar.AppBarLayout import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint diff --git a/app/src/main/java/app/pachli/util/ViewExtensions.kt b/app/src/main/java/app/pachli/util/ViewExtensions.kt index d1dada305..adc20f4ea 100644 --- a/app/src/main/java/app/pachli/util/ViewExtensions.kt +++ b/app/src/main/java/app/pachli/util/ViewExtensions.kt @@ -17,43 +17,6 @@ package app.pachli.util import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager2.widget.ViewPager2 -import timber.log.Timber - -/** - * Reduce ViewPager2's sensitivity to horizontal swipes. - */ -fun ViewPager2.reduceSwipeSensitivity() { - // ViewPager2 is very sensitive to horizontal motion when swiping vertically, and will - // trigger a page transition if the user's swipe is only a few tens of degrees off from - // vertical. This is a problem if the underlying content is a list that the user wants - // to scroll vertically -- it's far too easy to trigger an accidental horizontal swipe. - // - // One way to stop this is to reach in to ViewPager2's RecyclerView and adjust the amount - // of touch slop it has. - // - // See https://issuetracker.google.com/issues/139867645 and - // https://bladecoder.medium.com/fixing-recyclerview-nested-scrolling-in-opposite-direction-f587be5c1a04 - // for more (the approach in that Medium article works, but is still quite sensitive to - // horizontal movement while scrolling). - try { - val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView") - recyclerViewField.isAccessible = true - val recyclerView = recyclerViewField.get(this) as RecyclerView - - val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop") - touchSlopField.isAccessible = true - val touchSlop = touchSlopField.get(recyclerView) as Int - // Experimentally, 2 seems to be a sweet-spot, requiring a downward swipe that's at least - // 45 degrees off the vertical to trigger a change. This is consistent with maximum angle - // supported to open the nav. drawer. - val scaleFactor = 2 - touchSlopField.set(recyclerView, touchSlop * scaleFactor) - } catch (e: Exception) { - Timber.tag("reduceSwipeSensitibity").w(e) - } -} /** * TextViews with an ancestor RecyclerView can forget that they are selectable. Toggling diff --git a/core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt b/core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt index 94d7bd81f..acd5c9e92 100644 --- a/core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt +++ b/core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt @@ -549,12 +549,6 @@ class InstanceListActivityIntent(context: Context) : Intent() { } } -class LicenseActivityIntent(context: Context) : Intent() { - init { - setClassName(context, QuadrantConstants.LICENSE_ACTIVITY) - } -} - class ListActivityIntent(context: Context) : Intent() { init { setClassName(context, QuadrantConstants.LISTS_ACTIVITY) @@ -567,12 +561,6 @@ class LoginWebViewActivityIntent(context: Context) : Intent() { } } -class PrivacyPolicyActivityIntent(context: Context) : Intent() { - init { - setClassName(context, QuadrantConstants.PRIVACY_POLICY_ACTIVITY) - } -} - class ScheduledStatusActivityIntent(context: Context) : Intent() { init { setClassName(context, QuadrantConstants.SCHEDULED_STATUS_ACTIVITY) diff --git a/core/ui/src/main/kotlin/app/pachli/core/ui/ViewPager2Extensions.kt b/core/ui/src/main/kotlin/app/pachli/core/ui/ViewPager2Extensions.kt new file mode 100644 index 000000000..9e7148fbd --- /dev/null +++ b/core/ui/src/main/kotlin/app/pachli/core/ui/ViewPager2Extensions.kt @@ -0,0 +1,56 @@ +/* + * 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 . + */ + +package app.pachli.core.ui + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 +import timber.log.Timber + +/** + * Reduce ViewPager2's sensitivity to horizontal swipes. + */ +fun ViewPager2.reduceSwipeSensitivity() { + // ViewPager2 is very sensitive to horizontal motion when swiping vertically, and will + // trigger a page transition if the user's swipe is only a few tens of degrees off from + // vertical. This is a problem if the underlying content is a list that the user wants + // to scroll vertically -- it's far too easy to trigger an accidental horizontal swipe. + // + // One way to stop this is to reach in to ViewPager2's RecyclerView and adjust the amount + // of touch slop it has. + // + // See https://issuetracker.google.com/issues/139867645 and + // https://bladecoder.medium.com/fixing-recyclerview-nested-scrolling-in-opposite-direction-f587be5c1a04 + // for more (the approach in that Medium article works, but is still quite sensitive to + // horizontal movement while scrolling). + try { + val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView") + recyclerViewField.isAccessible = true + val recyclerView = recyclerViewField.get(this) as RecyclerView + + val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop") + touchSlopField.isAccessible = true + val touchSlop = touchSlopField.get(recyclerView) as Int + // Experimentally, 2 seems to be a sweet-spot, requiring a downward swipe that's at least + // 45 degrees off the vertical to trigger a change. This is consistent with maximum angle + // supported to open the nav. drawer. + val scaleFactor = 2 + touchSlopField.set(recyclerView, touchSlop * scaleFactor) + } catch (e: Exception) { + Timber.tag("reduceSwipeSensitibity").w(e) + } +} diff --git a/feature/about/build.gradle.kts b/feature/about/build.gradle.kts index 442d14498..a53f8516d 100644 --- a/feature/about/build.gradle.kts +++ b/feature/about/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { implementation(projects.core.activity) implementation(projects.core.common) implementation(projects.core.data) + implementation(projects.core.designsystem) implementation(projects.core.navigation) implementation(projects.core.ui) @@ -57,4 +58,7 @@ dependencies { implementation(libs.bundles.androidx) implementation(libs.bundles.aboutlibraries) + + // For FixedSizeDrawable + implementation(libs.glide.core) } diff --git a/feature/about/src/main/kotlin/app/pachli/feature/about/AboutActivity.kt b/feature/about/src/main/kotlin/app/pachli/feature/about/AboutActivity.kt index b13378a7a..867b0083a 100644 --- a/feature/about/src/main/kotlin/app/pachli/feature/about/AboutActivity.kt +++ b/feature/about/src/main/kotlin/app/pachli/feature/about/AboutActivity.kt @@ -17,137 +17,83 @@ package app.pachli.feature.about -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.os.Build +import android.annotation.SuppressLint import android.os.Bundle -import android.text.SpannableString -import android.text.SpannableStringBuilder -import android.text.method.LinkMovementMethod -import android.text.style.URLSpan -import android.text.util.Linkify -import android.widget.TextView -import android.widget.Toast -import androidx.annotation.StringRes -import androidx.lifecycle.lifecycleScope +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter import app.pachli.core.activity.BottomSheetActivity -import app.pachli.core.common.extensions.hide -import app.pachli.core.common.extensions.show -import app.pachli.core.common.util.versionName -import app.pachli.core.data.repository.InstanceInfoRepository -import app.pachli.core.navigation.LicenseActivityIntent -import app.pachli.core.navigation.PrivacyPolicyActivityIntent -import app.pachli.core.ui.NoUnderlineURLSpan +import app.pachli.core.designsystem.R as DR +import app.pachli.core.ui.reduceSwipeSensitivity import app.pachli.feature.about.databinding.ActivityAboutBinding +import com.bumptech.glide.request.target.FixedSizeDrawable +import com.google.android.material.tabs.TabLayoutMediator +import com.mikepenz.aboutlibraries.LibsBuilder import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject -import kotlinx.coroutines.launch @AndroidEntryPoint -class AboutActivity : BottomSheetActivity() { - @Inject - lateinit var instanceInfoRepository: InstanceInfoRepository - +class AboutActivity : BottomSheetActivity(), MenuProvider { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityAboutBinding.inflate(layoutInflater) setContentView(binding.root) - setSupportActionBar(binding.includedToolbar.toolbar) + setSupportActionBar(binding.toolbar) + binding.toolbar.run { + val navIconSize = resources.getDimensionPixelSize(DR.dimen.avatar_toolbar_nav_icon_size) + navigationIcon = FixedSizeDrawable( + AppCompatResources.getDrawable(this@AboutActivity, DR.mipmap.ic_launcher), + navIconSize, + navIconSize, + ) + } supportActionBar?.run { - setDisplayHomeAsUpEnabled(true) + setTitle(R.string.app_name) setDisplayShowHomeEnabled(true) } - setTitle(R.string.about_title_activity) + val adapter = AboutFragmentAdapter(this) + binding.pager.adapter = adapter + binding.pager.reduceSwipeSensitivity() - binding.versionTextView.text = getString( - R.string.about_app_version, - getString( - R.string.app_name, - ), - versionName(this), + TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position -> + tab.text = adapter.title(position) + }.attach() + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + @SuppressLint("SyntheticAccessor") + override fun handleOnBackPressed() { + if (binding.pager.currentItem != 0) binding.pager.currentItem = 0 else finish() + } + }, ) - - binding.deviceInfo.text = getString( - R.string.about_device_info, - Build.MANUFACTURER, - Build.MODEL, - Build.VERSION.RELEASE, - Build.VERSION.SDK_INT, - ) - - lifecycleScope.launch { - accountManager.activeAccount?.let { account -> - val instanceInfo = instanceInfoRepository.getInstanceInfo() - binding.accountInfo.text = getString( - R.string.about_account_info, - account.username, - account.domain, - instanceInfo.version, - ) - binding.accountInfoTitle.show() - binding.accountInfo.show() - } - } - - if (BuildConfig.CUSTOM_INSTANCE.isBlank()) { - binding.aboutPoweredBy.hide() - } - - binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_pachli_license) - binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site) - binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site) - - binding.aboutPrivacyPolicyTextView.setOnClickListener { - startActivity(PrivacyPolicyActivityIntent(this)) - } - - binding.appProfileButton.setOnClickListener { - viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL) - } - - binding.aboutLicensesButton.setOnClickListener { - startActivityWithSlideInAnimation(LicenseActivityIntent(this)) - } - - binding.copyDeviceInfo.setOnClickListener { - val text = "${binding.versionTextView.text}\n\nDevice:\n\n${binding.deviceInfo.text}\n\nAccount:\n\n${binding.accountInfo.text}" - val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Pachli version information", text) - clipboard.setPrimaryClip(clip) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - Toast.makeText( - this, - getString(R.string.about_copied), - Toast.LENGTH_SHORT, - ).show() - } - } } } -internal fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { - val text = SpannableString(context.getText(textId)) +class AboutFragmentAdapter(val activity: FragmentActivity) : FragmentStateAdapter(activity) { + override fun getItemCount() = 3 - Linkify.addLinks(text, Linkify.WEB_URLS) - - val builder = SpannableStringBuilder(text) - val urlSpans = text.getSpans(0, text.length, URLSpan::class.java) - for (span in urlSpans) { - val start = builder.getSpanStart(span) - val end = builder.getSpanEnd(span) - val flags = builder.getSpanFlags(span) - - val customSpan = NoUnderlineURLSpan(span.url) - - builder.removeSpan(span) - builder.setSpan(customSpan, start, end, flags) + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> AboutFragment.newInstance() + 1 -> LibsBuilder().supportFragment() + 2 -> PrivacyPolicyFragment.newInstance() + else -> throw IllegalStateException() + } } - setText(builder) - linksClickable = true - movementMethod = LinkMovementMethod.getInstance() + fun title(position: Int): CharSequence { + return when (position) { + 0 -> "About" + 1 -> activity.getString(R.string.title_licenses) + 2 -> activity.getString(R.string.about_privacy_policy) + else -> throw IllegalStateException() + } + } } diff --git a/feature/about/src/main/kotlin/app/pachli/feature/about/AboutFragment.kt b/feature/about/src/main/kotlin/app/pachli/feature/about/AboutFragment.kt new file mode 100644 index 000000000..9021618b9 --- /dev/null +++ b/feature/about/src/main/kotlin/app/pachli/feature/about/AboutFragment.kt @@ -0,0 +1,135 @@ +/* + * 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 . + */ + +package app.pachli.feature.about + +import android.content.ClipData +import android.content.ClipboardManager +import android.os.Build +import android.os.Bundle +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.method.LinkMovementMethod +import android.text.style.URLSpan +import android.text.util.Linkify +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat.getSystemService +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import app.pachli.core.activity.BottomSheetActivity +import app.pachli.core.common.extensions.hide +import app.pachli.core.common.extensions.show +import app.pachli.core.common.extensions.viewBinding +import app.pachli.core.common.util.versionName +import app.pachli.core.ui.NoUnderlineURLSpan +import app.pachli.feature.about.databinding.FragmentAboutBinding +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class AboutFragment : Fragment(R.layout.fragment_about) { + private val viewModel: AboutFragmentViewModel by viewModels() + + private val binding by viewBinding(FragmentAboutBinding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val version = getString( + R.string.about_app_version, + getString( + R.string.app_name, + ), + versionName(requireContext()), + ) + + binding.versionTextView.text = version + + val deviceInfo = getString( + R.string.about_device_info, + Build.MANUFACTURER, + Build.MODEL, + Build.VERSION.RELEASE, + Build.VERSION.SDK_INT, + ) + binding.deviceInfo.text = deviceInfo + + lifecycleScope.launch { + viewModel.accountInfo.collect { + binding.accountInfo.text = it + binding.accountInfoTitle.show() + binding.accountInfo.show() + binding.copyDeviceInfo.show() + } + } + + if (BuildConfig.CUSTOM_INSTANCE.isBlank()) { + binding.aboutPoweredBy.hide() + } + + binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_pachli_license) + binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site) + binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site) + + binding.appProfileButton.setOnClickListener { + (activity as? BottomSheetActivity)?.viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL) + } + + binding.copyDeviceInfo.setOnClickListener { + val text = "$version\n\nDevice:\n\n$deviceInfo\n\nAccount:\n\n${binding.accountInfo.text}" + val clipboard = getSystemService(requireContext(), ClipboardManager::class.java) as ClipboardManager + val clip = ClipData.newPlainText("Pachli version information", text) + clipboard.setPrimaryClip(clip) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText( + requireContext(), + getString(R.string.about_copied), + Toast.LENGTH_SHORT, + ).show() + } + } + } + + companion object { + fun newInstance() = AboutFragment() + } +} + +internal fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { + val text = SpannableString(context.getText(textId)) + + Linkify.addLinks(text, Linkify.WEB_URLS) + + val builder = SpannableStringBuilder(text) + val urlSpans = text.getSpans(0, text.length, URLSpan::class.java) + for (span in urlSpans) { + val start = builder.getSpanStart(span) + val end = builder.getSpanEnd(span) + val flags = builder.getSpanFlags(span) + + val customSpan = NoUnderlineURLSpan(span.url) + + builder.removeSpan(span) + builder.setSpan(customSpan, start, end, flags) + } + + setText(builder) + linksClickable = true + movementMethod = LinkMovementMethod.getInstance() +} diff --git a/feature/about/src/main/kotlin/app/pachli/feature/about/AboutFragmentViewModel.kt b/feature/about/src/main/kotlin/app/pachli/feature/about/AboutFragmentViewModel.kt new file mode 100644 index 000000000..ae0a90aa7 --- /dev/null +++ b/feature/about/src/main/kotlin/app/pachli/feature/about/AboutFragmentViewModel.kt @@ -0,0 +1,56 @@ +/* + * 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 . + */ + +package app.pachli.feature.about + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import app.pachli.core.accounts.AccountManager +import app.pachli.core.data.repository.InstanceInfoRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +@HiltViewModel +class AboutFragmentViewModel @Inject constructor( + private val application: Application, + private val accountManager: AccountManager, + private val instanceInfoRepository: InstanceInfoRepository, +) : AndroidViewModel(application) { + private val _accountInfo = MutableSharedFlow() + val accountInfo: Flow = _accountInfo.asSharedFlow() + + init { + viewModelScope.launch { + accountManager.activeAccount?.let { account -> + val instanceInfo = instanceInfoRepository.getInstanceInfo() + _accountInfo.emit( + application.getString( + R.string.about_account_info, + account.username, + account.domain, + instanceInfo.version, + ), + ) + } + } + } +} diff --git a/feature/about/src/main/kotlin/app/pachli/feature/about/LicenseActivity.kt b/feature/about/src/main/kotlin/app/pachli/feature/about/LicenseActivity.kt deleted file mode 100644 index 647689a66..000000000 --- a/feature/about/src/main/kotlin/app/pachli/feature/about/LicenseActivity.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2023 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 . - */ - -package app.pachli.feature.about - -import android.os.Bundle -import androidx.fragment.app.commit -import app.pachli.core.activity.BaseActivity -import app.pachli.feature.about.databinding.ActivityLicenseBinding -import com.mikepenz.aboutlibraries.LibsBuilder -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class LicenseActivity : BaseActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val binding = ActivityLicenseBinding.inflate(layoutInflater) - setContentView(binding.root) - - setSupportActionBar(binding.includedToolbar.toolbar) - supportActionBar?.run { - setDisplayHomeAsUpEnabled(true) - setDisplayShowHomeEnabled(true) - } - - setTitle(R.string.title_licenses) - - val fragment = LibsBuilder().supportFragment() - if (savedInstanceState == null) { - supportFragmentManager.commit { - setReorderingAllowed(true) - add(R.id.fragment_licenses, fragment) - } - } - } -} diff --git a/feature/about/src/main/kotlin/app/pachli/feature/about/PrivacyPolicyActivity.kt b/feature/about/src/main/kotlin/app/pachli/feature/about/PrivacyPolicyFragment.kt similarity index 65% rename from feature/about/src/main/kotlin/app/pachli/feature/about/PrivacyPolicyActivity.kt rename to feature/about/src/main/kotlin/app/pachli/feature/about/PrivacyPolicyFragment.kt index e19b73179..3a9b4a171 100644 --- a/feature/about/src/main/kotlin/app/pachli/feature/about/PrivacyPolicyActivity.kt +++ b/feature/about/src/main/kotlin/app/pachli/feature/about/PrivacyPolicyFragment.kt @@ -19,17 +19,23 @@ package app.pachli.feature.about import android.os.Bundle import android.util.Base64 -import app.pachli.core.activity.BaseActivity -import app.pachli.feature.about.databinding.ActivityPrivacyPolicyBinding +import android.view.View +import androidx.fragment.app.Fragment +import app.pachli.core.common.extensions.viewBinding +import app.pachli.feature.about.databinding.FragmentPrivacyPolicyBinding import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class PrivacyPolicyActivity : BaseActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val binding = ActivityPrivacyPolicyBinding.inflate(layoutInflater) - setContentView(binding.root) +class PrivacyPolicyFragment : Fragment(R.layout.fragment_privacy_policy) { + private val binding by viewBinding(FragmentPrivacyPolicyBinding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) val encoded = Base64.encodeToString(markdownR.html.PRIVACY_md.toByteArray(), Base64.NO_PADDING) binding.policy.loadData(encoded, "text/html", "base64") } + + companion object { + fun newInstance() = PrivacyPolicyFragment() + } } diff --git a/feature/about/src/main/res/layout/activity_about.xml b/feature/about/src/main/res/layout/activity_about.xml index 9e5d1ddd6..7a5e98cbe 100644 --- a/feature/about/src/main/res/layout/activity_about.xml +++ b/feature/about/src/main/res/layout/activity_about.xml @@ -1,4 +1,21 @@ + + - - - + android:layout_height="wrap_content" + android:elevation="@dimen/actionbar_elevation" + app:elevationOverlayEnabled="false"> - + + + app:tabGravity="fill" + app:tabMode="fixed" /> + - - - - - - - - - - - - - - - - - - - - - - - - - - -