diff --git a/app/build.gradle b/app/build.gradle index 98270fa70..aad6b34d1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,16 +1,18 @@ import com.android.tools.profgen.ArtProfileKt import com.android.tools.profgen.ArtProfileSerializer import com.android.tools.profgen.DexFile +import com.mikepenz.aboutlibraries.plugin.DuplicateMode plugins { - id "com.android.application" - id "kotlin-android" - id "kotlin-kapt" - id "kotlin-parcelize" - id "checkstyle" - id "org.sonarqube" version "4.0.0.2929" - id "org.jetbrains.kotlin.plugin.compose" version "${kotlin_version}" - id 'com.google.dagger.hilt.android' + alias libs.plugins.android.application + alias libs.plugins.kotlin.android + alias libs.plugins.kotlin.compose + alias libs.plugins.kotlin.kapt + alias libs.plugins.kotlin.parcelize + alias libs.plugins.checkstyle + alias libs.plugins.sonarqube + alias libs.plugins.hilt + alias libs.plugins.aboutlibraries } android { @@ -109,25 +111,6 @@ android { } } -ext { - checkstyleVersion = '10.12.1' - - androidxLifecycleVersion = '2.6.2' - androidxRoomVersion = '2.6.1' - androidxWorkVersion = '2.8.1' - - stateSaverVersion = '1.4.1' - exoPlayerVersion = '2.18.7' - googleAutoServiceVersion = '1.1.1' - groupieVersion = '2.10.1' - markwonVersion = '4.6.2' - - leakCanaryVersion = '2.12' - stethoVersion = '1.6.0' - - coilVersion = '3.0.3' -} - configurations { checkstyle ktlint @@ -137,7 +120,7 @@ checkstyle { getConfigDirectory().set(rootProject.file("checkstyle")) ignoreFailures false showViolations true - toolVersion = checkstyleVersion + toolVersion = libs.versions.checkstyle.get() } tasks.register('runCheckstyle', Checkstyle) { @@ -179,11 +162,13 @@ tasks.register('formatKtlint', JavaExec) { jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") } +apply from: 'check-dependencies.gradle' + afterEvaluate { if (!System.properties.containsKey('skipFormatKtlint')) { preDebugBuild.dependsOn formatKtlint } - preDebugBuild.dependsOn runCheckstyle, runKtlint + preDebugBuild.dependsOn runCheckstyle, runKtlint, checkDependenciesOrder } sonar { @@ -198,148 +183,153 @@ kapt { correctErrorTypes true } +aboutLibraries { + // note: offline mode prevents the plugin from fetching licenses at build time, which would be + // harmful for reproducible builds + offlineMode = true + duplicationMode = DuplicateMode.MERGE +} + dependencies { /** Desugaring **/ - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4' + coreLibraryDesugaring libs.desugar.jdk.libs.nio /** NewPipe libraries **/ - // You can use a local version by uncommenting a few lines in settings.gradle - // Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub - // 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' - // WORKAROUND: v0.24.2 can't be resolved by jitpack -> use git commit hash instead - implementation 'com.github.TeamNewPipe:NewPipeExtractor:d3d5f2b3f03a5f2b479b9f6fdf1c2555cbb9de0e' - implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' + implementation libs.teamnewpipe.nanojson + implementation libs.teamnewpipe.newpipe.extractor + implementation libs.teamnewpipe.nononsense.filepicker /** Checkstyle **/ - checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" - ktlint 'com.pinterest:ktlint:0.45.2' + checkstyle libs.tools.checkstyle + ktlint libs.tools.ktlint /** Kotlin **/ - implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" + implementation libs.kotlin.stdlib /** AndroidX **/ - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.documentfile:documentfile:1.0.1' - implementation 'androidx.fragment:fragment-compose:1.8.2' - implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}" - implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' - implementation 'androidx.media:media:1.7.0' - implementation 'androidx.preference:preference:1.2.1' - implementation 'androidx.recyclerview:recyclerview:1.3.2' - implementation "androidx.room:room-runtime:${androidxRoomVersion}" - implementation "androidx.room:room-rxjava3:${androidxRoomVersion}" - kapt "androidx.room:room-compiler:${androidxRoomVersion}" - implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + implementation libs.androidx.appcompat + implementation libs.androidx.cardview + implementation libs.androidx.constraintlayout + implementation libs.androidx.core.ktx + implementation libs.androidx.documentfile + implementation libs.androidx.fragment.compose + implementation libs.androidx.lifecycle.livedata + implementation libs.androidx.lifecycle.viewmodel + implementation libs.androidx.localbroadcastmanager + implementation libs.androidx.media + implementation libs.androidx.preference + implementation libs.androidx.recyclerview + implementation libs.androidx.room.runtime + implementation libs.androidx.room.rxjava3 + kapt libs.androidx.room.compiler + implementation libs.androidx.swiperefreshlayout // 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' + implementation libs.androidx.viewpager2 + implementation libs.androidx.work.runtime + implementation libs.androidx.work.rxjava3 + implementation libs.androidx.material /** Third-party libraries **/ // Instance state boilerplate elimination - implementation 'com.github.livefront:bridge:v2.0.2' - implementation "com.evernote:android-state:$stateSaverVersion" - kapt "com.evernote:android-state-processor:$stateSaverVersion" + implementation libs.livefront.bridge + implementation libs.android.state + kapt libs.android.state.processor // HTML parser - implementation "org.jsoup:jsoup:1.17.2" + implementation libs.jsoup // HTTP client - implementation "com.squareup.okhttp3:okhttp:4.12.0" + implementation libs.okhttp // Media player - implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}" + implementation libs.exoplayer.core + implementation libs.exoplayer.dash + implementation libs.exoplayer.database + implementation libs.exoplayer.datasource + implementation libs.exoplayer.hls + implementation libs.exoplayer.smoothstreaming + implementation libs.exoplayer.ui + implementation libs.extension.mediasession // Metadata generator for service descriptors - compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}" - kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}" + compileOnly libs.auto.service + kapt libs.auto.service.kapt // Manager for complex RecyclerView layouts - implementation "com.github.lisawray.groupie:groupie:${groupieVersion}" - implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}" + implementation libs.lisawray.groupie + implementation libs.lisawray.groupie.viewbinding // Image loading - implementation "io.coil-kt.coil3:coil-compose:${coilVersion}" - implementation "io.coil-kt.coil3:coil-network-okhttp:${coilVersion}" + implementation libs.coil.compose + implementation libs.coil.network.okhttp // Markdown library for Android - implementation "io.noties.markwon:core:${markwonVersion}" - implementation "io.noties.markwon:linkify:${markwonVersion}" + implementation libs.markwon.core + implementation libs.markwon.linkify // Crash reporting - implementation "ch.acra:acra-core:5.11.3" + implementation libs.acra.core // Properly restarting - implementation 'com.jakewharton:process-phoenix:2.1.2' + implementation libs.process.phoenix // Reactive extensions for Java VM - implementation "io.reactivex.rxjava3:rxjava:3.1.8" - implementation "io.reactivex.rxjava3:rxandroid:3.0.2" + implementation libs.rxjava3.rxjava + implementation libs.rxjava3.rxandroid // RxJava binding APIs for Android UI widgets - implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0" + implementation libs.rxbinding4.rxbinding // Date and time formatting - implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final" + implementation libs.prettytime // Jetpack Compose - implementation(platform('androidx.compose:compose-bom:2024.10.01')) - implementation 'androidx.compose.material3:material3' - implementation 'androidx.compose.material3.adaptive:adaptive' - implementation 'androidx.activity:activity-compose' - implementation 'androidx.compose.ui:ui-tooling-preview' - implementation 'androidx.lifecycle:lifecycle-viewmodel-compose' - implementation 'androidx.compose.ui:ui-text' // Needed for parsing HTML to AnnotatedString - implementation 'androidx.compose.material:material-icons-extended' + implementation(platform(libs.androidx.compose.bom)) + implementation libs.androidx.compose.material3 + implementation libs.androidx.compose.adaptive + implementation libs.androidx.activity.compose + implementation libs.androidx.compose.ui.tooling.preview + implementation libs.androidx.lifecycle.viewmodel.compose + implementation libs.androidx.compose.ui.text // Needed for parsing HTML to AnnotatedString + implementation libs.androidx.compose.material.icons.extended // Jetpack Compose related dependencies - implementation 'androidx.paging:paging-compose:3.3.2' - implementation "androidx.navigation:navigation-compose:2.8.3" + implementation libs.androidx.paging.compose + implementation libs.androidx.navigation.compose // Coroutines interop - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.1' + implementation libs.kotlinx.coroutines.rx3 + + // Library loading for About screen + implementation libs.aboutlibraries.compose.m3 // Hilt - implementation("com.google.dagger:hilt-android:2.51.1") - kapt("com.google.dagger:hilt-compiler:2.51.1") + implementation libs.hilt.android + kapt(libs.hilt.compiler) // Scroll - implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0' + implementation libs.lazycolumnscrollbar /** Debugging **/ // Memory leak detection - debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" - debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}" - debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}" + debugImplementation libs.leakcanary.object.watcher + debugImplementation libs.leakcanary.plumber.android + debugImplementation libs.leakcanary.android.core // Debug bridge for Android - debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" - debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}" + debugImplementation libs.stetho + debugImplementation libs.stetho.okhttp3 // Jetpack Compose - debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation libs.androidx.compose.ui.tooling /** Testing **/ - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:5.6.0' + testImplementation libs.junit + testImplementation libs.mockito.core - androidTestImplementation "androidx.test.ext:junit:1.1.5" - androidTestImplementation "androidx.test:runner:1.5.2" - androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}" - androidTestImplementation "org.assertj:assertj-core:3.24.2" + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.runner + androidTestImplementation libs.androidx.room.testing + androidTestImplementation libs.assertj.core } static String getGitWorkingBranch() { diff --git a/app/check-dependencies.gradle b/app/check-dependencies.gradle new file mode 100644 index 000000000..7646bc584 --- /dev/null +++ b/app/check-dependencies.gradle @@ -0,0 +1,48 @@ +tasks.register('checkDependenciesOrder') { + group = 'verification' + description = 'Checks that each section in libs.versions.toml is sorted alphabetically' + + def tomlFile = file('../gradle/libs.versions.toml') + + doLast { + if (!tomlFile.exists()) { + throw new GradleException('TOML file not found') + } + + def lines = tomlFile.readLines() + def nonSortedBlocks = [] + def currentBlock = [] + def prevLine = '' + def prevIndex = 0 + + lines.eachWithIndex { line, lineIndex -> + if (line.trim() && !line.startsWith('#')) { + if (line.startsWith('[')) { + prevLine = '' + } else { + def currIndex = lineIndex + 1 + if (prevLine > line) { + if (currentBlock && currentBlock[-1] == "${prevIndex}: ${prevLine}") { + currentBlock.add("${currIndex}: ${line}") + } else { + if (!currentBlock.isEmpty()) { + nonSortedBlocks.add(currentBlock) + currentBlock = [] + } + currentBlock.add("${prevIndex}: ${prevLine}") + currentBlock.add("${currIndex}: ${line}") + } + } + prevLine = line + prevIndex = lineIndex + 1 + } + } + } + + if (!currentBlock.isEmpty()) { + nonSortedBlocks.add(currentBlock) + throw new GradleException("The following lines were not sorted:\n" + + nonSortedBlocks.collect { it.join("\n") }.join("\n\n")) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/App.kt b/app/src/main/java/org/schabi/newpipe/App.kt index 0c065f491..8501cee09 100644 --- a/app/src/main/java/org/schabi/newpipe/App.kt +++ b/app/src/main/java/org/schabi/newpipe/App.kt @@ -10,6 +10,7 @@ import androidx.core.content.getSystemService import androidx.preference.PreferenceManager import coil3.ImageLoader import coil3.SingletonImageLoader +import coil3.network.okhttp.OkHttpNetworkFetcherFactory import coil3.request.allowRgb565 import coil3.request.crossfade import coil3.util.DebugLogger @@ -123,7 +124,9 @@ open class App : .logger(if (BuildConfig.DEBUG) DebugLogger() else null) .allowRgb565(getSystemService()!!.isLowRamDevice) .crossfade(true) - .build() + .components { + add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client)) + }.build() protected open fun getDownloader(): Downloader { val downloader = DownloaderImpl.init(null) diff --git a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java index 9ddbe96df..ee5450a62 100644 --- a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java +++ b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java @@ -48,6 +48,11 @@ public final class DownloaderImpl extends Downloader { this.mCookies = new HashMap<>(); } + @NonNull + public OkHttpClient getClient() { + return client; + } + /** * It's recommended to call exactly once in the entire lifetime of the application. * 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 60a1cff37..b437c6acb 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt @@ -1,203 +1,31 @@ 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.activity.enableEdgeToEdge 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.ui.components.common.ScaffoldWithToolbar +import org.schabi.newpipe.ui.screens.AboutScreen +import org.schabi.newpipe.ui.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) + enableEdgeToEdge() 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( - "Android-State", "2018", "Evernote", - "https://github.com/Evernote/android-state", StandardLicenses.EPL1 - ), - SoftwareComponent( - "Bridge", "2021", "Livefront", - "https://github.com/livefront/bridge", StandardLicenses.APACHE2 - ), - 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/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/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/about/StandardLicenses.kt b/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.kt deleted file mode 100644 index c5b9618fe..000000000 --- a/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.schabi.newpipe.about - -/** - * Class containing information about standard software licenses. - */ -object StandardLicenses { - @JvmField - val GPL3 = License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html") - - @JvmField - val APACHE2 = License("Apache License, Version 2.0", "ALv2", "apache2.html") - - @JvmField - val MPL2 = License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html") - - @JvmField - val MIT = License("MIT License", "MIT", "mit.html") - - @JvmField - val EPL1 = License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html") -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java index d4e73bcac..8c939a3e8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java @@ -6,9 +6,11 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.Nullable; +import androidx.compose.ui.platform.ComposeView; import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.R; +import org.schabi.newpipe.ui.emptystate.EmptyStateUtil; public class EmptyFragment extends BaseFragment { private static final String SHOW_MESSAGE = "SHOW_MESSAGE"; @@ -26,8 +28,10 @@ public class EmptyFragment extends BaseFragment { final Bundle savedInstanceState) { final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE); final View view = inflater.inflate(R.layout.fragment_empty, container, false); - view.findViewById(R.id.empty_state_view).setVisibility( - showMessage ? View.VISIBLE : View.GONE); + + final ComposeView composeView = view.findViewById(R.id.empty_state_view); + EmptyStateUtil.setEmptyStateComposable(composeView); + composeView.setVisibility(showMessage ? View.VISIBLE : View.GONE); return view; } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 5a91bc30e..36b305966 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -10,7 +10,6 @@ import android.graphics.Color; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -45,6 +44,8 @@ import org.schabi.newpipe.fragments.detail.TabAdapter; import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.local.feed.notifications.NotificationHelper; import org.schabi.newpipe.local.subscription.SubscriptionManager; +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec; +import org.schabi.newpipe.ui.emptystate.EmptyStateUtil; import org.schabi.newpipe.util.ChannelTabHelper; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; @@ -199,6 +200,11 @@ public class ChannelFragment extends BaseStateFragment protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); + EmptyStateUtil.setEmptyStateComposable( + binding.emptyStateView, + EmptyStateSpec.Companion.getContentNotSupported() + ); + tabAdapter = new TabAdapter(getChildFragmentManager()); binding.viewPager.setAdapter(tabAdapter); binding.tabLayout.setupWithViewPager(binding.viewPager); @@ -645,8 +651,6 @@ public class ChannelFragment extends BaseStateFragment return; } - binding.errorContentNotSupported.setVisibility(View.VISIBLE); - binding.channelKaomoji.setText("(︶︹︺)"); - binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f); + binding.emptyStateView.setVisibility(View.VISIBLE); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java index 5d398821a..feb23b6ac 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelTabFragment.java @@ -26,6 +26,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder; import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.ui.emptystate.EmptyStateUtil; import org.schabi.newpipe.util.ChannelTabHelper; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.PlayButtonHelper; @@ -79,6 +80,12 @@ public class ChannelTabFragment extends BaseListInfoFragment Bundle.parcelable(key: String?): T? { return BundleCompat.getParcelable(this, key, T::class.java) } -inline fun Bundle.parcelableArrayList(key: String?): ArrayList? { - return BundleCompat.getParcelableArrayList(this, key, T::class.java) -} - inline fun Bundle.serializable(key: String?): T? { return BundleCompat.getSerializable(this, key, T::class.java) } diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index a5e1594d1..4f60e36ae 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -38,6 +38,8 @@ import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder; import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec; +import org.schabi.newpipe.ui.emptystate.EmptyStateUtil; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.debounce.DebounceSavable; @@ -123,6 +125,10 @@ public final class BookmarkFragment extends BaseLocalListFragment() { override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) { // super.onViewCreated() calls initListeners() which require the binding to be initialized _feedBinding = FragmentFeedBinding.bind(rootView) + feedBinding.emptyStateView.setEmptyStateComposable() super.onViewCreated(rootView, savedInstanceState) val factory = FeedViewModel.getFactory(requireContext(), groupId) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 5583a2c4a..e4a9b79a2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -56,6 +56,7 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard import org.schabi.newpipe.streams.io.StoredFileHelper +import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.ServiceHelper @@ -257,6 +258,8 @@ class SubscriptionFragment : BaseStateFragment() { binding.itemsList.adapter = groupAdapter binding.itemsList.itemAnimator = null + binding.emptyStateView.setEmptyStateComposable() + viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java] viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) } viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/ImportSubscriptionsHintPlaceholderItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ImportSubscriptionsHintPlaceholderItem.kt index 93b551895..cf0b8c3ff 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/ImportSubscriptionsHintPlaceholderItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ImportSubscriptionsHintPlaceholderItem.kt @@ -3,14 +3,18 @@ package org.schabi.newpipe.local.subscription.item import android.view.View import com.xwray.groupie.viewbinding.BindableItem import org.schabi.newpipe.R -import org.schabi.newpipe.databinding.ListEmptyViewBinding +import org.schabi.newpipe.databinding.ListEmptyViewSubscriptionsBinding +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec +import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable /** * When there are no subscriptions, show a hint to the user about how to import subscriptions */ -class ImportSubscriptionsHintPlaceholderItem : BindableItem() { +class ImportSubscriptionsHintPlaceholderItem : BindableItem() { override fun getLayout(): Int = R.layout.list_empty_view_subscriptions - override fun bind(viewBinding: ListEmptyViewBinding, position: Int) {} + override fun bind(viewBinding: ListEmptyViewSubscriptionsBinding, position: Int) { + viewBinding.root.setEmptyStateComposable(EmptyStateSpec.NoSubscriptionsHint) + } override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount - override fun initializeViewBinding(view: View) = ListEmptyViewBinding.bind(view) + override fun initializeViewBinding(view: View) = ListEmptyViewSubscriptionsBinding.bind(view) } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java index c566313e3..cbd6b0656 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java @@ -11,6 +11,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.compose.ui.platform.ComposeView; import androidx.fragment.app.DialogFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -19,6 +20,8 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.local.subscription.SubscriptionManager; +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec; +import org.schabi.newpipe.ui.emptystate.EmptyStateUtil; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.image.CoilHelper; @@ -57,7 +60,7 @@ public class SelectChannelFragment extends DialogFragment { private OnCancelListener onCancelListener = null; private ProgressBar progressBar; - private TextView emptyView; + private ComposeView emptyView; private RecyclerView recyclerView; private List subscriptions = new Vector<>(); @@ -91,6 +94,9 @@ public class SelectChannelFragment extends DialogFragment { progressBar = v.findViewById(R.id.progressBar); emptyView = v.findViewById(R.id.empty_state_view); + + EmptyStateUtil.setEmptyStateComposable(emptyView, + EmptyStateSpec.Companion.getNoSubscriptions()); progressBar.setVisibility(View.VISIBLE); recyclerView.setVisibility(View.GONE); emptyView.setVisibility(View.GONE); diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java index c340dca22..6227d95a9 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java @@ -11,6 +11,7 @@ import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.compose.ui.platform.ComposeView; import androidx.fragment.app.DialogFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -27,6 +28,8 @@ import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec; +import org.schabi.newpipe.ui.emptystate.EmptyStateUtil; import org.schabi.newpipe.util.image.CoilHelper; import java.util.List; @@ -40,7 +43,7 @@ public class SelectPlaylistFragment extends DialogFragment { private OnSelectedListener onSelectedListener = null; private ProgressBar progressBar; - private TextView emptyView; + private ComposeView emptyView; private RecyclerView recyclerView; private Disposable disposable = null; @@ -62,6 +65,8 @@ public class SelectPlaylistFragment extends DialogFragment { recyclerView = v.findViewById(R.id.items_list); emptyView = v.findViewById(R.id.empty_state_view); + EmptyStateUtil.setEmptyStateComposable(emptyView, + EmptyStateSpec.Companion.getNoBookmarkedPlaylist()); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter(); recyclerView.setAdapter(playlistAdapter); diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java index 9d169d660..f667bb900 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchFragment.java @@ -11,6 +11,8 @@ import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding; +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec; +import org.schabi.newpipe.ui.emptystate.EmptyStateUtil; import java.util.List; @@ -39,6 +41,9 @@ public class PreferenceSearchFragment extends Fragment { binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false); binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext())); + EmptyStateUtil.setEmptyStateComposable( + binding.emptyStateView, + EmptyStateSpec.Companion.getNoSearchMaxSizeResult()); adapter = new PreferenceSearchAdapter(); adapter.setOnItemClickListener(this::onItemClicked); diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/about/AboutTab.kt b/app/src/main/java/org/schabi/newpipe/ui/components/about/AboutTab.kt new file mode 100644 index 000000000..3bba5dba9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/about/AboutTab.kt @@ -0,0 +1,147 @@ +package org.schabi.newpipe.ui.components.about + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +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.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.getDrawable +import coil3.compose.AsyncImage +import my.nanihadesuka.compose.ColumnScrollbar +import org.schabi.newpipe.BuildConfig +import org.schabi.newpipe.R +import org.schabi.newpipe.ui.components.common.defaultThemedScrollbarSettings +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 + ) +) + +private class AboutData( + @StringRes val title: Int, + @StringRes val description: Int, + @StringRes val buttonText: Int, + @StringRes val url: Int +) + +private class AboutDataProvider : CollectionPreviewParameterProvider(ABOUT_ITEMS) + +@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) +@Composable +@NonRestartableComposable +fun AboutTab() { + val scrollState = rememberScrollState() + + ColumnScrollbar(state = scrollState, settings = defaultThemedScrollbarSettings()) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .wrapContentSize(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // note: the preview + val context = LocalContext.current + val launcherDrawable = remember { getDrawable(context, R.mipmap.ic_launcher) } + AsyncImage( + model = launcherDrawable, + contentDescription = stringResource(R.string.app_name), + ) + Spacer(Modifier.height(4.dp)) + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + ) + Text( + text = BuildConfig.VERSION_NAME, + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.app_description), + textAlign = TextAlign.Center, + ) + } + + for (item in ABOUT_ITEMS) { + AboutItem(item, Modifier.padding(horizontal = 16.dp)) + } + + Spacer(Modifier.height(8.dp)) + } + } +} + +@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) +@Composable +@NonRestartableComposable +private fun AboutItem( + @PreviewParameter(AboutDataProvider::class) aboutData: AboutData, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = stringResource(aboutData.title), + style = MaterialTheme.typography.titleMedium + ) + Text( + text = stringResource(aboutData.description), + style = MaterialTheme.typography.bodyMedium + ) + + 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/ui/components/about/Library.kt b/app/src/main/java/org/schabi/newpipe/ui/components/about/Library.kt new file mode 100644 index 000000000..97a2be949 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/about/Library.kt @@ -0,0 +1,186 @@ +@file:OptIn(ExperimentalLayoutApi::class) + +package org.schabi.newpipe.ui.components.about + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Badge +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.compose.ui.unit.dp +import com.mikepenz.aboutlibraries.entity.Developer +import com.mikepenz.aboutlibraries.entity.Library +import com.mikepenz.aboutlibraries.entity.License +import com.mikepenz.aboutlibraries.entity.Organization +import com.mikepenz.aboutlibraries.entity.Scm +import com.mikepenz.aboutlibraries.ui.compose.m3.util.author +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet +import org.schabi.newpipe.ui.theme.AppTheme +import org.schabi.newpipe.util.external_communication.ShareUtils + +@Composable +fun Library( + @PreviewParameter(LibraryProvider::class) library: Library, + showLicenseDialog: (licenseFilename: String) -> Unit, + descriptionMaxLines: Int, +) { + val spdxLicense = library.licenses.firstOrNull()?.spdxId?.takeIf { it.isNotBlank() } + val licenseAssetPath = spdxLicense?.let { SPDX_ID_TO_ASSET_PATH[it] } + val context = LocalContext.current + + Column( + modifier = ( + if (licenseAssetPath != null) { + Modifier.clickable { + showLicenseDialog(licenseAssetPath) + } + } else if (spdxLicense != null) { + Modifier.clickable { + ShareUtils.openUrlInBrowser(context, "https://spdx.org/licenses/$spdxLicense.html") + } + } else { + Modifier + } + ) + .padding(vertical = 8.dp, horizontal = 16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = library.name, + modifier = Modifier.weight(0.75f), + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + val version = library.artifactVersion + if (!version.isNullOrBlank()) { + Text( + version, + modifier = if (version.length > 12) { + // limit the version size if it's too many characters (can happen e.g. if + // the version is a commit hash) + Modifier.weight(0.25f) + } else { + Modifier + }.padding(start = 8.dp), + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + val author = library.author + if (author.isNotBlank()) { + Text( + text = author, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + val description = library.description + if (!description.isNullOrBlank() && description != library.name) { + Spacer(Modifier.height(3.dp)) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + maxLines = descriptionMaxLines, + overflow = TextOverflow.Ellipsis, + ) + } + if (library.licenses.isNotEmpty()) { + FlowRow( + modifier = Modifier.padding(top = 6.dp, bottom = 4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + library.licenses.forEach { + Badge { + Text(text = it.spdxId?.takeIf { it.isNotBlank() } ?: it.name) + } + } + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun LibraryPreview(@PreviewParameter(LibraryProvider::class) library: Library) { + AppTheme { + Library(library, {}, 2) + } +} + +private class LibraryProvider : CollectionPreviewParameterProvider( + listOf( + Library( + uniqueId = "org.schabi.newpipe.extractor", + artifactVersion = "v0.24.3", + name = "NewPipeExtractor", + description = "NewPipe Extractor is a library for extracting things from streaming sites. It is a core component of NewPipe, but could be used independently.", + website = "https://newpipe.net", + developers = listOf(Developer("TeamNewPipe", "https://newpipe.net")).toImmutableList(), + organization = Organization("TeamNewPipe", "https://newpipe.net"), + scm = Scm(null, null, "https://github.com/TeamNewPipe/NewPipeExtractor"), + licenses = setOf( + License( + name = "GNU General Public License v3.0", + url = "https://api.github.com/licenses/gpl-3.0", + year = null, + spdxId = "GPL-3.0-only", + licenseContent = LoremIpsum().values.first(), + hash = "1234" + ), + License( + name = "GNU General Public License v3.0", + url = "https://api.github.com/licenses/gpl-3.0", + year = null, + spdxId = "GPL-3.0-only", + licenseContent = LoremIpsum().values.first(), + hash = "4321" + ) + ).toImmutableSet() + ), + Library( + uniqueId = "org.schabi.newpipe.extractor", + artifactVersion = "v0.24.3", + name = "NewPipeExtractor", + description = "NewPipe Extractor is a library for extracting things from streaming sites. It is a core component of NewPipe, but could be used independently.", + website = null, + developers = listOf().toImmutableList(), + organization = null, + scm = null, + licenses = setOf( + License( + name = "GNU General Public License v3.0", + url = "https://api.github.com/licenses/gpl-3.0", + year = null, + spdxId = "GPL-3.0-only", + licenseContent = LoremIpsum().values.first(), + hash = "1234" + ) + ).toImmutableSet() + ) + ) +) diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/about/LibraryDefinitions.kt b/app/src/main/java/org/schabi/newpipe/ui/components/about/LibraryDefinitions.kt new file mode 100644 index 000000000..6ab103c99 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/about/LibraryDefinitions.kt @@ -0,0 +1,138 @@ +/** + * The library definitions for most libraries are autogenerated by the AboutLibraries plugin. + * This file is only for TeamNewPipe-related libraries. + */ + +package org.schabi.newpipe.ui.components.about + +import android.content.Context +import com.mikepenz.aboutlibraries.entity.Developer +import com.mikepenz.aboutlibraries.entity.Library +import com.mikepenz.aboutlibraries.entity.License +import com.mikepenz.aboutlibraries.entity.Scm +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet +import org.schabi.newpipe.BuildConfig +import org.schabi.newpipe.R + +val SPDX_ID_TO_ASSET_PATH = mapOf( + "Apache-2.0" to "apache2.html", + "EPL-1.0" to "epl1.html", + "GPL-3.0-only" to "gpl_3.html", + "GPL-3.0-or-later" to "gpl_3.html", + "MIT" to "mit.html", + "MPL-2.0" to "mpl2.html", +) + +fun getFirstPartyLibraries( + context: Context, + teamNewPipeLibraries: List, +): List { + val gpl3 = setOf( + License( + name = "GNU General Public License v3.0", + url = "https://www.gnu.org/licenses/gpl-3.0.txt", + year = null, + spdxId = "GPL-3.0-or-later", + licenseContent = null, + hash = "GPL-3.0-or-later", + ) + ).toImmutableSet() + + val npeId = "com.github.TeamNewPipe:NewPipeExtractor" + val npe = teamNewPipeLibraries.firstOrNull { it.uniqueId == npeId } + + return listOf( + Library( + uniqueId = BuildConfig.APPLICATION_ID, + artifactVersion = BuildConfig.VERSION_NAME, + name = context.getString(R.string.app_name), + description = context.getString(R.string.app_description), + website = context.getString(R.string.website_url), + developers = listOf( + Developer( + name = context.getString(R.string.team_newpipe), + organisationUrl = context.getString(R.string.website_url) + ) + ).toImmutableList(), + organization = null, + scm = Scm(null, null, context.getString(R.string.github_url)), + licenses = gpl3, + ), + Library( + uniqueId = npeId, + artifactVersion = npe?.artifactVersion, + name = context.getString(R.string.newpipe_extractor), + description = context.getString(R.string.newpipe_extractor_description), + website = context.getString(R.string.newpipe_extractor_github_url), + developers = listOf( + Developer( + name = context.getString(R.string.team_newpipe), + organisationUrl = context.getString(R.string.website_url) + ) + ).toImmutableList(), + organization = null, + scm = Scm(null, null, context.getString(R.string.newpipe_extractor_github_url)), + licenses = gpl3, + ), + ) +} + +fun getAdditionalThirdPartyLibraries( + context: Context, + teamNewPipeLibraries: List, + licenses: ImmutableSet, +): List { + val apache2 = licenses.firstOrNull { it.spdxId == "Apache-2.0" } + val mit = licenses.firstOrNull { it.spdxId == "MIT" } + val mpl2 = licenses.firstOrNull { it.spdxId == "MPL-2.0" } + + val nanojsonId = "com.github.TeamNewPipe:nanojson" + val nanojson = teamNewPipeLibraries.firstOrNull { it.uniqueId == nanojsonId } + val nnfpId = "com.github.TeamNewPipe:NoNonsense-FilePicker" + val nnfp = teamNewPipeLibraries.firstOrNull { it.uniqueId == nnfpId } + + return listOf( + Library( + uniqueId = nnfpId, + artifactVersion = nnfp?.artifactVersion, + name = "NoNonsense-FilePicker", + description = "A file/directory-picker for Android.", + website = "https://github.com/TeamNewPipe/NoNonsense-FilePicker", + developers = listOf( + Developer( + name = "Jonas Kalderstam", + organisationUrl = "https://github.com/spacecowboy/NoNonsense-FilePicker", + ), + Developer( + name = context.getString(R.string.team_newpipe), + organisationUrl = context.getString(R.string.website_url) + ) + ).toImmutableList(), + organization = null, + scm = Scm(null, null, "https://github.com/TeamNewPipe/NoNonsense-FilePicker"), + licenses = listOfNotNull(mpl2).toImmutableSet(), + ), + Library( + uniqueId = nanojsonId, + artifactVersion = nanojson?.artifactVersion, + name = "nanojson", + description = "nanojson is a tiny, fast, and compliant JSON parser and writer for Java.", + website = "https://github.com/TeamNewPipe/nanojson", + developers = listOf( + Developer( + name = "mmastrac", + organisationUrl = "https://github.com/mmastrac/nanojson", + ), + Developer( + name = context.getString(R.string.team_newpipe), + organisationUrl = context.getString(R.string.website_url) + ), + ).toImmutableList(), + organization = null, + scm = Scm(null, null, "https://github.com/TeamNewPipe/nanojson"), + licenses = listOfNotNull(mit, apache2).toImmutableSet() + ), + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseDialog.kt b/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseDialog.kt new file mode 100644 index 000000000..24421a93a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseDialog.kt @@ -0,0 +1,51 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package org.schabi.newpipe.ui.components.about + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar +import org.schabi.newpipe.ui.components.common.LoadingIndicator + +@Composable +fun LicenseDialog(licenseHtml: AnnotatedString, onDismissRequest: () -> Unit) { + val lazyListState = rememberLazyListState() + + ModalBottomSheet(onDismissRequest) { + CompositionLocalProvider( + // contentColorFor(MaterialTheme.colorScheme.containerColor), i.e. ModalBottomSheet's + // default background color, does not resolve correctly, so need to manually set the + // content color for MaterialTheme.colorScheme.background instead + LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background) + ) { + LazyColumnThemedScrollbar(state = lazyListState) { + LazyColumn( + state = lazyListState + ) { + item { + if (licenseHtml.isEmpty()) { + LoadingIndicator(modifier = Modifier.padding(32.dp)) + } else { + Text( + text = licenseHtml, + modifier = Modifier.padding(horizontal = 12.dp), + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseTab.kt b/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseTab.kt new file mode 100644 index 000000000..46e71ba56 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseTab.kt @@ -0,0 +1,105 @@ +package org.schabi.newpipe.ui.components.about + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import org.schabi.newpipe.R +import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar +import org.schabi.newpipe.ui.components.common.LoadingIndicator + +@Composable +@NonRestartableComposable +fun LicenseTab(viewModel: LicenseTabViewModel = viewModel()) { + val lazyListState = rememberLazyListState() + val stateFlow = viewModel.state.collectAsState() + val state = stateFlow.value + + if (state.licenseDialogHtml != null) { + LicenseDialog( + licenseHtml = state.licenseDialogHtml, + onDismissRequest = { viewModel.closeLicenseDialog() } + ) + } + + LazyColumnThemedScrollbar(state = lazyListState) { + LazyColumn( + state = lazyListState, + ) { + item { + Text( + text = stringResource(R.string.app_license_title), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding( + start = 16.dp, + top = 16.dp, + end = 16.dp, + bottom = 8.dp + ), + ) + } + item { + Text( + text = stringResource(R.string.app_license), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 8.dp + ), + ) + } + if (state.firstPartyLibraries == null) { + item { + LoadingIndicator(modifier = Modifier.padding(32.dp)) + } + } else { + for (library in state.firstPartyLibraries) { + item { + Library( + library = library, + showLicenseDialog = viewModel::showLicenseDialog, + descriptionMaxLines = Int.MAX_VALUE, + ) + } + } + } + + item { + Text( + text = stringResource(R.string.title_licenses), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding( + start = 16.dp, + top = 16.dp, + end = 16.dp, + bottom = 8.dp + ), + ) + } + if (state.thirdPartyLibraries == null) { + item { + LoadingIndicator(modifier = Modifier.padding(32.dp)) + } + } else { + for (library in state.thirdPartyLibraries) { + item { + Library( + library = library, + showLicenseDialog = viewModel::showLicenseDialog, + descriptionMaxLines = 2, + ) + } + } + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseTabViewModel.kt b/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseTabViewModel.kt new file mode 100644 index 000000000..eeb87816c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseTabViewModel.kt @@ -0,0 +1,82 @@ +package org.schabi.newpipe.ui.components.about + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.fromHtml +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mikepenz.aboutlibraries.Libs +import com.mikepenz.aboutlibraries.entity.Library +import com.mikepenz.aboutlibraries.util.withContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.schabi.newpipe.App + +class LicenseTabViewModel : ViewModel() { + private val _state = MutableStateFlow(LicenseTabState(null, null, null)) + val state: StateFlow = _state + private var licenseLoadJob: Job? = null + + init { + viewModelScope.launch { + withContext(Dispatchers.IO) { + loadLibraries() + } + } + } + + private fun loadLibraries() { + val context = App.instance + val libs = Libs.Builder().withContext(context).build() + val (teamNewPipeLibraries, thirdParty) = libs.libraries + .toMutableList() + .partition { it.uniqueId.startsWith("com.github.TeamNewPipe") } + + val firstParty = getFirstPartyLibraries(context, teamNewPipeLibraries) + val allThirdParty = + getAdditionalThirdPartyLibraries(context, teamNewPipeLibraries, libs.licenses) + + thirdParty + + _state.update { + it.copy( + firstPartyLibraries = firstParty, + thirdPartyLibraries = allThirdParty, + ) + } + } + + fun showLicenseDialog(filename: String) { + licenseLoadJob?.cancel() + _state.update { it.copy(licenseDialogHtml = AnnotatedString("")) } + licenseLoadJob = viewModelScope.launch { + withContext(Dispatchers.IO) { + val text = App.instance.assets.open(filename).bufferedReader().use { it.readText() } + val parsedHtml = AnnotatedString.fromHtml(text) + _state.update { + if (it.licenseDialogHtml != null && isActive) { + it.copy(licenseDialogHtml = parsedHtml) + } else { + it + } + } + } + } + } + + fun closeLicenseDialog() { + licenseLoadJob?.cancel() + _state.update { it.copy(licenseDialogHtml = null) } + } + + data class LicenseTabState( + val firstPartyLibraries: List?, + val thirdPartyLibraries: List?, + // null if dialog closed, empty if loading, otherwise license HTML content + val licenseDialogHtml: AnnotatedString?, + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/NoItemsMessage.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/NoItemsMessage.kt deleted file mode 100644 index afb53fdf4..000000000 --- a/app/src/main/java/org/schabi/newpipe/ui/components/common/NoItemsMessage.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.schabi.newpipe.ui.components.common - -import android.content.res.Configuration -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp -import org.schabi.newpipe.R -import org.schabi.newpipe.ui.theme.AppTheme - -@Composable -fun NoItemsMessage(@StringRes message: Int) { - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentSize(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text(text = "(╯°-°)╯", fontSize = 35.sp) - Text(text = stringResource(id = message), fontSize = 24.sp) - } -} - -@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) -@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun NoItemsMessagePreview() { - AppTheme { - Surface(color = MaterialTheme.colorScheme.background) { - NoItemsMessage(message = R.string.no_videos) - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/ScaffoldWithToolbar.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/ScaffoldWithToolbar.kt new file mode 100644 index 000000000..18139c7a6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/ScaffoldWithToolbar.kt @@ -0,0 +1,63 @@ +package org.schabi.newpipe.ui.components.common + +import android.content.res.Configuration +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.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +@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) }, + // TODO decide whether to use default colors instead + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + scrolledContainerColor = MaterialTheme.colorScheme.primaryContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + ) + } + }, + actions = actions + ) + }, + content = content + ) +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ScaffoldWithToolbarPreview() { + ScaffoldWithToolbar( + title = "Example", + onBackClick = {}, + content = {} + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt index 6eee01bc0..3c6c49d35 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt @@ -26,9 +26,10 @@ import org.schabi.newpipe.R import org.schabi.newpipe.extractor.stream.StreamInfo import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.info_list.ItemViewMode -import org.schabi.newpipe.ui.components.common.NoItemsMessage import org.schabi.newpipe.ui.components.items.ItemList import org.schabi.newpipe.ui.components.items.stream.StreamInfoItem +import org.schabi.newpipe.ui.emptystate.EmptyStateComposable +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.NO_SERVICE_ID @@ -41,43 +42,44 @@ fun RelatedItems(info: StreamInfo) { mutableStateOf(sharedPreferences.getBoolean(key, false)) } - if (info.relatedItems.isEmpty()) { - NoItemsMessage(message = R.string.no_videos) - } else { - ItemList( - items = info.relatedItems, - mode = ItemViewMode.LIST, - listHeader = { - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 12.dp, end = 12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text(text = stringResource(R.string.auto_queue_description)) + ItemList( + items = info.relatedItems, + mode = ItemViewMode.LIST, + listHeader = { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = stringResource(R.string.auto_queue_description)) - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text(text = stringResource(R.string.auto_queue_toggle)) - Switch( - checked = isAutoQueueEnabled, - onCheckedChange = { - isAutoQueueEnabled = it - sharedPreferences.edit { - putBoolean(key, it) - } + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = stringResource(R.string.auto_queue_toggle)) + Switch( + checked = isAutoQueueEnabled, + onCheckedChange = { + isAutoQueueEnabled = it + sharedPreferences.edit { + putBoolean(key, it) } - ) - } + } + ) } } } - ) - } + if (info.relatedItems.isEmpty()) { + item { + EmptyStateComposable(EmptyStateSpec.NoVideos) + } + } + } + ) } @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesDialog.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesDialog.kt index 17ea900a5..d6d00b28c 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentRepliesDialog.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.unit.dp @@ -38,7 +39,8 @@ import org.schabi.newpipe.extractor.stream.Description import org.schabi.newpipe.paging.CommentRepliesSource import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar import org.schabi.newpipe.ui.components.common.LoadingIndicator -import org.schabi.newpipe.ui.components.common.NoItemsMessage +import org.schabi.newpipe.ui.emptystate.EmptyStateComposable +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec import org.schabi.newpipe.ui.theme.AppTheme @Composable @@ -130,13 +132,17 @@ private fun CommentRepliesDialog( val refresh = comments.loadState.refresh if (refresh is LoadState.Loading) { LoadingIndicator(modifier = Modifier.padding(top = 8.dp)) + } else if (refresh is LoadState.Error) { + // TODO use error panel instead + EmptyStateComposable( + EmptyStateSpec.DisabledComments.copy( + descriptionText = { + stringResource(R.string.error_unable_to_load_comments) + } + ) + ) } else { - val message = if (refresh is LoadState.Error) { - R.string.error_unable_to_load_comments - } else { - R.string.no_comments - } - NoItemsMessage(message) + EmptyStateComposable(EmptyStateSpec.NoComments) } } } else { diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentSection.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentSection.kt index 33c4e2139..d603c4a6f 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentSection.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/comment/CommentSection.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -28,7 +29,8 @@ import org.schabi.newpipe.extractor.comments.CommentsInfoItem import org.schabi.newpipe.extractor.stream.Description import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar import org.schabi.newpipe.ui.components.common.LoadingIndicator -import org.schabi.newpipe.ui.components.common.NoItemsMessage +import org.schabi.newpipe.ui.emptystate.EmptyStateComposable +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.viewmodels.CommentsViewModel import org.schabi.newpipe.viewmodels.util.Resource @@ -66,11 +68,11 @@ private fun CommentSection( if (commentInfo.isCommentsDisabled) { item { - NoItemsMessage(R.string.comments_are_disabled) + EmptyStateComposable(EmptyStateSpec.DisabledComments) } } else if (count == 0) { item { - NoItemsMessage(R.string.no_comments) + EmptyStateComposable(EmptyStateSpec.NoComments) } } else { // do not show anything if the comment count is unknown @@ -95,7 +97,14 @@ private fun CommentSection( is LoadState.Error -> { item { - NoItemsMessage(R.string.error_unable_to_load_comments) + // TODO use error panel instead + EmptyStateComposable( + EmptyStateSpec.DisabledComments.copy( + descriptionText = { + stringResource(R.string.error_unable_to_load_comments) + } + ) + ) } } @@ -110,7 +119,14 @@ private fun CommentSection( is Resource.Error -> { item { - NoItemsMessage(R.string.error_unable_to_load_comments) + // TODO use error panel instead + EmptyStateComposable( + EmptyStateSpec.DisabledComments.copy( + descriptionText = { + stringResource(R.string.error_unable_to_load_comments) + } + ) + ) } } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateComposable.kt b/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateComposable.kt new file mode 100644 index 000000000..ab9bf6336 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateComposable.kt @@ -0,0 +1,159 @@ +package org.schabi.newpipe.ui.emptystate + +import android.graphics.Color +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.R +import org.schabi.newpipe.ui.theme.AppTheme + +@Composable +fun EmptyStateComposable( + spec: EmptyStateSpec, + modifier: Modifier = Modifier, +) = EmptyStateComposable( + modifier = spec.modifier(modifier), + emojiText = spec.emojiText(), + descriptionText = spec.descriptionText(), +) + +@Composable +private fun EmptyStateComposable( + emojiText: String, + descriptionText: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = emojiText, + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + ) + + Text( + modifier = Modifier + .padding(top = 6.dp) + .padding(horizontal = 16.dp), + text = descriptionText, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + ) + } +} + +@Preview(showBackground = true, backgroundColor = Color.WHITE.toLong()) +@Composable +fun EmptyStateComposableGenericErrorPreview() { + AppTheme { + EmptyStateComposable(EmptyStateSpec.GenericError) + } +} + +@Preview(showBackground = true, backgroundColor = Color.WHITE.toLong()) +@Composable +fun EmptyStateComposableNoCommentPreview() { + AppTheme { + EmptyStateComposable(EmptyStateSpec.NoComments) + } +} + +data class EmptyStateSpec( + val modifier: (Modifier) -> Modifier, + val emojiText: @Composable () -> String, + val descriptionText: @Composable () -> String, +) { + companion object { + + val GenericError = + EmptyStateSpec( + modifier = { + it + .fillMaxWidth() + .heightIn(min = 128.dp) + }, + emojiText = { "¯\\_(ツ)_/¯" }, + descriptionText = { stringResource(id = R.string.empty_list_subtitle) }, + ) + + val NoVideos = + EmptyStateSpec( + modifier = { + it + .fillMaxWidth() + .heightIn(min = 128.dp) + }, + emojiText = { "(╯°-°)╯" }, + descriptionText = { stringResource(id = R.string.no_videos) }, + ) + + val NoComments = + EmptyStateSpec( + modifier = { + it + .fillMaxWidth() + .heightIn(min = 128.dp) + }, + emojiText = { "¯\\_(╹x╹)_/¯" }, + descriptionText = { stringResource(id = R.string.no_comments) }, + ) + + val DisabledComments = + NoComments.copy( + descriptionText = { stringResource(id = R.string.comments_are_disabled) }, + ) + + val NoSearchResult = + NoComments.copy( + modifier = { it }, + emojiText = { "╰(°●°╰)" }, + descriptionText = { stringResource(id = R.string.search_no_results) } + ) + + val NoSearchMaxSizeResult = + NoSearchResult.copy( + modifier = { it.fillMaxSize() }, + ) + + val ContentNotSupported = + NoComments.copy( + modifier = { it.padding(top = 90.dp) }, + emojiText = { "(︶︹︺)" }, + descriptionText = { stringResource(id = R.string.content_not_supported) }, + ) + + val NoBookmarkedPlaylist = + EmptyStateSpec( + modifier = { it }, + emojiText = { "(╥﹏╥)" }, + descriptionText = { stringResource(id = R.string.no_playlist_bookmarked_yet) }, + ) + + val NoSubscriptionsHint = + EmptyStateSpec( + modifier = { it }, + emojiText = { "(꩜ᯅ꩜)" }, + descriptionText = { stringResource(id = R.string.import_subscriptions_hint) }, + ) + + val NoSubscriptions = + NoSubscriptionsHint.copy( + descriptionText = { stringResource(id = R.string.no_channel_subscribed_yet) }, + ) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateUtil.kt b/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateUtil.kt new file mode 100644 index 000000000..2fced431f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateUtil.kt @@ -0,0 +1,30 @@ +@file:JvmName("EmptyStateUtil") + +package org.schabi.newpipe.ui.emptystate + +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import org.schabi.newpipe.ui.theme.AppTheme + +@JvmOverloads +fun ComposeView.setEmptyStateComposable( + spec: EmptyStateSpec = EmptyStateSpec.GenericError, + strategy: ViewCompositionStrategy = ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, +) = apply { + setViewCompositionStrategy(strategy) + setContent { + AppTheme { + CompositionLocalProvider( + LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background) + ) { + EmptyStateComposable( + spec = spec + ) + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/AboutScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/AboutScreen.kt new file mode 100644 index 000000000..673a22892 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/AboutScreen.kt @@ -0,0 +1,84 @@ +package org.schabi.newpipe.ui.screens + +import android.content.res.Configuration +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.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 org.schabi.newpipe.R +import org.schabi.newpipe.ui.components.about.AboutTab +import org.schabi.newpipe.ui.components.about.LicenseTab +import org.schabi.newpipe.ui.theme.AppTheme + +private val TITLES = intArrayOf(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, + containerColor = MaterialTheme.colorScheme.primaryContainer + ) { + 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 -> + 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/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 690ed4a97..ad9a3b7cd 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -22,6 +22,7 @@ import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.compose.ui.platform.ComposeView; import androidx.fragment.app.Fragment; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.GridLayoutManager; @@ -34,6 +35,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard; import org.schabi.newpipe.streams.io.StoredFileHelper; +import org.schabi.newpipe.ui.emptystate.EmptyStateUtil; import org.schabi.newpipe.util.FilePickerActivityHelper; import java.io.File; @@ -108,7 +110,8 @@ public class MissionsFragment extends Fragment { mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE); // Views - mEmpty = v.findViewById(R.id.list_empty_view); + mEmpty = v.findViewById(R.id.empty_state_view); + EmptyStateUtil.setEmptyStateComposable((ComposeView) mEmpty); mList = v.findViewById(R.id.mission_recycler); // Init layouts managers 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 @@ - - - - - - - - - - - - - - - - - -