1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-01-08 16:30:34 +00:00

Merge branch 'refactor' into Video-description-compose

# Conflicts:
#	app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt
This commit is contained in:
Isira Seneviratne 2024-11-28 06:06:19 +05:30
commit d00084089f
59 changed files with 1563 additions and 1091 deletions

View File

@ -1,16 +1,18 @@
import com.android.tools.profgen.ArtProfileKt import com.android.tools.profgen.ArtProfileKt
import com.android.tools.profgen.ArtProfileSerializer import com.android.tools.profgen.ArtProfileSerializer
import com.android.tools.profgen.DexFile import com.android.tools.profgen.DexFile
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
plugins { plugins {
id "com.android.application" alias libs.plugins.android.application
id "kotlin-android" alias libs.plugins.kotlin.android
id "kotlin-kapt" alias libs.plugins.kotlin.compose
id "kotlin-parcelize" alias libs.plugins.kotlin.kapt
id "checkstyle" alias libs.plugins.kotlin.parcelize
id "org.sonarqube" version "4.0.0.2929" alias libs.plugins.checkstyle
id "org.jetbrains.kotlin.plugin.compose" version "${kotlin_version}" alias libs.plugins.sonarqube
id 'com.google.dagger.hilt.android' alias libs.plugins.hilt
alias libs.plugins.aboutlibraries
} }
android { 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 { configurations {
checkstyle checkstyle
ktlint ktlint
@ -137,7 +120,7 @@ checkstyle {
getConfigDirectory().set(rootProject.file("checkstyle")) getConfigDirectory().set(rootProject.file("checkstyle"))
ignoreFailures false ignoreFailures false
showViolations true showViolations true
toolVersion = checkstyleVersion toolVersion = libs.versions.checkstyle.get()
} }
tasks.register('runCheckstyle', Checkstyle) { tasks.register('runCheckstyle', Checkstyle) {
@ -179,11 +162,13 @@ tasks.register('formatKtlint', JavaExec) {
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
} }
apply from: 'check-dependencies.gradle'
afterEvaluate { afterEvaluate {
if (!System.properties.containsKey('skipFormatKtlint')) { if (!System.properties.containsKey('skipFormatKtlint')) {
preDebugBuild.dependsOn formatKtlint preDebugBuild.dependsOn formatKtlint
} }
preDebugBuild.dependsOn runCheckstyle, runKtlint preDebugBuild.dependsOn runCheckstyle, runKtlint, checkDependenciesOrder
} }
sonar { sonar {
@ -198,148 +183,153 @@ kapt {
correctErrorTypes true 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 { dependencies {
/** Desugaring **/ /** Desugaring **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4' coreLibraryDesugaring libs.desugar.jdk.libs.nio
/** NewPipe libraries **/ /** NewPipe libraries **/
// You can use a local version by uncommenting a few lines in settings.gradle implementation libs.teamnewpipe.nanojson
// Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub implementation libs.teamnewpipe.newpipe.extractor
// name and the commit hash with the commit hash of the (pushed) commit you want to test implementation libs.teamnewpipe.nononsense.filepicker
// 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'
/** Checkstyle **/ /** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" checkstyle libs.tools.checkstyle
ktlint 'com.pinterest:ktlint:0.45.2' ktlint libs.tools.ktlint
/** Kotlin **/ /** Kotlin **/
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" implementation libs.kotlin.stdlib
/** AndroidX **/ /** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.6.1' implementation libs.androidx.appcompat
implementation 'androidx.cardview:cardview:1.0.0' implementation libs.androidx.cardview
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation libs.androidx.constraintlayout
implementation 'androidx.core:core-ktx:1.12.0' implementation libs.androidx.core.ktx
implementation 'androidx.documentfile:documentfile:1.0.1' implementation libs.androidx.documentfile
implementation 'androidx.fragment:fragment-compose:1.8.2' implementation libs.androidx.fragment.compose
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}" implementation libs.androidx.lifecycle.livedata
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}" implementation libs.androidx.lifecycle.viewmodel
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' implementation libs.androidx.localbroadcastmanager
implementation 'androidx.media:media:1.7.0' implementation libs.androidx.media
implementation 'androidx.preference:preference:1.2.1' implementation libs.androidx.preference
implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation libs.androidx.recyclerview
implementation "androidx.room:room-runtime:${androidxRoomVersion}" implementation libs.androidx.room.runtime
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}" implementation libs.androidx.room.rxjava3
kapt "androidx.room:room-compiler:${androidxRoomVersion}" kapt libs.androidx.room.compiler
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation libs.androidx.swiperefreshlayout
// Newer version specified to prevent accessibility regressions with RecyclerView, see: // Newer version specified to prevent accessibility regressions with RecyclerView, see:
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01 // https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02' implementation libs.androidx.viewpager2
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}" implementation libs.androidx.work.runtime
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}" implementation libs.androidx.work.rxjava3
implementation 'com.google.android.material:material:1.11.0' implementation libs.androidx.material
/** Third-party libraries **/ /** Third-party libraries **/
// Instance state boilerplate elimination // Instance state boilerplate elimination
implementation 'com.github.livefront:bridge:v2.0.2' implementation libs.livefront.bridge
implementation "com.evernote:android-state:$stateSaverVersion" implementation libs.android.state
kapt "com.evernote:android-state-processor:$stateSaverVersion" kapt libs.android.state.processor
// HTML parser // HTML parser
implementation "org.jsoup:jsoup:1.17.2" implementation libs.jsoup
// HTTP client // HTTP client
implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation libs.okhttp
// Media player // Media player
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}" implementation libs.exoplayer.core
implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}" implementation libs.exoplayer.dash
implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}" implementation libs.exoplayer.database
implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}" implementation libs.exoplayer.datasource
implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}" implementation libs.exoplayer.hls
implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}" implementation libs.exoplayer.smoothstreaming
implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}" implementation libs.exoplayer.ui
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}" implementation libs.extension.mediasession
// Metadata generator for service descriptors // Metadata generator for service descriptors
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}" compileOnly libs.auto.service
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}" kapt libs.auto.service.kapt
// Manager for complex RecyclerView layouts // Manager for complex RecyclerView layouts
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}" implementation libs.lisawray.groupie
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}" implementation libs.lisawray.groupie.viewbinding
// Image loading // Image loading
implementation "io.coil-kt.coil3:coil-compose:${coilVersion}" implementation libs.coil.compose
implementation "io.coil-kt.coil3:coil-network-okhttp:${coilVersion}" implementation libs.coil.network.okhttp
// Markdown library for Android // Markdown library for Android
implementation "io.noties.markwon:core:${markwonVersion}" implementation libs.markwon.core
implementation "io.noties.markwon:linkify:${markwonVersion}" implementation libs.markwon.linkify
// Crash reporting // Crash reporting
implementation "ch.acra:acra-core:5.11.3" implementation libs.acra.core
// Properly restarting // Properly restarting
implementation 'com.jakewharton:process-phoenix:2.1.2' implementation libs.process.phoenix
// Reactive extensions for Java VM // Reactive extensions for Java VM
implementation "io.reactivex.rxjava3:rxjava:3.1.8" implementation libs.rxjava3.rxjava
implementation "io.reactivex.rxjava3:rxandroid:3.0.2" implementation libs.rxjava3.rxandroid
// RxJava binding APIs for Android UI widgets // RxJava binding APIs for Android UI widgets
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0" implementation libs.rxbinding4.rxbinding
// Date and time formatting // Date and time formatting
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final" implementation libs.prettytime
// Jetpack Compose // Jetpack Compose
implementation(platform('androidx.compose:compose-bom:2024.10.01')) implementation(platform(libs.androidx.compose.bom))
implementation 'androidx.compose.material3:material3' implementation libs.androidx.compose.material3
implementation 'androidx.compose.material3.adaptive:adaptive' implementation libs.androidx.compose.adaptive
implementation 'androidx.activity:activity-compose' implementation libs.androidx.activity.compose
implementation 'androidx.compose.ui:ui-tooling-preview' implementation libs.androidx.compose.ui.tooling.preview
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose' implementation libs.androidx.lifecycle.viewmodel.compose
implementation 'androidx.compose.ui:ui-text' // Needed for parsing HTML to AnnotatedString implementation libs.androidx.compose.ui.text // Needed for parsing HTML to AnnotatedString
implementation 'androidx.compose.material:material-icons-extended' implementation libs.androidx.compose.material.icons.extended
// Jetpack Compose related dependencies // Jetpack Compose related dependencies
implementation 'androidx.paging:paging-compose:3.3.2' implementation libs.androidx.paging.compose
implementation "androidx.navigation:navigation-compose:2.8.3" implementation libs.androidx.navigation.compose
// Coroutines interop // 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 // Hilt
implementation("com.google.dagger:hilt-android:2.51.1") implementation libs.hilt.android
kapt("com.google.dagger:hilt-compiler:2.51.1") kapt(libs.hilt.compiler)
// Scroll // Scroll
implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0' implementation libs.lazycolumnscrollbar
/** Debugging **/ /** Debugging **/
// Memory leak detection // Memory leak detection
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" debugImplementation libs.leakcanary.object.watcher
debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}" debugImplementation libs.leakcanary.plumber.android
debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}" debugImplementation libs.leakcanary.android.core
// Debug bridge for Android // Debug bridge for Android
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" debugImplementation libs.stetho
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}" debugImplementation libs.stetho.okhttp3
// Jetpack Compose // Jetpack Compose
debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation libs.androidx.compose.ui.tooling
/** Testing **/ /** Testing **/
testImplementation 'junit:junit:4.13.2' testImplementation libs.junit
testImplementation 'org.mockito:mockito-core:5.6.0' testImplementation libs.mockito.core
androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation libs.androidx.junit
androidTestImplementation "androidx.test:runner:1.5.2" androidTestImplementation libs.androidx.runner
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}" androidTestImplementation libs.androidx.room.testing
androidTestImplementation "org.assertj:assertj-core:3.24.2" androidTestImplementation libs.assertj.core
} }
static String getGitWorkingBranch() { static String getGitWorkingBranch() {

View File

@ -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"))
}
}
}

View File

@ -10,6 +10,7 @@ import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import coil3.ImageLoader import coil3.ImageLoader
import coil3.SingletonImageLoader import coil3.SingletonImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.allowRgb565 import coil3.request.allowRgb565
import coil3.request.crossfade import coil3.request.crossfade
import coil3.util.DebugLogger import coil3.util.DebugLogger
@ -123,7 +124,9 @@ open class App :
.logger(if (BuildConfig.DEBUG) DebugLogger() else null) .logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice) .allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
.crossfade(true) .crossfade(true)
.build() .components {
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
}.build()
protected open fun getDownloader(): Downloader { protected open fun getDownloader(): Downloader {
val downloader = DownloaderImpl.init(null) val downloader = DownloaderImpl.init(null)

View File

@ -48,6 +48,11 @@ public final class DownloaderImpl extends Downloader {
this.mCookies = new HashMap<>(); this.mCookies = new HashMap<>();
} }
@NonNull
public OkHttpClient getClient() {
return client;
}
/** /**
* It's recommended to call exactly once in the entire lifetime of the application. * It's recommended to call exactly once in the entire lifetime of the application.
* *

View File

@ -1,203 +1,31 @@
package org.schabi.newpipe.about package org.schabi.newpipe.about
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import androidx.activity.compose.setContent
import android.view.MenuItem import androidx.activity.enableEdgeToEdge
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.compose.ui.res.stringResource
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityAboutBinding import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
import org.schabi.newpipe.databinding.FragmentAboutBinding 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.Localization
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
class AboutActivity : AppCompatActivity() { class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Localization.assureCorrectAppLanguage(this) Localization.assureCorrectAppLanguage(this)
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeHelper.setTheme(this)
title = getString(R.string.title_activity_about)
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater) setContent {
setContentView(aboutBinding.root) AppTheme {
setSupportActionBar(aboutBinding.aboutToolbar) ScaffoldWithToolbar(
supportActionBar?.setDisplayHomeAsUpEnabled(true) title = stringResource(R.string.title_activity_about),
onBackClick = { onBackPressedDispatcher.onBackPressed() }
// Create the adapter that will return a fragment for each of the three ) { padding ->
// primary sections of the activity. AboutScreen(padding)
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")
}
}
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
),
)
} }
} }

View File

@ -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

View File

@ -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<SoftwareComponent>
private var activeSoftwareComponent: SoftwareComponent? = null
private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(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<SoftwareComponent>): LicenseFragment {
val fragment = LicenseFragment()
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
return fragment
}
}
}

View File

@ -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("</head>", "<style>${getLicenseStylesheet(context)}</style></head>")
} 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)
}

View File

@ -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

View File

@ -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")
}

View File

@ -6,9 +6,11 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.compose.ui.platform.ComposeView;
import org.schabi.newpipe.BaseFragment; import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
public class EmptyFragment extends BaseFragment { public class EmptyFragment extends BaseFragment {
private static final String SHOW_MESSAGE = "SHOW_MESSAGE"; private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
@ -26,8 +28,10 @@ public class EmptyFragment extends BaseFragment {
final Bundle savedInstanceState) { final Bundle savedInstanceState) {
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE); final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
final View view = inflater.inflate(R.layout.fragment_empty, container, false); 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; return view;
} }
} }

View File

@ -10,7 +10,6 @@ import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; 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.ktx.AnimationType;
import org.schabi.newpipe.local.feed.notifications.NotificationHelper; import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
import org.schabi.newpipe.local.subscription.SubscriptionManager; 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.ChannelTabHelper;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -199,6 +200,11 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
EmptyStateUtil.setEmptyStateComposable(
binding.emptyStateView,
EmptyStateSpec.Companion.getContentNotSupported()
);
tabAdapter = new TabAdapter(getChildFragmentManager()); tabAdapter = new TabAdapter(getChildFragmentManager());
binding.viewPager.setAdapter(tabAdapter); binding.viewPager.setAdapter(tabAdapter);
binding.tabLayout.setupWithViewPager(binding.viewPager); binding.tabLayout.setupWithViewPager(binding.viewPager);
@ -645,8 +651,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
return; return;
} }
binding.errorContentNotSupported.setVisibility(View.VISIBLE); binding.emptyStateView.setVisibility(View.VISIBLE);
binding.channelKaomoji.setText("(︶︹︺)");
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
} }
} }

View File

@ -26,6 +26,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder; import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; 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.ChannelTabHelper;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.PlayButtonHelper; import org.schabi.newpipe.util.PlayButtonHelper;
@ -79,6 +80,12 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
return inflater.inflate(R.layout.fragment_channel_tab, container, false); return inflater.inflate(R.layout.fragment_channel_tab, container, false);
} }
@Override
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
EmptyStateUtil.setEmptyStateComposable(rootView.findViewById(R.id.empty_state_view));
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();

View File

@ -64,6 +64,8 @@ import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -344,6 +346,10 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
EmptyStateUtil.setEmptyStateComposable(
searchBinding.emptyStateView,
EmptyStateSpec.Companion.getNoSearchResult());
searchBinding.suggestionsList.setAdapter(suggestionListAdapter); searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
// animations are just strange and useless, since the suggestions keep changing too much // animations are just strange and useless, since the suggestions keep changing too much
searchBinding.suggestionsList.setItemAnimator(null); searchBinding.suggestionsList.setItemAnimator(null);

View File

@ -9,10 +9,6 @@ inline fun <reified T : Parcelable> Bundle.parcelable(key: String?): T? {
return BundleCompat.getParcelable(this, key, T::class.java) return BundleCompat.getParcelable(this, key, T::class.java)
} }
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
}
inline fun <reified T : Serializable> Bundle.serializable(key: String?): T? { inline fun <reified T : Serializable> Bundle.serializable(key: String?): T? {
return BundleCompat.getSerializable(this, key, T::class.java) return BundleCompat.getSerializable(this, key, T::class.java)
} }

View File

@ -38,6 +38,8 @@ import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder; import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager; 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.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.debounce.DebounceSavable; import org.schabi.newpipe.util.debounce.DebounceSavable;
@ -123,6 +125,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
itemListAdapter.setUseItemHandle(true); itemListAdapter.setUseItemHandle(true);
EmptyStateUtil.setEmptyStateComposable(
rootView.findViewById(R.id.empty_state_view),
EmptyStateSpec.Companion.getNoBookmarkedPlaylist()
);
} }
@Override @Override

View File

@ -74,6 +74,7 @@ import org.schabi.newpipe.ktx.slideUp
import org.schabi.newpipe.local.feed.item.StreamItem import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedLoadService import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.local.subscription.SubscriptionManager import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
import org.schabi.newpipe.util.DeviceUtils import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.NavigationHelper
@ -132,6 +133,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) { override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
// super.onViewCreated() calls initListeners() which require the binding to be initialized // super.onViewCreated() calls initListeners() which require the binding to be initialized
_feedBinding = FragmentFeedBinding.bind(rootView) _feedBinding = FragmentFeedBinding.bind(rootView)
feedBinding.emptyStateView.setEmptyStateComposable()
super.onViewCreated(rootView, savedInstanceState) super.onViewCreated(rootView, savedInstanceState)
val factory = FeedViewModel.getFactory(requireContext(), groupId) val factory = FeedViewModel.getFactory(requireContext(), groupId)

View File

@ -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.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
import org.schabi.newpipe.streams.io.StoredFileHelper 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.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ServiceHelper import org.schabi.newpipe.util.ServiceHelper
@ -257,6 +258,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
binding.itemsList.adapter = groupAdapter binding.itemsList.adapter = groupAdapter
binding.itemsList.itemAnimator = null binding.itemsList.itemAnimator = null
binding.emptyStateView.setEmptyStateComposable()
viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java] viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) } viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) {

View File

@ -3,14 +3,18 @@ package org.schabi.newpipe.local.subscription.item
import android.view.View import android.view.View
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R 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 * When there are no subscriptions, show a hint to the user about how to import subscriptions
*/ */
class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewBinding>() { class ImportSubscriptionsHintPlaceholderItem : BindableItem<ListEmptyViewSubscriptionsBinding>() {
override fun getLayout(): Int = R.layout.list_empty_view_subscriptions 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 getSpanSize(spanCount: Int, position: Int): Int = spanCount
override fun initializeViewBinding(view: View) = ListEmptyViewBinding.bind(view) override fun initializeViewBinding(view: View) = ListEmptyViewSubscriptionsBinding.bind(view)
} }

View File

@ -11,6 +11,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.compose.ui.platform.ComposeView;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; 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.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.local.subscription.SubscriptionManager; 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.ThemeHelper;
import org.schabi.newpipe.util.image.CoilHelper; import org.schabi.newpipe.util.image.CoilHelper;
@ -57,7 +60,7 @@ public class SelectChannelFragment extends DialogFragment {
private OnCancelListener onCancelListener = null; private OnCancelListener onCancelListener = null;
private ProgressBar progressBar; private ProgressBar progressBar;
private TextView emptyView; private ComposeView emptyView;
private RecyclerView recyclerView; private RecyclerView recyclerView;
private List<SubscriptionEntity> subscriptions = new Vector<>(); private List<SubscriptionEntity> subscriptions = new Vector<>();
@ -91,6 +94,9 @@ public class SelectChannelFragment extends DialogFragment {
progressBar = v.findViewById(R.id.progressBar); progressBar = v.findViewById(R.id.progressBar);
emptyView = v.findViewById(R.id.empty_state_view); emptyView = v.findViewById(R.id.empty_state_view);
EmptyStateUtil.setEmptyStateComposable(emptyView,
EmptyStateSpec.Companion.getNoSubscriptions());
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE); recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE); emptyView.setVisibility(View.GONE);

View File

@ -11,6 +11,7 @@ import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.compose.ui.platform.ComposeView;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; 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.error.UserAction;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager; 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 org.schabi.newpipe.util.image.CoilHelper;
import java.util.List; import java.util.List;
@ -40,7 +43,7 @@ public class SelectPlaylistFragment extends DialogFragment {
private OnSelectedListener onSelectedListener = null; private OnSelectedListener onSelectedListener = null;
private ProgressBar progressBar; private ProgressBar progressBar;
private TextView emptyView; private ComposeView emptyView;
private RecyclerView recyclerView; private RecyclerView recyclerView;
private Disposable disposable = null; private Disposable disposable = null;
@ -62,6 +65,8 @@ public class SelectPlaylistFragment extends DialogFragment {
recyclerView = v.findViewById(R.id.items_list); recyclerView = v.findViewById(R.id.items_list);
emptyView = v.findViewById(R.id.empty_state_view); emptyView = v.findViewById(R.id.empty_state_view);
EmptyStateUtil.setEmptyStateComposable(emptyView,
EmptyStateSpec.Companion.getNoBookmarkedPlaylist());
recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter(); final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter();
recyclerView.setAdapter(playlistAdapter); recyclerView.setAdapter(playlistAdapter);

View File

@ -11,6 +11,8 @@ import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding; import org.schabi.newpipe.databinding.SettingsPreferencesearchFragmentBinding;
import org.schabi.newpipe.ui.emptystate.EmptyStateSpec;
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
import java.util.List; import java.util.List;
@ -39,6 +41,9 @@ public class PreferenceSearchFragment extends Fragment {
binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false); binding = SettingsPreferencesearchFragmentBinding.inflate(inflater, container, false);
binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext())); binding.searchResults.setLayoutManager(new LinearLayoutManager(getContext()));
EmptyStateUtil.setEmptyStateComposable(
binding.emptyStateView,
EmptyStateSpec.Companion.getNoSearchMaxSizeResult());
adapter = new PreferenceSearchAdapter(); adapter = new PreferenceSearchAdapter();
adapter.setOnItemClickListener(this::onItemClicked); adapter.setOnItemClickListener(this::onItemClicked);

View File

@ -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<AboutData>(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))
}
}
}

View File

@ -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<Library>(
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<Developer>().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()
)
)
)

View File

@ -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<Library>,
): List<Library> {
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<Library>,
licenses: ImmutableSet<License>,
): List<Library> {
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()
),
)
}

View File

@ -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),
)
}
}
}
}
}
}
}

View File

@ -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,
)
}
}
}
}
}
}

View File

@ -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<LicenseTabState> = _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<Library>?,
val thirdPartyLibraries: List<Library>?,
// null if dialog closed, empty if loading, otherwise license HTML content
val licenseDialogHtml: AnnotatedString?,
)
}

View File

@ -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)
}
}
}

View File

@ -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 = {}
)
}

View File

@ -26,9 +26,10 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfo import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.info_list.ItemViewMode 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.ItemList
import org.schabi.newpipe.ui.components.items.stream.StreamInfoItem 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.ui.theme.AppTheme
import org.schabi.newpipe.util.NO_SERVICE_ID import org.schabi.newpipe.util.NO_SERVICE_ID
@ -41,9 +42,6 @@ fun RelatedItems(info: StreamInfo) {
mutableStateOf(sharedPreferences.getBoolean(key, false)) mutableStateOf(sharedPreferences.getBoolean(key, false))
} }
if (info.relatedItems.isEmpty()) {
NoItemsMessage(message = R.string.no_videos)
} else {
ItemList( ItemList(
items = info.relatedItems, items = info.relatedItems,
mode = ItemViewMode.LIST, mode = ItemViewMode.LIST,
@ -75,9 +73,13 @@ fun RelatedItems(info: StreamInfo) {
} }
} }
} }
if (info.relatedItems.isEmpty()) {
item {
EmptyStateComposable(EmptyStateSpec.NoVideos)
}
}
} }
) )
}
} }
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)

View File

@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.pluralStringResource 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.Preview
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.dp 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.paging.CommentRepliesSource
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
import org.schabi.newpipe.ui.components.common.LoadingIndicator 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.ui.theme.AppTheme
@Composable @Composable
@ -130,13 +132,17 @@ private fun CommentRepliesDialog(
val refresh = comments.loadState.refresh val refresh = comments.loadState.refresh
if (refresh is LoadState.Loading) { if (refresh is LoadState.Loading) {
LoadingIndicator(modifier = Modifier.padding(top = 8.dp)) LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
} else { } else if (refresh is LoadState.Error) {
val message = if (refresh is LoadState.Error) { // TODO use error panel instead
R.string.error_unable_to_load_comments EmptyStateComposable(
} else { EmptyStateSpec.DisabledComments.copy(
R.string.no_comments descriptionText = {
stringResource(R.string.error_unable_to_load_comments)
} }
NoItemsMessage(message) )
)
} else {
EmptyStateComposable(EmptyStateSpec.NoComments)
} }
} }
} else { } else {

View File

@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.pluralStringResource 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.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.extractor.stream.Description
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
import org.schabi.newpipe.ui.components.common.LoadingIndicator 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.ui.theme.AppTheme
import org.schabi.newpipe.viewmodels.CommentsViewModel import org.schabi.newpipe.viewmodels.CommentsViewModel
import org.schabi.newpipe.viewmodels.util.Resource import org.schabi.newpipe.viewmodels.util.Resource
@ -66,11 +68,11 @@ private fun CommentSection(
if (commentInfo.isCommentsDisabled) { if (commentInfo.isCommentsDisabled) {
item { item {
NoItemsMessage(R.string.comments_are_disabled) EmptyStateComposable(EmptyStateSpec.DisabledComments)
} }
} else if (count == 0) { } else if (count == 0) {
item { item {
NoItemsMessage(R.string.no_comments) EmptyStateComposable(EmptyStateSpec.NoComments)
} }
} else { } else {
// do not show anything if the comment count is unknown // do not show anything if the comment count is unknown
@ -95,7 +97,14 @@ private fun CommentSection(
is LoadState.Error -> { is LoadState.Error -> {
item { 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 -> { is Resource.Error -> {
item { 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)
}
)
)
} }
} }
} }

View File

@ -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) },
)
}
}

View File

@ -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
)
}
}
}
}

View File

@ -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))
}
}
}

View File

@ -22,6 +22,7 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.compose.ui.platform.ComposeView;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
@ -34,6 +35,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard; import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.ui.emptystate.EmptyStateUtil;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File; 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); mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
// Views // 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); mList = v.findViewById(R.id.mission_recycler);
// Init layouts managers // Init layouts managers

View File

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="org.schabi.newpipe.about.AboutActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/ThemeOverlay.AppCompat.DayNight.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/about_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ToolbarTheme"
app:layout_scrollFlags="scroll|enterAlways" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/about_tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabTextColor="@color/white"
app:tabIndicatorColor="@color/white" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/about_viewPager2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,148 +0,0 @@
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.schabi.newpipe.about.AboutActivity$AboutFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:contentDescription="@string/app_name"
android:src="@mipmap/ic_launcher" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/app_name"
android:textAppearance="@android:style/TextAppearance.Large" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/about_app_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:textAppearance="@android:style/TextAppearance.Medium"
tools:text="0.9.9" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="5dp"
android:text="@string/app_description" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:text="@string/faq_title"
android:textAppearance="@android:style/TextAppearance.Medium" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/faq_description" />
<Button
android:id="@+id/faq_link"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/faq" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:text="@string/contribution_title"
android:textAppearance="@android:style/TextAppearance.Medium" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/contribution_encouragement" />
<Button
android:id="@+id/about_github_link"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/view_on_github" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:text="@string/donation_title"
android:textAppearance="@android:style/TextAppearance.Medium" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/donation_encouragement" />
<Button
android:id="@+id/about_donation_link"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/give_back" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:text="@string/website_title"
android:textAppearance="@android:style/TextAppearance.Medium" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/website_encouragement" />
<Button
android:id="@+id/about_website_link"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/open_in_browser" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:text="@string/privacy_policy_title"
android:textAppearance="@android:style/TextAppearance.Medium" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/privacy_policy_encouragement" />
<Button
android:id="@+id/about_privacy_policy_link"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/read_privacy_policy" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -24,15 +24,15 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<include <androidx.compose.ui.platform.ComposeView
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
layout="@layout/list_empty_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:layout_marginTop="50dp" android:layout_marginTop="50dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible"
/>
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -168,37 +168,14 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<LinearLayout <androidx.compose.ui.platform.ComposeView
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:orientation="vertical"
android:paddingTop="90dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible"
/>
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/channel_kaomoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="10dp"
android:fontFamily="monospace"
android:text="(︶︹︺)"
android:textSize="35sp"
tools:ignore="HardcodedText" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/error_content_not_supported"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/content_not_supported"
android:textSize="15sp"
android:visibility="gone" />
</LinearLayout>
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include

View File

@ -20,15 +20,15 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<include <androidx.compose.ui.platform.ComposeView
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
layout="@layout/list_empty_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:gravity="center" android:gravity="center"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible"
/>
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include

View File

@ -20,36 +20,14 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<LinearLayout <androidx.compose.ui.platform.ComposeView
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:orientation="vertical"
android:paddingTop="90dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible"
/>
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/channel_kaomoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="10dp"
android:fontFamily="monospace"
android:text="(╯°-°)╯"
android:textSize="35sp"
tools:ignore="HardcodedText,UnusedAttribute" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/channel_no_videos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/empty_view_no_videos"
android:textSize="24sp" />
</LinearLayout>
<!--ERROR PANEL--> <!--ERROR PANEL-->
<include <include

View File

@ -7,12 +7,13 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<include <androidx.compose.ui.platform.ComposeView
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
layout="@layout/list_empty_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="90dp" /> android:layout_marginTop="90dp"
/>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</RelativeLayout> </RelativeLayout>

View File

@ -140,15 +140,15 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<include <androidx.compose.ui.platform.ComposeView
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
layout="@layout/list_empty_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:layout_marginTop="50dp" android:layout_marginTop="50dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible"
/>
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin">
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginRight="@dimen/activity_vertical_margin"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/app_license_title"
android:textAppearance="@android:style/TextAppearance.Large" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:text="@string/app_license" />
<Button
android:id="@+id/licenses_app_read_license"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginRight="@dimen/activity_vertical_margin"
android:text="@string/read_full_license" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="10dp"
android:paddingRight="@dimen/activity_horizontal_margin"
android:text="@string/title_licenses"
android:textAppearance="?android:attr/textAppearanceLarge" />
<LinearLayout
android:id="@+id/licenses_software_components"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -52,33 +52,14 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<LinearLayout <androidx.compose.ui.platform.ComposeView
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:orientation="vertical"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible"
/>
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="10dp"
android:fontFamily="monospace"
android:text="╰(°●°╰)"
android:textSize="35sp"
tools:ignore="HardcodedText,UnusedAttribute" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/search_no_results"
android:textSize="24sp" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/suggestions_panel" android:id="@+id/suggestions_panel"

View File

@ -24,16 +24,16 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<include <androidx.compose.ui.platform.ComposeView
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
layout="@layout/list_empty_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/items_list" android:layout_below="@id/items_list"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:layout_marginTop="50dp" android:layout_marginTop="50dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible"
/>
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:textAppearance="@android:style/TextAppearance.Medium"
tools:text="Software Name" />
<TextView
android:id="@+id/copyright"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:paddingBottom="10dp"
tools:text="@string/copyright" />
</RelativeLayout>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="128dp"
android:orientation="vertical">
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="¯\\_(ツ)_/¯"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="HardcodedText" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="6dp"
android:gravity="center"
android:paddingHorizontal="16dp"
android:text="@string/empty_list_subtitle" />
</LinearLayout>

View File

@ -1,25 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.compose.ui.platform.ComposeView
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="128dp" android:minHeight="128dp"
android:orientation="vertical"> xmlns:android="http://schemas.android.com/apk/res/android" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="¯\\_(ツ)_/¯"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="HardcodedText" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="6dp"
android:gravity="center"
android:paddingHorizontal="16dp"
android:text="@string/import_subscriptions_hint" />
</LinearLayout>

View File

@ -3,10 +3,11 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include <androidx.compose.ui.platform.ComposeView
android:id="@+id/list_empty_view" android:id="@+id/empty_state_view"
layout="@layout/list_empty_view" android:layout_width="match_parent"
android:visibility="gone" /> android:layout_height="wrap_content"
android:minHeight="128dp" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/mission_recycler" android:id="@+id/mission_recycler"

View File

@ -24,14 +24,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:listitem="@layout/select_channel_item" /> tools:listitem="@layout/select_channel_item" />
<androidx.compose.ui.platform.ComposeView
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="10dp" android:layout_margin="10dp" />
android:text="@string/no_channel_subscribed_yet"
android:textAppearance="?android:attr/textAppearanceListItem" />
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"

View File

@ -26,14 +26,11 @@
</androidx.recyclerview.widget.RecyclerView> </androidx.recyclerview.widget.RecyclerView>
<androidx.compose.ui.platform.ComposeView
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="10dp" android:layout_margin="10dp" />
android:text="@string/no_playlist_bookmarked_yet"
android:textAppearance="?android:attr/textAppearanceListItem" />
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"

View File

@ -12,33 +12,14 @@
android:layout_height="4dp" android:layout_height="4dp"
android:background="?attr/toolbar_shadow" /> android:background="?attr/toolbar_shadow" />
<LinearLayout <androidx.compose.ui.platform.ComposeView
android:id="@+id/empty_state_view" android:id="@+id/empty_state_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:orientation="vertical"
android:visibility="gone" android:visibility="gone"
tools:visibility="gone"> tools:visibility="gone"
/>
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="10dp"
android:fontFamily="monospace"
android:text="╰(°●°╰)"
android:textSize="35sp"
tools:ignore="HardcodedText,UnusedAttribute" />
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/search_no_results"
android:textSize="24sp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchResults" android:id="@+id/searchResults"

View File

@ -18,10 +18,13 @@
<string name="sha1">SHA-1</string> <string name="sha1">SHA-1</string>
<string name="recaptcha">reCAPTCHA</string> <string name="recaptcha">reCAPTCHA</string>
<string name="github_url">https://github.com/TeamNewPipe/NewPipe</string> <string name="github_url">https://github.com/TeamNewPipe/NewPipe</string>
<string name="newpipe_extractor_github_url">https://github.com/TeamNewPipe/NewPipeExtractor</string>
<string name="donation_url">https://newpipe.net/donate/</string> <string name="donation_url">https://newpipe.net/donate/</string>
<string name="website_url">https://newpipe.net/</string> <string name="website_url">https://newpipe.net/</string>
<string name="privacy_policy_url">https://newpipe.net/legal/privacy/</string> <string name="privacy_policy_url">https://newpipe.net/legal/privacy/</string>
<string name="faq_url">https://newpipe.net/FAQ/</string> <string name="faq_url">https://newpipe.net/FAQ/</string>
<string name="team_newpipe">TeamNewPipe</string>
<string name="newpipe_extractor">NewPipeExtractor</string>
<string name="service_kiosk_string">%1$s/%2$s</string> <string name="service_kiosk_string">%1$s/%2$s</string>
<string name="youtube">YouTube</string> <string name="youtube">YouTube</string>
<string name="preferred_open_action_share_menu_title">@string/app_name</string> <string name="preferred_open_action_share_menu_title">@string/app_name</string>

View File

@ -857,6 +857,7 @@
<string name="show_less">Show less</string> <string name="show_less">Show less</string>
<string name="import_settings_vulnerable_format">The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.</string> <string name="import_settings_vulnerable_format">The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.</string>
<string name="auto_queue_description">Next</string> <string name="auto_queue_description">Next</string>
<string name="newpipe_extractor_description">NewPipeExtractor is a library for extracting things from streaming sites. It is a core component of NewPipe, but could be used independently.</string>
<plurals name="comments"> <plurals name="comments">
<item quantity="one">%d comment</item> <item quantity="one">%d comment</item>
<item quantity="other">%d comments</item> <item quantity="other">%d comments</item>

View File

@ -1,15 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '2.0.21'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.7.1' classpath libs.android.tools.build.gradle
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath libs.kotlin.gradle.plugin
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1' classpath libs.hilt.android.gradle.plugin
classpath libs.aboutlibraries.plugin
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

157
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,157 @@
[versions]
aboutLibraries = "11.2.3"
acraCore = "5.11.3"
androidState = "1.4.1"
androidx-junit = "1.1.5"
appcompat = "1.6.1"
assertjCore = "3.24.2"
auto-service = "1.1.1"
bridge = "2.0.2"
cardview = "1.0.0"
checkstyle = "10.12.1"
coil = "3.0.4"
constraintlayout = "2.1.4"
core-ktx = "1.12.0"
desugar-jdk-libs-nio = "2.0.4"
documentFile = "1.0.1"
exoplayer = "2.18.7"
fragment-compose = "1.8.2"
gradle = "8.7.1"
groupie = "2.10.1"
hilt = "2.51.1"
jetpack-compose = "2024.10.01"
jsoup = "1.17.2"
junit = "4.13.2"
kotlin = "2.0.21"
kotlinxCoroutinesRx3 = "1.8.1"
ktlint = "0.45.2"
lazycolumnscrollbar = "2.2.0"
leakcanary = "2.12"
lifecycle = "2.6.2"
localbroadcastmanager = "1.1.0"
markwon = "4.6.2"
material = "1.11.0"
media = "1.7.0"
mockitoCore = "5.6.0"
navigationCompose = "2.8.3"
okhttp = "4.12.0"
pagingCompose = "3.3.2"
preference = "1.2.1"
prettytime = "5.0.8.Final"
processPhoenix = "2.1.2"
recyclerview = "1.3.2"
room = "2.6.1"
runner = "1.5.2"
rxandroid = "3.0.2"
rxbinding = "4.0.0"
rxjava = "3.1.8"
sonarqube = "4.0.0.2929"
stetho = "1.6.0"
swiperefreshlayout = "1.1.0"
# 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/
teamnewpipe-filepicker = "5.0.0"
teamnewpipe-nanojson = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
teamnewpipe-newpipe-extractor = "d3d5f2b3f03a5f2b479b9f6fdf1c2555cbb9de0e"
viewpager2 = "1.1.0-beta02"
work = "2.8.1"
[plugins]
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin" }
android-application = { id = "com.android.application" }
checkstyle = { id = "checkstyle" }
hilt = { id = "com.google.dagger.hilt.android" }
kotlin-android = { id = "kotlin-android" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-kapt = { id = "kotlin-kapt" }
kotlin-parcelize = { id = "kotlin-parcelize" }
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
[libraries]
aboutlibraries-compose-m3 = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "aboutLibraries" }
aboutlibraries-plugin = { group = "com.mikepenz.aboutlibraries.plugin", name = "aboutlibraries-plugin", version.ref = "aboutLibraries" }
acra-core = { group = "ch.acra", name = "acra-core", version.ref = "acraCore" }
android-state = { group = "com.evernote", name = "android-state", version.ref = "androidState" }
android-state-processor = { group = "com.evernote", name = "android-state-processor", version.ref = "androidState" }
android-tools-build-gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "gradle" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-cardview = { group = "androidx.cardview", name = "cardview", version.ref = "cardview" }
androidx-compose-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "jetpack-compose" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-ui-text = { group = "androidx.compose.ui", name = "ui-text" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentFile" }
androidx-fragment-compose = { group = "androidx.fragment", name = "fragment-compose", version.ref = "fragment-compose" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-junit" }
androidx-lifecycle-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose" }
androidx-localbroadcastmanager = { group = "androidx.localbroadcastmanager", name = "localbroadcastmanager", version.ref = "localbroadcastmanager" }
androidx-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-media = { group = "androidx.media", name = "media", version.ref = "media" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "pagingCompose" }
androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-rxjava3 = { group = "androidx.room", name = "room-rxjava3", version.ref = "room" }
androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-viewpager2 = { group = "androidx.viewpager2", name = "viewpager2", version.ref = "viewpager2" }
androidx-work-runtime = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
androidx-work-rxjava3 = { group = "androidx.work", name = "work-rxjava3", version.ref = "work" }
assertj-core = { group = "org.assertj", name = "assertj-core", version.ref = "assertjCore" }
auto-service = { group = "com.google.auto.service", name = "auto-service-annotations", version.ref = "auto-service" }
auto-service-kapt = { group = "com.google.auto.service", name = "auto-service", version.ref = "auto-service" }
coil-compose = { group = "io.coil-kt.coil3", name = 'coil-compose', version.ref = "coil" }
coil-network-okhttp = { group = "io.coil-kt.coil3", name = 'coil-network-okhttp', version.ref = "coil" }
desugar-jdk-libs-nio = { group = "com.android.tools", name = "desugar_jdk_libs_nio", version.ref = "desugar-jdk-libs-nio" }
exoplayer-core = { group = "com.google.android.exoplayer", name = "exoplayer-core", version.ref = "exoplayer" }
exoplayer-dash = { module = "com.google.android.exoplayer:exoplayer-dash", version.ref = "exoplayer" }
exoplayer-database = { group = "com.google.android.exoplayer", name = "exoplayer-database", version.ref = "exoplayer" }
exoplayer-datasource = { group = "com.google.android.exoplayer", name = "exoplayer-datasource", version.ref = "exoplayer" }
exoplayer-hls = { group = "com.google.android.exoplayer", name = "exoplayer-hls", version.ref = "exoplayer" }
exoplayer-smoothstreaming = { group = "com.google.android.exoplayer", name = "exoplayer-smoothstreaming", version.ref = "exoplayer" }
exoplayer-ui = { group = "com.google.android.exoplayer", name = "exoplayer-ui", version.ref = "exoplayer" }
extension-mediasession = { group = "com.google.android.exoplayer", name = "extension-mediasession", version.ref = "exoplayer" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-gradle-plugin = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
jsoup = { group = "org.jsoup", name = "jsoup", version.ref = "jsoup" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
kotlinx-coroutines-rx3 = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-rx3", version.ref = "kotlinxCoroutinesRx3" }
lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazycolumnscrollbar" }
leakcanary-android-core = { module = "com.squareup.leakcanary:leakcanary-android-core", version.ref = "leakcanary" }
leakcanary-object-watcher = { group = "com.squareup.leakcanary", name = "leakcanary-object-watcher-android", version.ref = "leakcanary" }
leakcanary-plumber-android = { group = "com.squareup.leakcanary", name = "plumber-android", version.ref = "leakcanary" }
lisawray-groupie = { group = "com.github.lisawray.groupie", name = "groupie", version.ref = "groupie" }
lisawray-groupie-viewbinding = { group = "com.github.lisawray.groupie", name = "groupie-viewbinding", version.ref = "groupie" }
livefront-bridge = { group = "com.github.livefront", name = "bridge", version.ref = "bridge" }
markwon-core = { group = "io.noties.markwon", name = "core", version.ref = "markwon" }
markwon-linkify = { group = "io.noties.markwon", name = "linkify", version.ref = "markwon" }
mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockitoCore" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
prettytime = { group = "org.ocpsoft.prettytime", name = "prettytime", version.ref = "prettytime" }
process-phoenix = { group = "com.jakewharton", name = "process-phoenix", version.ref = "processPhoenix" }
rxbinding4-rxbinding = { group = "com.jakewharton.rxbinding4", name = "rxbinding", version.ref = "rxbinding" }
rxjava3-rxandroid = { group = "io.reactivex.rxjava3", name = "rxandroid", version.ref = "rxandroid" }
rxjava3-rxjava = { group = "io.reactivex.rxjava3", name = "rxjava", version.ref = "rxjava" }
stetho = { group = "com.facebook.stetho", name = "stetho", version.ref = "stetho" }
stetho-okhttp3 = { group = "com.facebook.stetho", name = "stetho-okhttp3", version.ref = "stetho" }
teamnewpipe-nanojson = { group = "com.github.TeamNewPipe", name = "nanojson", version.ref = "teamnewpipe-nanojson" }
teamnewpipe-newpipe-extractor = { group = "com.github.TeamNewPipe", name = "NewPipeExtractor", version.ref = "teamnewpipe-newpipe-extractor" }
teamnewpipe-nononsense-filepicker = { group = "com.github.TeamNewPipe", name = "NoNonsense-FilePicker", version.ref = "teamnewpipe-filepicker" }
tools-checkstyle = { group = "com.puppycrawl.tools", name = "checkstyle", version.ref = "checkstyle" }
tools-ktlint = { group = "com.pinterest", name = "ktlint", version.ref = "ktlint" }