Skip to content

Commit

Permalink
Migrate about activity to Jetpack Compose
Browse files Browse the repository at this point in the history
  • Loading branch information
Isira-Seneviratne committed Jul 14, 2024
1 parent 5062d38 commit 047c288
Show file tree
Hide file tree
Showing 21 changed files with 468 additions and 696 deletions.
13 changes: 6 additions & 7 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ android {
}

composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
kotlinCompilerExtensionVersion = "1.5.14"
}
}

Expand Down Expand Up @@ -203,7 +203,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.0'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.1'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'

/** Checkstyle **/
Expand All @@ -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'
Expand Down Expand Up @@ -290,10 +287,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
Expand Down
195 changes: 12 additions & 183 deletions app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
Original file line number Diff line number Diff line change
@@ -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))
}
}

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")
setContent {
AppTheme {
ScaffoldWithToolbar(
title = stringResource(R.string.title_activity_about),
onBackClick = { onBackPressedDispatcher.onBackPressed() }
) { padding ->
AboutScreen(padding)
}
}
}

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(
"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
),
)
}
}
28 changes: 28 additions & 0 deletions app/src/main/java/org/schabi/newpipe/about/AboutClasses.kt
Original file line number Diff line number Diff line change
@@ -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
)
96 changes: 96 additions & 0 deletions app/src/main/java/org/schabi/newpipe/about/AboutScreen.kt
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
Loading

0 comments on commit 047c288

Please sign in to comment.