From 05271d95a920a7b8bb7e3e45e5d6ef1aa43ce0e0 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 13 Jul 2024 20:01:07 +0530 Subject: [PATCH 01/16] Migrate about activity to Jetpack Compose --- app/build.gradle | 11 +- .../org/schabi/newpipe/about/AboutActivity.kt | 195 ++---------------- .../org/schabi/newpipe/about/AboutClasses.kt | 28 +++ .../org/schabi/newpipe/about/AboutScreen.kt | 96 +++++++++ .../java/org/schabi/newpipe/about/AboutTab.kt | 95 +++++++++ .../java/org/schabi/newpipe/about/License.kt | 11 - .../schabi/newpipe/about/LicenseFragment.kt | 140 ------------- .../newpipe/about/LicenseFragmentHelper.kt | 52 ----- .../org/schabi/newpipe/about/LicenseTab.kt | 184 +++++++++++++++++ .../schabi/newpipe/about/SoftwareComponent.kt | 17 -- .../schabi/newpipe/{ui => compose}/Toolbar.kt | 6 +- .../compose/screen/ScaffoldWithToolbar.kt | 40 ++++ .../newpipe/{ui => compose}/theme/Color.kt | 2 +- .../{ui => compose}/theme/SizeTokens.kt | 2 +- .../newpipe/{ui => compose}/theme/Theme.kt | 2 +- .../java/org/schabi/newpipe/ktx/Bundle.kt | 9 - app/src/main/res/layout/activity_about.xml | 39 ---- app/src/main/res/layout/fragment_about.xml | 148 ------------- app/src/main/res/layout/fragment_licenses.xml | 55 ----- .../res/layout/item_software_component.xml | 28 --- build.gradle | 2 +- 21 files changed, 467 insertions(+), 695 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/about/AboutClasses.kt create mode 100644 app/src/main/java/org/schabi/newpipe/about/AboutScreen.kt create mode 100644 app/src/main/java/org/schabi/newpipe/about/AboutTab.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/about/License.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt create mode 100644 app/src/main/java/org/schabi/newpipe/about/LicenseTab.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt rename app/src/main/java/org/schabi/newpipe/{ui => compose}/Toolbar.kt (97%) create mode 100644 app/src/main/java/org/schabi/newpipe/compose/screen/ScaffoldWithToolbar.kt rename app/src/main/java/org/schabi/newpipe/{ui => compose}/theme/Color.kt (98%) rename app/src/main/java/org/schabi/newpipe/{ui => compose}/theme/SizeTokens.kt (88%) rename app/src/main/java/org/schabi/newpipe/{ui => compose}/theme/Theme.kt (98%) delete mode 100644 app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt delete mode 100644 app/src/main/res/layout/activity_about.xml delete mode 100644 app/src/main/res/layout/fragment_about.xml delete mode 100644 app/src/main/res/layout/fragment_licenses.xml delete mode 100644 app/src/main/res/layout/item_software_component.xml diff --git a/app/build.gradle b/app/build.gradle index 9ea725ad9..50c08f883 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -106,7 +106,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = "1.5.3" + kotlinCompilerExtensionVersion = "1.5.14" } } @@ -230,9 +230,6 @@ dependencies { implementation "androidx.room:room-rxjava3:${androidxRoomVersion}" kapt "androidx.room:room-compiler:${androidxRoomVersion}" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - // Newer version specified to prevent accessibility regressions with RecyclerView, see: - // https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01 - implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02' implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}" implementation "androidx.work:work-rxjava3:${androidxWorkVersion}" implementation 'com.google.android.material:material:1.11.0' @@ -289,10 +286,12 @@ dependencies { implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final" // Jetpack Compose - implementation(platform('androidx.compose:compose-bom:2024.02.01')) - implementation 'androidx.compose.material3:material3' + implementation(platform('androidx.compose:compose-bom:2024.06.00')) + implementation 'androidx.compose.material3:material3:1.3.0-beta04' implementation 'androidx.activity:activity-compose' implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.ui:ui-text:1.7.0-beta05' // Needed for parsing HTML to AnnotatedString + implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0' /** Debugging **/ // Memory leak detection diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt index 0d0d0d48d..0741c272a 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt @@ -1,199 +1,28 @@ package org.schabi.newpipe.about import android.os.Bundle -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import androidx.annotation.StringRes +import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.viewpager2.adapter.FragmentStateAdapter -import com.google.android.material.tabs.TabLayoutMediator -import org.schabi.newpipe.BuildConfig +import androidx.compose.ui.res.stringResource import org.schabi.newpipe.R -import org.schabi.newpipe.databinding.ActivityAboutBinding -import org.schabi.newpipe.databinding.FragmentAboutBinding +import org.schabi.newpipe.compose.screen.ScaffoldWithToolbar +import org.schabi.newpipe.compose.theme.AppTheme import org.schabi.newpipe.util.Localization -import org.schabi.newpipe.util.ThemeHelper -import org.schabi.newpipe.util.external_communication.ShareUtils class AboutActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { Localization.assureCorrectAppLanguage(this) super.onCreate(savedInstanceState) - ThemeHelper.setTheme(this) - title = getString(R.string.title_activity_about) - val aboutBinding = ActivityAboutBinding.inflate(layoutInflater) - setContentView(aboutBinding.root) - setSupportActionBar(aboutBinding.aboutToolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - // Create the adapter that will return a fragment for each of the three - // primary sections of the activity. - val mAboutStateAdapter = AboutStateAdapter(this) - // Set up the ViewPager with the sections adapter. - aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter - TabLayoutMediator( - aboutBinding.aboutTabLayout, - aboutBinding.aboutViewPager2 - ) { tab, position -> - tab.setText(mAboutStateAdapter.getPageTitle(position)) - }.attach() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - finish() - return true - } - return super.onOptionsItemSelected(item) - } - - /** - * A placeholder fragment containing a simple view. - */ - class AboutFragment : Fragment() { - private fun Button.openLink(@StringRes url: Int) { - setOnClickListener { - ShareUtils.openUrlInApp(context, requireContext().getString(url)) + setContent { + AppTheme { + ScaffoldWithToolbar( + title = stringResource(R.string.title_activity_about), + onBackClick = { onBackPressedDispatcher.onBackPressed() } + ) { padding -> + AboutScreen(padding) + } } } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - FragmentAboutBinding.inflate(inflater, container, false).apply { - aboutAppVersion.text = BuildConfig.VERSION_NAME - aboutGithubLink.openLink(R.string.github_url) - aboutDonationLink.openLink(R.string.donation_url) - aboutWebsiteLink.openLink(R.string.website_url) - aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url) - faqLink.openLink(R.string.faq_url) - return root - } - } - } - - /** - * A [FragmentStateAdapter] that returns a fragment corresponding to - * one of the sections/tabs/pages. - */ - private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { - private val posAbout = 0 - private val posLicense = 1 - private val totalCount = 2 - - override fun createFragment(position: Int): Fragment { - return when (position) { - posAbout -> AboutFragment() - posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS) - else -> throw IllegalArgumentException("Unknown position for ViewPager2") - } - } - - override fun getItemCount(): Int { - // Show 2 total pages. - return totalCount - } - - fun getPageTitle(position: Int): Int { - return when (position) { - posAbout -> R.string.tab_about - posLicense -> R.string.tab_licenses - else -> throw IllegalArgumentException("Unknown position for ViewPager2") - } - } - } - - companion object { - /** - * List of all software components. - */ - private val SOFTWARE_COMPONENTS = arrayListOf( - SoftwareComponent( - "ACRA", "2013", "Kevin Gaudin", - "https://github.com/ACRA/acra", StandardLicenses.APACHE2 - ), - SoftwareComponent( - "AndroidX", "2005 - 2011", "The Android Open Source Project", - "https://developer.android.com/jetpack", StandardLicenses.APACHE2 - ), - SoftwareComponent( - "ExoPlayer", "2014 - 2020", "Google, Inc.", - "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2 - ), - SoftwareComponent( - "GigaGet", "2014 - 2015", "Peter Cai", - "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3 - ), - SoftwareComponent( - "Groupie", "2016", "Lisa Wray", - "https://github.com/lisawray/groupie", StandardLicenses.MIT - ), - SoftwareComponent( - "Icepick", "2015", "Frankie Sardo", - "https://github.com/frankiesardo/icepick", StandardLicenses.EPL1 - ), - SoftwareComponent( - "Jsoup", "2009 - 2020", "Jonathan Hedley", - "https://github.com/jhy/jsoup", StandardLicenses.MIT - ), - SoftwareComponent( - "Markwon", "2019", "Dimitry Ivanov", - "https://github.com/noties/Markwon", StandardLicenses.APACHE2 - ), - SoftwareComponent( - "Material Components for Android", "2016 - 2020", "Google, Inc.", - "https://github.com/material-components/material-components-android", - StandardLicenses.APACHE2 - ), - SoftwareComponent( - "NewPipe Extractor", "2017 - 2020", "Christian Schabesberger", - "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3 - ), - SoftwareComponent( - "NoNonsense-FilePicker", "2016", "Jonas Kalderstam", - "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2 - ), - SoftwareComponent( - "OkHttp", "2019", "Square, Inc.", - "https://square.github.io/okhttp/", StandardLicenses.APACHE2 - ), - SoftwareComponent( - "Coil", "2023", "Coil Contributors", - "https://coil-kt.github.io/coil/", StandardLicenses.APACHE2 - ), - SoftwareComponent( - "PrettyTime", "2012 - 2020", "Lincoln Baxter, III", - "https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2 - ), - SoftwareComponent( - "ProcessPhoenix", "2015", "Jake Wharton", - "https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2 - ), - SoftwareComponent( - "RxAndroid", "2015", "The RxAndroid authors", - "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2 - ), - SoftwareComponent( - "RxBinding", "2015", "Jake Wharton", - "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2 - ), - SoftwareComponent( - "RxJava", "2016 - 2020", "RxJava Contributors", - "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2 - ), - SoftwareComponent( - "SearchPreference", "2018", "ByteHamster", - "https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT - ), - ) } } diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutClasses.kt b/app/src/main/java/org/schabi/newpipe/about/AboutClasses.kt new file mode 100644 index 000000000..bacd944c6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/about/AboutClasses.kt @@ -0,0 +1,28 @@ +package org.schabi.newpipe.about + +import android.content.Context +import androidx.annotation.StringRes + +class AboutData( + @StringRes val title: Int, + @StringRes val description: Int, + @StringRes val buttonText: Int, + @StringRes val url: Int +) + +/** + * Class for storing information about a software license. + */ +class License(val name: String, val abbreviation: String, val filename: String) { + fun getFormattedLicense(context: Context): String { + return context.assets.open(filename).bufferedReader().use { it.readText() } + } +} + +class SoftwareComponent( + val name: String, + val years: String, + val copyrightOwner: String, + val link: String, + val license: License +) diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutScreen.kt b/app/src/main/java/org/schabi/newpipe/about/AboutScreen.kt new file mode 100644 index 000000000..088380f74 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/about/AboutScreen.kt @@ -0,0 +1,96 @@ +package org.schabi.newpipe.about + +import android.content.res.Configuration +import androidx.collection.intListOf +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import my.nanihadesuka.compose.ColumnScrollbar +import org.schabi.newpipe.R +import org.schabi.newpipe.compose.theme.AppTheme + +private val TITLES = intListOf(R.string.tab_about, R.string.tab_licenses) + +@Composable +@NonRestartableComposable +fun AboutScreen(padding: PaddingValues) { + Column(modifier = Modifier.padding(padding)) { + var tabIndex by rememberSaveable { mutableIntStateOf(0) } + val pagerState = rememberPagerState { TITLES.size } + + LaunchedEffect(tabIndex) { + pagerState.animateScrollToPage(tabIndex) + } + LaunchedEffect(pagerState.currentPage) { + tabIndex = pagerState.currentPage + } + + TabRow(selectedTabIndex = tabIndex) { + TITLES.forEachIndexed { index, titleId -> + Tab( + text = { Text(text = stringResource(titleId)) }, + selected = tabIndex == index, + onClick = { tabIndex = index } + ) + } + } + + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { page -> + val scrollState = rememberScrollState() + + ColumnScrollbar(state = scrollState) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 10.dp) + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (page == 0) { + AboutTab() + } else { + LicenseTab() + } + } + } + } + } +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AboutScreenPreview() { + AppTheme { + Surface(color = MaterialTheme.colorScheme.background) { + AboutScreen(PaddingValues(8.dp)) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutTab.kt b/app/src/main/java/org/schabi/newpipe/about/AboutTab.kt new file mode 100644 index 000000000..f391c7790 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/about/AboutTab.kt @@ -0,0 +1,95 @@ +package org.schabi.newpipe.about + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import org.schabi.newpipe.BuildConfig +import org.schabi.newpipe.R +import org.schabi.newpipe.util.external_communication.ShareUtils + +private val ABOUT_ITEMS = listOf( + AboutData(R.string.faq_title, R.string.faq_description, R.string.faq, R.string.faq_url), + AboutData( + R.string.contribution_title, R.string.contribution_encouragement, + R.string.view_on_github, R.string.github_url + ), + AboutData( + R.string.donation_title, R.string.donation_encouragement, R.string.give_back, + R.string.donation_url + ), + AboutData( + R.string.website_title, R.string.website_encouragement, R.string.open_in_browser, + R.string.website_url + ), + AboutData( + R.string.privacy_policy_title, R.string.privacy_policy_encouragement, + R.string.read_privacy_policy, R.string.privacy_policy_url + ) +) + +@Composable +@NonRestartableComposable +fun AboutTab() { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentSize(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.mipmap.ic_launcher_foreground), + contentDescription = stringResource(R.string.app_name), + modifier = Modifier + .clip(CircleShape) + .background(Color.Red) + ) + Text( + style = MaterialTheme.typography.titleLarge, + text = stringResource(R.string.app_name) + ) + Text(text = BuildConfig.VERSION_NAME) + } + + Text(text = stringResource(R.string.app_description)) + + for (item in ABOUT_ITEMS) { + AboutItem(item) + } +} + +@Composable +@NonRestartableComposable +private fun AboutItem(aboutData: AboutData) { + Column { + Text( + text = stringResource(aboutData.title), + style = MaterialTheme.typography.titleMedium + ) + Text(text = stringResource(aboutData.description)) + + val context = LocalContext.current + TextButton( + modifier = Modifier.fillMaxWidth() + .wrapContentWidth(Alignment.End), + onClick = { ShareUtils.openUrlInApp(context, context.getString(aboutData.url)) } + ) { + Text(text = stringResource(aboutData.buttonText)) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/about/License.kt b/app/src/main/java/org/schabi/newpipe/about/License.kt deleted file mode 100644 index 117ff9bf5..000000000 --- a/app/src/main/java/org/schabi/newpipe/about/License.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.schabi.newpipe.about - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import java.io.Serializable - -/** - * Class for storing information about a software license. - */ -@Parcelize -class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt deleted file mode 100644 index 9f5ad2a7a..000000000 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt +++ /dev/null @@ -1,140 +0,0 @@ -package org.schabi.newpipe.about - -import android.os.Bundle -import android.util.Base64 -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.webkit.WebView -import androidx.appcompat.app.AlertDialog -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.schedulers.Schedulers -import org.schabi.newpipe.BuildConfig -import org.schabi.newpipe.R -import org.schabi.newpipe.databinding.FragmentLicensesBinding -import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding -import org.schabi.newpipe.ktx.parcelableArrayList -import org.schabi.newpipe.util.Localization -import org.schabi.newpipe.util.external_communication.ShareUtils - -/** - * Fragment containing the software licenses. - */ -class LicenseFragment : Fragment() { - private lateinit var softwareComponents: List - private var activeSoftwareComponent: SoftwareComponent? = null - private val compositeDisposable = CompositeDisposable() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - softwareComponents = arguments?.parcelableArrayList(ARG_COMPONENTS)!! - .sortedBy { it.name } // Sort components by name - activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent - } - - override fun onDestroy() { - compositeDisposable.dispose() - super.onDestroy() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val binding = FragmentLicensesBinding.inflate(inflater, container, false) - binding.licensesAppReadLicense.setOnClickListener { - compositeDisposable.add( - showLicense(NEWPIPE_SOFTWARE_COMPONENT) - ) - } - for (component in softwareComponents) { - val componentBinding = ItemSoftwareComponentBinding - .inflate(inflater, container, false) - componentBinding.name.text = component.name - componentBinding.copyright.text = getString( - R.string.copyright, - component.years, - component.copyrightOwner, - component.license.abbreviation - ) - val root: View = componentBinding.root - root.tag = component - root.setOnClickListener { - compositeDisposable.add( - showLicense(component) - ) - } - binding.licensesSoftwareComponents.addView(root) - registerForContextMenu(root) - } - activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) } - return binding.root - } - - override fun onSaveInstanceState(savedInstanceState: Bundle) { - super.onSaveInstanceState(savedInstanceState) - activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) } - } - - private fun showLicense( - softwareComponent: SoftwareComponent - ): Disposable { - return if (context == null) { - Disposable.empty() - } else { - val context = requireContext() - activeSoftwareComponent = softwareComponent - Observable.fromCallable { getFormattedLicense(context, softwareComponent.license) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { formattedLicense -> - val webViewData = Base64.encodeToString( - formattedLicense.toByteArray(), Base64.NO_PADDING - ) - val webView = WebView(context) - webView.loadData(webViewData, "text/html; charset=UTF-8", "base64") - - Localization.assureCorrectAppLanguage(context) - val builder = AlertDialog.Builder(requireContext()) - .setTitle(softwareComponent.name) - .setView(webView) - .setOnCancelListener { activeSoftwareComponent = null } - .setOnDismissListener { activeSoftwareComponent = null } - .setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() } - - if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) { - builder.setNeutralButton(R.string.open_website_license) { _, _ -> - ShareUtils.openUrlInApp(requireContext(), softwareComponent.link) - } - } - - builder.show() - } - } - } - - companion object { - private const val ARG_COMPONENTS = "components" - private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT" - private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent( - "NewPipe", - "2014-2023", - "Team NewPipe", - "https://newpipe.net/", - StandardLicenses.GPL3, - BuildConfig.VERSION_NAME - ) - - fun newInstance(softwareComponents: ArrayList): LicenseFragment { - val fragment = LicenseFragment() - fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents) - return fragment - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt deleted file mode 100644 index 56e21c88a..000000000 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt +++ /dev/null @@ -1,52 +0,0 @@ -package org.schabi.newpipe.about - -import android.content.Context -import org.schabi.newpipe.R -import org.schabi.newpipe.util.ThemeHelper -import java.io.IOException - -/** - * @param context the context to use - * @param license the license - * @return String which contains a HTML formatted license page - * styled according to the context's theme - */ -fun getFormattedLicense(context: Context, license: License): String { - try { - return context.assets.open(license.filename).bufferedReader().use { it.readText() } - // split the HTML file and insert the stylesheet into the HEAD of the file - .replace("", "") - } catch (e: IOException) { - throw IllegalArgumentException("Could not get license file: ${license.filename}", e) - } -} - -/** - * @param context the Android context - * @return String which is a CSS stylesheet according to the context's theme - */ -fun getLicenseStylesheet(context: Context): String { - val isLightTheme = ThemeHelper.isLightThemeSelected(context) - val licenseBackgroundColor = getHexRGBColor( - context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color - ) - val licenseTextColor = getHexRGBColor( - context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color - ) - val youtubePrimaryColor = getHexRGBColor( - context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color - ) - return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" + - "a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}" -} - -/** - * Cast R.color to a hexadecimal color value. - * - * @param context the context to use - * @param color the color number from R.color - * @return a six characters long String with hexadecimal RGB values - */ -fun getHexRGBColor(context: Context, color: Int): String { - return context.getString(color).substring(3) -} diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseTab.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseTab.kt new file mode 100644 index 000000000..19f687455 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseTab.kt @@ -0,0 +1,184 @@ +package org.schabi.newpipe.about + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.text.style.TextDecoration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.schabi.newpipe.R + +private val SOFTWARE_COMPONENTS = listOf( + SoftwareComponent( + "ACRA", "2013", "Kevin Gaudin", + "https://github.com/ACRA/acra", StandardLicenses.APACHE2 + ), + SoftwareComponent( + "AndroidX", "2005 - 2011", "The Android Open Source Project", + "https://developer.android.com/jetpack", StandardLicenses.APACHE2 + ), + SoftwareComponent( + "ExoPlayer", "2014 - 2020", "Google, Inc.", + "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2 + ), + SoftwareComponent( + "GigaGet", "2014 - 2015", "Peter Cai", + "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3 + ), + SoftwareComponent( + "Groupie", "2016", "Lisa Wray", + "https://github.com/lisawray/groupie", StandardLicenses.MIT + ), + SoftwareComponent( + "Icepick", "2015", "Frankie Sardo", + "https://github.com/frankiesardo/icepick", StandardLicenses.EPL1 + ), + SoftwareComponent( + "Jsoup", "2009 - 2020", "Jonathan Hedley", + "https://github.com/jhy/jsoup", StandardLicenses.MIT + ), + SoftwareComponent( + "LazyColumnScrollbar", "2024", "nani", + "https://github.com/nanihadesuka/LazyColumnScrollbar", StandardLicenses.MIT + ), + SoftwareComponent( + "Markwon", "2019", "Dimitry Ivanov", + "https://github.com/noties/Markwon", StandardLicenses.APACHE2 + ), + SoftwareComponent( + "Material Components for Android", "2016 - 2020", "Google, Inc.", + "https://github.com/material-components/material-components-android", + StandardLicenses.APACHE2 + ), + SoftwareComponent( + "NewPipe Extractor", "2017 - 2020", "Christian Schabesberger", + "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3 + ), + SoftwareComponent( + "NoNonsense-FilePicker", "2016", "Jonas Kalderstam", + "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2 + ), + SoftwareComponent( + "OkHttp", "2019", "Square, Inc.", + "https://square.github.io/okhttp/", StandardLicenses.APACHE2 + ), + SoftwareComponent( + "Picasso", "2013", "Square, Inc.", + "https://square.github.io/picasso/", StandardLicenses.APACHE2 + ), + SoftwareComponent( + "PrettyTime", "2012 - 2020", "Lincoln Baxter, III", + "https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2 + ), + SoftwareComponent( + "ProcessPhoenix", "2015", "Jake Wharton", + "https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2 + ), + SoftwareComponent( + "RxAndroid", "2015", "The RxAndroid authors", + "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2 + ), + SoftwareComponent( + "RxBinding", "2015", "Jake Wharton", + "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2 + ), + SoftwareComponent( + "RxJava", "2016 - 2020", "RxJava Contributors", + "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2 + ), + SoftwareComponent( + "SearchPreference", "2018", "ByteHamster", + "https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT + ) +) + +@Composable +@NonRestartableComposable +fun LicenseTab() { + var selectedLicense by remember { mutableStateOf(null) } + val onClick = remember { + { it: SoftwareComponent -> selectedLicense = it } + } + + Text( + text = stringResource(R.string.app_license_title), + style = MaterialTheme.typography.titleLarge + ) + Text( + text = stringResource(R.string.app_license), + style = MaterialTheme.typography.bodyMedium + ) + + Text( + text = stringResource(R.string.title_licenses), + style = MaterialTheme.typography.titleLarge, + ) + for (component in SOFTWARE_COMPONENTS) { + LicenseItem(component, onClick) + } + + selectedLicense?.let { + var formattedLicense by remember { mutableStateOf("") } + + val context = LocalContext.current + LaunchedEffect(key1 = it) { + formattedLicense = withContext(Dispatchers.IO) { + it.license.getFormattedLicense(context) + } + } + + AlertDialog( + onDismissRequest = { selectedLicense = null }, + confirmButton = {}, + title = { Text(text = it.name) }, + text = { + val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline)) + Text( + modifier = Modifier.verticalScroll(rememberScrollState()), + text = AnnotatedString.fromHtml(formattedLicense, styles) + ) + } + ) + } +} + +@Composable +@NonRestartableComposable +private fun LicenseItem( + softwareComponent: SoftwareComponent, + onClick: (SoftwareComponent) -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick(softwareComponent) } + ) { + Text(text = softwareComponent.name) + Text( + style = MaterialTheme.typography.bodyMedium, + text = stringResource( + R.string.copyright, softwareComponent.years, + softwareComponent.copyrightOwner, softwareComponent.license.abbreviation + ) + ) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt b/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt deleted file mode 100644 index 262641caa..000000000 --- a/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.schabi.newpipe.about - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import java.io.Serializable - -@Parcelize -class SoftwareComponent -@JvmOverloads -constructor( - val name: String, - val years: String, - val copyrightOwner: String, - val link: String, - val license: License, - val version: String? = null -) : Parcelable, Serializable diff --git a/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt b/app/src/main/java/org/schabi/newpipe/compose/Toolbar.kt similarity index 97% rename from app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt rename to app/src/main/java/org/schabi/newpipe/compose/Toolbar.kt index b788932a2..469d88ec0 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/Toolbar.kt @@ -1,4 +1,4 @@ -package org.schabi.newpipe.ui +package org.schabi.newpipe.compose import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -27,8 +27,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import org.schabi.newpipe.R -import org.schabi.newpipe.ui.theme.AppTheme -import org.schabi.newpipe.ui.theme.SizeTokens +import org.schabi.newpipe.compose.theme.AppTheme +import org.schabi.newpipe.compose.theme.SizeTokens @Composable fun TextAction(text: String, modifier: Modifier = Modifier) { diff --git a/app/src/main/java/org/schabi/newpipe/compose/screen/ScaffoldWithToolbar.kt b/app/src/main/java/org/schabi/newpipe/compose/screen/ScaffoldWithToolbar.kt new file mode 100644 index 000000000..52e982c3b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/compose/screen/ScaffoldWithToolbar.kt @@ -0,0 +1,40 @@ +package org.schabi.newpipe.compose.screen + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ScaffoldWithToolbar( + title: String, + onBackClick: () -> Unit, + actions: @Composable RowScope.() -> Unit = {}, + content: @Composable (PaddingValues) -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = title) }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + ) + } + }, + actions = actions + ) + }, + content = content + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt b/app/src/main/java/org/schabi/newpipe/compose/theme/Color.kt similarity index 98% rename from app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt rename to app/src/main/java/org/schabi/newpipe/compose/theme/Color.kt index b61906ebe..0aa330390 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/theme/Color.kt @@ -1,4 +1,4 @@ -package org.schabi.newpipe.ui.theme +package org.schabi.newpipe.compose.theme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/org/schabi/newpipe/ui/theme/SizeTokens.kt b/app/src/main/java/org/schabi/newpipe/compose/theme/SizeTokens.kt similarity index 88% rename from app/src/main/java/org/schabi/newpipe/ui/theme/SizeTokens.kt rename to app/src/main/java/org/schabi/newpipe/compose/theme/SizeTokens.kt index d8104d7ae..274fa9d43 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/theme/SizeTokens.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/theme/SizeTokens.kt @@ -1,4 +1,4 @@ -package org.schabi.newpipe.ui.theme +package org.schabi.newpipe.compose.theme import androidx.compose.ui.unit.dp diff --git a/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt b/app/src/main/java/org/schabi/newpipe/compose/theme/Theme.kt similarity index 98% rename from app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt rename to app/src/main/java/org/schabi/newpipe/compose/theme/Theme.kt index 846794d72..1c9325f96 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt +++ b/app/src/main/java/org/schabi/newpipe/compose/theme/Theme.kt @@ -1,4 +1,4 @@ -package org.schabi.newpipe.ui.theme +package org.schabi.newpipe.compose.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme diff --git a/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt b/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt deleted file mode 100644 index 61721d546..000000000 --- a/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.schabi.newpipe.ktx - -import android.os.Bundle -import android.os.Parcelable -import androidx.core.os.BundleCompat - -inline fun Bundle.parcelableArrayList(key: String?): ArrayList? { - return BundleCompat.getParcelableArrayList(this, key, T::class.java) -} diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml deleted file mode 100644 index 661c4affc..000000000 --- a/app/src/main/res/layout/activity_about.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml deleted file mode 100644 index 5e6e11d00..000000000 --- a/app/src/main/res/layout/fragment_about.xml +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - - - - - - - -