From b36eb5f0c6cc5b8bb1a0886fc66cc84689538076 Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Thu, 1 Aug 2024 09:08:04 +0530
Subject: [PATCH 01/15] Rename .java to .kt
---
.../videos/{RelatedItemsFragment.java => RelatedItemsFragment.kt} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename app/src/main/java/org/schabi/newpipe/fragments/list/videos/{RelatedItemsFragment.java => RelatedItemsFragment.kt} (100%)
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.kt
similarity index 100%
rename from app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.java
rename to app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.kt
From a73da16d2766795d91e663f1e4a4c7cc8dc14a66 Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Thu, 1 Aug 2024 09:08:06 +0530
Subject: [PATCH 02/15] Migrate related items fragment to Jetpack Compose
---
app/build.gradle | 16 +-
.../list/videos/RelatedItemsFragment.kt | 205 +++---------------
.../java/org/schabi/newpipe/ktx/Bundle.kt | 5 +
.../schabi/newpipe/local/feed/FeedFragment.kt | 5 +-
.../subscription/SubscriptionFragment.kt | 1 +
.../subscription/dialog/FeedGroupDialog.kt | 1 +
.../NotificationModeConfigFragment.kt | 2 +
.../newpipe/ui/components/items/ItemList.kt | 111 ++++++++++
.../ui/components/items/ItemThumbnail.kt | 83 +++++++
.../items/playlist/PlaylistListItem.kt | 72 ++++++
.../components/items/stream/StreamListItem.kt | 87 ++++++++
.../ui/components/items/stream/StreamMenu.kt | 75 +++++++
.../ui/components/items/stream/StreamUtils.kt | 68 ++++++
.../ui/components/video/RelatedItems.kt | 92 ++++++++
.../java/org/schabi/newpipe/util/Constants.kt | 1 +
.../res/layout/fragment_related_items.xml | 70 ------
.../main/res/layout/related_items_header.xml | 33 ---
app/src/main/res/values/strings.xml | 1 +
build.gradle | 2 +-
19 files changed, 648 insertions(+), 282 deletions(-)
create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt
create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt
create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamUtils.kt
create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt
delete mode 100644 app/src/main/res/layout/fragment_related_items.xml
delete mode 100644 app/src/main/res/layout/related_items_header.xml
diff --git a/app/build.gradle b/app/build.gradle
index 9ea725ad90e..c3699669861 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -9,6 +9,7 @@ plugins {
id "kotlin-parcelize"
id "checkstyle"
id "org.sonarqube" version "4.0.0.2929"
+ id "org.jetbrains.kotlin.plugin.compose" version "${kotlin_version}"
}
android {
@@ -104,10 +105,6 @@ android {
'META-INF/COPYRIGHT']
}
}
-
- composeOptions {
- kotlinCompilerExtensionVersion = "1.5.3"
- }
}
ext {
@@ -267,7 +264,7 @@ dependencies {
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
// Image loading
- implementation 'io.coil-kt:coil:2.7.0'
+ implementation 'io.coil-kt:coil-compose:2.7.0'
// Markdown library for Android
implementation "io.noties.markwon:core:${markwonVersion}"
@@ -289,10 +286,15 @@ dependencies {
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
// Jetpack Compose
- implementation(platform('androidx.compose:compose-bom:2024.02.01'))
- implementation 'androidx.compose.material3:material3'
+ implementation(platform('androidx.compose:compose-bom:2024.06.00'))
+ implementation 'androidx.compose.material3:material3:1.3.0-beta05'
+ implementation 'androidx.compose.material3.adaptive:adaptive:1.0.0-beta04'
implementation 'androidx.activity:activity-compose'
implementation 'androidx.compose.ui:ui-tooling-preview'
+ implementation 'androidx.compose.ui:ui-text:1.7.0-beta06' // Needed for parsing HTML to AnnotatedString
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-compose'
+ implementation 'androidx.paging:paging-compose:3.3.1'
+ implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0'
/** Debugging **/
// Memory leak detection
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.kt
index e46937ede3d..503179cc103 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.kt
@@ -1,176 +1,41 @@
-package org.schabi.newpipe.fragments.list.videos;
-
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.preference.PreferenceManager;
-
-import org.schabi.newpipe.R;
-import org.schabi.newpipe.databinding.RelatedItemsHeaderBinding;
-import org.schabi.newpipe.error.UserAction;
-import org.schabi.newpipe.extractor.InfoItem;
-import org.schabi.newpipe.extractor.ListExtractor;
-import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
-import org.schabi.newpipe.info_list.ItemViewMode;
-import org.schabi.newpipe.ktx.ViewUtils;
-
-import java.io.Serializable;
-import java.util.function.Supplier;
-
-import io.reactivex.rxjava3.core.Single;
-
-public class RelatedItemsFragment extends BaseListInfoFragment
- implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String INFO_KEY = "related_info_key";
-
- private RelatedItemsInfo relatedItemsInfo;
-
- /*//////////////////////////////////////////////////////////////////////////
- // Views
- //////////////////////////////////////////////////////////////////////////*/
-
- private RelatedItemsHeaderBinding headerBinding;
-
- public static RelatedItemsFragment getInstance(final StreamInfo info) {
- final RelatedItemsFragment instance = new RelatedItemsFragment();
- instance.setInitialData(info);
- return instance;
- }
-
- public RelatedItemsFragment() {
- super(UserAction.REQUESTED_STREAM);
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // LifeCycle
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- public View onCreateView(@NonNull final LayoutInflater inflater,
- @Nullable final ViewGroup container,
- @Nullable final Bundle savedInstanceState) {
- return inflater.inflate(R.layout.fragment_related_items, container, false);
- }
-
- @Override
- public void onDestroyView() {
- headerBinding = null;
- super.onDestroyView();
- }
-
- @Override
- protected Supplier getListHeaderSupplier() {
- if (relatedItemsInfo == null || relatedItemsInfo.getRelatedItems() == null) {
- return null;
- }
-
- headerBinding = RelatedItemsHeaderBinding
- .inflate(activity.getLayoutInflater(), itemsList, false);
-
- final SharedPreferences pref = PreferenceManager
- .getDefaultSharedPreferences(requireContext());
- final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
- headerBinding.autoplaySwitch.setChecked(autoplay);
- headerBinding.autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
- PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
- .putBoolean(getString(R.string.auto_queue_key), b).apply());
-
- return headerBinding::getRoot;
- }
-
- @Override
- protected Single> loadMoreItemsLogic() {
- return Single.fromCallable(ListExtractor.InfoItemsPage::emptyPage);
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Contract
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- protected Single loadResult(final boolean forceLoad) {
- return Single.fromCallable(() -> relatedItemsInfo);
- }
-
- @Override
- public void showLoading() {
- super.showLoading();
- if (headerBinding != null) {
- headerBinding.getRoot().setVisibility(View.INVISIBLE);
- }
- }
-
- @Override
- public void handleResult(@NonNull final RelatedItemsInfo result) {
- super.handleResult(result);
-
- if (headerBinding != null) {
- headerBinding.getRoot().setVisibility(View.VISIBLE);
- }
- ViewUtils.slideUp(requireView(), 120, 96, 0.06f);
-
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Utils
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- public void setTitle(final String title) {
- // Nothing to do - override parent
- }
-
- @Override
- public void onCreateOptionsMenu(@NonNull final Menu menu,
- @NonNull final MenuInflater inflater) {
- // Nothing to do - override parent
- }
-
- private void setInitialData(final StreamInfo info) {
- super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
- if (this.relatedItemsInfo == null) {
- this.relatedItemsInfo = new RelatedItemsInfo(info);
- }
- }
-
- @Override
- public void onSaveInstanceState(@NonNull final Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putSerializable(INFO_KEY, relatedItemsInfo);
- }
-
- @Override
- protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
- super.onRestoreInstanceState(savedState);
- final Serializable serializable = savedState.getSerializable(INFO_KEY);
- if (serializable instanceof RelatedItemsInfo) {
- this.relatedItemsInfo = (RelatedItemsInfo) serializable;
- }
- }
-
- @Override
- public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
- final String key) {
- if (headerBinding != null && getString(R.string.auto_queue_key).equals(key)) {
- headerBinding.autoplaySwitch.setChecked(sharedPreferences.getBoolean(key, false));
+package org.schabi.newpipe.fragments.list.videos
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.platform.ComposeView
+import androidx.core.os.bundleOf
+import androidx.fragment.app.Fragment
+import org.schabi.newpipe.extractor.stream.StreamInfo
+import org.schabi.newpipe.ktx.serializable
+import org.schabi.newpipe.ui.components.video.RelatedItems
+import org.schabi.newpipe.ui.theme.AppTheme
+import org.schabi.newpipe.util.KEY_INFO
+
+class RelatedItemsFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireContext()).apply {
+ setContent {
+ AppTheme {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ RelatedItems(requireArguments().serializable(KEY_INFO)!!)
+ }
+ }
+ }
}
}
- @Override
- protected ItemViewMode getItemViewMode() {
- ItemViewMode mode = super.getItemViewMode();
- // Only list mode is supported. Either List or card will be used.
- if (mode != ItemViewMode.LIST && mode != ItemViewMode.CARD) {
- mode = ItemViewMode.LIST;
+ companion object {
+ @JvmStatic
+ fun getInstance(info: StreamInfo) = RelatedItemsFragment().apply {
+ arguments = bundleOf(KEY_INFO to info)
}
- return mode;
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt b/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt
index 61721d5467c..e248b8b6c63 100644
--- a/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt
+++ b/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt
@@ -3,7 +3,12 @@ package org.schabi.newpipe.ktx
import android.os.Bundle
import android.os.Parcelable
import androidx.core.os.BundleCompat
+import java.io.Serializable
inline fun Bundle.parcelableArrayList(key: String?): ArrayList? {
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
}
+
+inline fun Bundle.serializable(key: String?): T? {
+ return BundleCompat.getSerializable(this, key, T::class.java)
+}
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
index e8c5b1e3497..b9929130905 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
@@ -202,6 +202,7 @@ class FeedFragment : BaseStateFragment() {
// Menu
// /////////////////////////////////////////////////////////////////////////
+ @Deprecated("Deprecated in Java")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
@@ -212,6 +213,7 @@ class FeedFragment : BaseStateFragment() {
inflater.inflate(R.menu.menu_feed_fragment, menu)
}
+ @Deprecated("Deprecated in Java")
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menu_item_feed_help) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
@@ -253,7 +255,7 @@ class FeedFragment : BaseStateFragment() {
viewModel.getShowFutureItemsFromPreferences()
)
- AlertDialog.Builder(context!!)
+ AlertDialog.Builder(requireContext())
.setTitle(R.string.feed_hide_streams_title)
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
checkedDialogItems[which] = isChecked
@@ -267,6 +269,7 @@ class FeedFragment : BaseStateFragment() {
.show()
}
+ @Deprecated("Deprecated in Java")
override fun onDestroyOptionsMenu() {
super.onDestroyOptionsMenu()
activity?.supportActionBar?.subtitle = null
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt
index fe232105913..59bbaee9d22 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt
@@ -129,6 +129,7 @@ class SubscriptionFragment : BaseStateFragment() {
// Menu
// ////////////////////////////////////////////////////////////////////////
+ @Deprecated("Deprecated in Java")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt
index 41761fb0102..954b872a637 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt
@@ -94,6 +94,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireActivity(), theme) {
+ @Deprecated("Deprecated in Java")
override fun onBackPressed() {
if (!this@FeedGroupDialog.onBackPressed()) {
super.onBackPressed()
diff --git a/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigFragment.kt
index 581768c304d..2df3e33b669 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigFragment.kt
@@ -77,11 +77,13 @@ class NotificationModeConfigFragment : Fragment() {
super.onDestroy()
}
+ @Deprecated("Deprecated in Java")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_notifications_channels, menu)
}
+ @Deprecated("Deprecated in Java")
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_toggle_all -> {
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
new file mode 100644
index 00000000000..5bcb79d51f7
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
@@ -0,0 +1,111 @@
+package org.schabi.newpipe.ui.components.items
+
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.fragment.app.FragmentActivity
+import androidx.preference.PreferenceManager
+import androidx.window.core.layout.WindowWidthSizeClass
+import my.nanihadesuka.compose.LazyColumnScrollbar
+import org.schabi.newpipe.R
+import org.schabi.newpipe.extractor.InfoItem
+import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
+import org.schabi.newpipe.extractor.stream.StreamInfoItem
+import org.schabi.newpipe.info_list.ItemViewMode
+import org.schabi.newpipe.ui.components.items.playlist.PlaylistListItem
+import org.schabi.newpipe.ui.components.items.stream.StreamListItem
+import org.schabi.newpipe.util.NavigationHelper
+
+@Composable
+fun ItemList(
+ items: List,
+ mode: ItemViewMode = determineItemViewMode(),
+ listHeader: LazyListScope.() -> Unit = {}
+) {
+ val context = LocalContext.current
+ val onClick = remember {
+ { item: InfoItem ->
+ val fragmentManager = (context as FragmentActivity).supportFragmentManager
+ if (item is StreamInfoItem) {
+ NavigationHelper.openVideoDetailFragment(
+ context, fragmentManager, item.serviceId, item.url, item.name, null, false
+ )
+ } else if (item is PlaylistInfoItem) {
+ NavigationHelper.openPlaylistFragment(
+ fragmentManager, item.serviceId, item.url, item.name
+ )
+ }
+ }
+ }
+
+ // Handle long clicks for stream items
+ // TODO: Adjust the menu display depending on where it was triggered
+ var selectedStream by remember { mutableStateOf(null) }
+ val onLongClick = remember {
+ { stream: StreamInfoItem ->
+ selectedStream = stream
+ }
+ }
+ val onDismissPopup = remember {
+ {
+ selectedStream = null
+ }
+ }
+
+ if (mode == ItemViewMode.GRID) {
+ // TODO: Implement grid layout using LazyVerticalGrid and LazyVerticalGridScrollbar.
+ } else {
+ // Card or list views
+ val listState = rememberLazyListState()
+
+ LazyColumnScrollbar(state = listState) {
+ LazyColumn(state = listState) {
+ listHeader()
+
+ items(items.size) {
+ val item = items[it]
+
+ // TODO: Implement card layouts.
+ if (item is StreamInfoItem) {
+ val isSelected = selectedStream == item
+ StreamListItem(item, isSelected, onClick, onLongClick, onDismissPopup)
+ } else if (item is PlaylistInfoItem) {
+ PlaylistListItem(item, onClick)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun determineItemViewMode(): ItemViewMode {
+ val listMode = PreferenceManager.getDefaultSharedPreferences(LocalContext.current)
+ .getString(
+ stringResource(R.string.list_view_mode_key),
+ stringResource(R.string.list_view_mode_value)
+ )
+
+ return when (listMode) {
+ stringResource(R.string.list_view_mode_list_key) -> ItemViewMode.LIST
+ stringResource(R.string.list_view_mode_grid_key) -> ItemViewMode.GRID
+ stringResource(R.string.list_view_mode_card_key) -> ItemViewMode.CARD
+ else -> {
+ // Auto mode - evaluate whether to use Grid based on screen real estate.
+ val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
+ if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) {
+ ItemViewMode.GRID
+ } else {
+ ItemViewMode.LIST
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt
new file mode 100644
index 00000000000..58b39955150
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt
@@ -0,0 +1,83 @@
+package org.schabi.newpipe.ui.components.items
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+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.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import org.schabi.newpipe.R
+import org.schabi.newpipe.extractor.InfoItem
+import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
+import org.schabi.newpipe.extractor.stream.StreamInfoItem
+import org.schabi.newpipe.util.Localization
+import org.schabi.newpipe.util.StreamTypeUtil
+import org.schabi.newpipe.util.image.ImageStrategy
+
+@Composable
+fun ItemThumbnail(
+ item: InfoItem,
+ modifier: Modifier = Modifier,
+ contentScale: ContentScale = ContentScale.Fit
+) {
+ Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) {
+ AsyncImage(
+ model = ImageStrategy.choosePreferredImage(item.thumbnails),
+ contentDescription = null,
+ placeholder = painterResource(R.drawable.placeholder_thumbnail_video),
+ error = painterResource(R.drawable.placeholder_thumbnail_video),
+ contentScale = contentScale,
+ modifier = modifier
+ )
+
+ val isLive = item is StreamInfoItem && StreamTypeUtil.isLiveStream(item.streamType)
+ val background = if (isLive) Color.Red else Color.Black
+ val nestedModifier = Modifier
+ .padding(2.dp)
+ .background(background.copy(alpha = 0.5f))
+ .padding(2.dp)
+
+ if (item is StreamInfoItem) {
+ Text(
+ text = if (isLive) {
+ stringResource(R.string.duration_live)
+ } else {
+ Localization.getDurationString(item.duration)
+ },
+ color = Color.White,
+ style = MaterialTheme.typography.bodySmall,
+ modifier = nestedModifier
+ )
+ } else if (item is PlaylistInfoItem) {
+ Row(modifier = nestedModifier, verticalAlignment = Alignment.CenterVertically) {
+ Image(
+ painter = painterResource(R.drawable.ic_playlist_play),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(Color.White),
+ modifier = Modifier.size(18.dp)
+ )
+
+ val context = LocalContext.current
+ Text(
+ text = Localization.localizeStreamCountMini(context, item.streamCount),
+ color = Color.White,
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.padding(start = 4.dp)
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt
new file mode 100644
index 00000000000..0cc8e68dd0f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt
@@ -0,0 +1,72 @@
+package org.schabi.newpipe.ui.components.items.playlist
+
+import android.content.res.Configuration
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+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.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import org.schabi.newpipe.extractor.InfoItem
+import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
+import org.schabi.newpipe.ui.components.items.ItemThumbnail
+import org.schabi.newpipe.ui.theme.AppTheme
+import org.schabi.newpipe.util.NO_SERVICE_ID
+
+@Composable
+fun PlaylistListItem(
+ playlist: PlaylistInfoItem,
+ onClick: (InfoItem) -> Unit = {},
+) {
+ Row(
+ modifier = Modifier
+ .clickable { onClick(playlist) }
+ .fillMaxWidth()
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ ItemThumbnail(
+ item = playlist,
+ modifier = Modifier.size(width = 98.dp, height = 55.dp)
+ )
+
+ Column {
+ Text(
+ text = playlist.name,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.titleSmall,
+ maxLines = 1
+ )
+
+ Text(
+ text = playlist.uploaderName.orEmpty(),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+}
+
+@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
+@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun PlaylistListItemPreview() {
+ val playlist = PlaylistInfoItem(NO_SERVICE_ID, "", "Playlist")
+ playlist.uploaderName = "Uploader"
+
+ AppTheme {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ PlaylistListItem(playlist)
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
new file mode 100644
index 00000000000..12cb1245494
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
@@ -0,0 +1,87 @@
+package org.schabi.newpipe.ui.components.items.stream
+
+import android.content.res.Configuration
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+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.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import org.schabi.newpipe.extractor.stream.StreamInfoItem
+import org.schabi.newpipe.ui.components.items.ItemThumbnail
+import org.schabi.newpipe.ui.theme.AppTheme
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun StreamListItem(
+ stream: StreamInfoItem,
+ isSelected: Boolean = false,
+ onClick: (StreamInfoItem) -> Unit = {},
+ onLongClick: (StreamInfoItem) -> Unit = {},
+ onDismissPopup: () -> Unit = {}
+) {
+ Box {
+ Row(
+ modifier = Modifier
+ .combinedClickable(
+ onLongClick = { onLongClick(stream) },
+ onClick = { onClick(stream) }
+ )
+ .fillMaxWidth()
+ .padding(12.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ ItemThumbnail(
+ item = stream,
+ modifier = Modifier.size(width = 98.dp, height = 55.dp)
+ )
+
+ Column {
+ Text(
+ text = stream.name,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.titleSmall,
+ maxLines = 1
+ )
+
+ Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall)
+
+ Text(
+ text = getStreamInfoDetail(stream),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+
+ if (isSelected) {
+ StreamMenu(stream, onDismissPopup)
+ }
+ }
+}
+
+@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
+@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun StreamListItemPreview(
+ @PreviewParameter(StreamItemPreviewProvider::class) stream: StreamInfoItem
+) {
+ AppTheme {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ StreamListItem(stream)
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
new file mode 100644
index 00000000000..a2c84647d60
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
@@ -0,0 +1,75 @@
+package org.schabi.newpipe.ui.components.items.stream
+
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.fragment.app.FragmentActivity
+import org.schabi.newpipe.R
+import org.schabi.newpipe.download.DownloadDialog
+import org.schabi.newpipe.extractor.stream.StreamInfo
+import org.schabi.newpipe.extractor.stream.StreamInfoItem
+import org.schabi.newpipe.util.SparseItemUtil
+import org.schabi.newpipe.util.external_communication.ShareUtils
+
+@Composable
+fun StreamMenu(
+ stream: StreamInfoItem,
+ onDismissRequest: () -> Unit
+) {
+ val context = LocalContext.current
+
+ // TODO: Implement remaining click actions
+ DropdownMenu(expanded = true, onDismissRequest = onDismissRequest) {
+ DropdownMenuItem(
+ text = { Text(text = stringResource(R.string.start_here_on_background)) },
+ onClick = onDismissRequest
+ )
+ DropdownMenuItem(
+ text = { Text(text = stringResource(R.string.start_here_on_popup)) },
+ onClick = onDismissRequest
+ )
+ DropdownMenuItem(
+ text = { Text(text = stringResource(R.string.download)) },
+ onClick = {
+ onDismissRequest()
+ SparseItemUtil.fetchStreamInfoAndSaveToDatabase(
+ context, stream.serviceId, stream.url
+ ) { info: StreamInfo ->
+ // TODO: Use an AlertDialog composable instead.
+ val downloadDialog = DownloadDialog(context, info)
+ val fragmentManager = (context as FragmentActivity).supportFragmentManager
+ downloadDialog.show(fragmentManager, "downloadDialog")
+ }
+ }
+ )
+ DropdownMenuItem(
+ text = { Text(text = stringResource(R.string.add_to_playlist)) },
+ onClick = onDismissRequest
+ )
+ DropdownMenuItem(
+ text = { Text(text = stringResource(R.string.share)) },
+ onClick = {
+ onDismissRequest()
+ ShareUtils.shareText(context, stream.name, stream.url, stream.thumbnails)
+ }
+ )
+ DropdownMenuItem(
+ text = { Text(text = stringResource(R.string.open_in_browser)) },
+ onClick = {
+ onDismissRequest()
+ ShareUtils.openUrlInBrowser(context, stream.url)
+ }
+ )
+ DropdownMenuItem(
+ text = { Text(text = stringResource(R.string.mark_as_watched)) },
+ onClick = onDismissRequest
+ )
+ DropdownMenuItem(
+ text = { Text(text = stringResource(R.string.show_channel_details)) },
+ onClick = onDismissRequest
+ )
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamUtils.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamUtils.kt
new file mode 100644
index 00000000000..cdfe613edf3
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamUtils.kt
@@ -0,0 +1,68 @@
+package org.schabi.newpipe.ui.components.items.stream
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import org.schabi.newpipe.extractor.Image
+import org.schabi.newpipe.extractor.stream.StreamInfoItem
+import org.schabi.newpipe.extractor.stream.StreamType
+import org.schabi.newpipe.util.Localization
+import org.schabi.newpipe.util.NO_SERVICE_ID
+import java.util.concurrent.TimeUnit
+
+fun StreamInfoItem(
+ serviceId: Int = NO_SERVICE_ID,
+ url: String = "",
+ name: String = "Stream",
+ streamType: StreamType,
+ uploaderName: String? = "Uploader",
+ uploaderUrl: String? = null,
+ uploaderAvatars: List = emptyList(),
+ duration: Long = TimeUnit.HOURS.toSeconds(1),
+ viewCount: Long = 10,
+ textualUploadDate: String = "1 month ago"
+) = StreamInfoItem(serviceId, url, name, streamType).apply {
+ this.uploaderName = uploaderName
+ this.uploaderUrl = uploaderUrl
+ this.uploaderAvatars = uploaderAvatars
+ this.duration = duration
+ this.viewCount = viewCount
+ this.textualUploadDate = textualUploadDate
+}
+
+@Composable
+internal fun getStreamInfoDetail(stream: StreamInfoItem): String {
+ val context = LocalContext.current
+
+ return rememberSaveable(stream) {
+ val count = stream.viewCount
+ val views = if (count >= 0) {
+ when (stream.streamType) {
+ StreamType.AUDIO_LIVE_STREAM -> Localization.listeningCount(context, count)
+ StreamType.LIVE_STREAM -> Localization.shortWatchingCount(context, count)
+ else -> Localization.shortViewCount(context, count)
+ }
+ } else {
+ ""
+ }
+ val date =
+ Localization.relativeTimeOrTextual(context, stream.uploadDate, stream.textualUploadDate)
+
+ if (views.isEmpty()) {
+ date.orEmpty()
+ } else if (date.isNullOrEmpty()) {
+ views
+ } else {
+ "$views • $date"
+ }
+ }
+}
+
+internal class StreamItemPreviewProvider : PreviewParameterProvider {
+ override val values = sequenceOf(
+ StreamInfoItem(streamType = StreamType.NONE),
+ StreamInfoItem(streamType = StreamType.LIVE_STREAM),
+ StreamInfoItem(streamType = StreamType.AUDIO_LIVE_STREAM),
+ )
+}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt
new file mode 100644
index 00000000000..d92d7e74a81
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt
@@ -0,0 +1,92 @@
+package org.schabi.newpipe.ui.components.video
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.content.edit
+import androidx.preference.PreferenceManager
+import org.schabi.newpipe.R
+import org.schabi.newpipe.extractor.stream.StreamInfo
+import org.schabi.newpipe.extractor.stream.StreamType
+import org.schabi.newpipe.ui.components.items.ItemList
+import org.schabi.newpipe.ui.components.items.stream.StreamInfoItem
+import org.schabi.newpipe.ui.theme.AppTheme
+import org.schabi.newpipe.util.NO_SERVICE_ID
+
+@Composable
+fun RelatedItems(info: StreamInfo) {
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(LocalContext.current)
+ val key = stringResource(R.string.auto_queue_key)
+ // TODO: AndroidX DataStore might be a better option.
+ var isAutoQueueEnabled by rememberSaveable {
+ mutableStateOf(sharedPreferences.getBoolean(key, false))
+ }
+
+ ItemList(
+ items = info.relatedItems,
+ listHeader = {
+ item {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 12.dp, end = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(text = stringResource(R.string.auto_queue_description))
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(text = stringResource(R.string.auto_queue_toggle))
+ Switch(
+ checked = isAutoQueueEnabled,
+ onCheckedChange = {
+ isAutoQueueEnabled = it
+ sharedPreferences.edit {
+ putBoolean(key, it)
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ )
+}
+
+@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
+@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun RelatedItemsPreview() {
+ val info = StreamInfo(NO_SERVICE_ID, "", "", StreamType.VIDEO_STREAM, "", "", 0)
+ info.relatedItems = listOf(
+ StreamInfoItem(streamType = StreamType.NONE),
+ StreamInfoItem(streamType = StreamType.LIVE_STREAM),
+ StreamInfoItem(streamType = StreamType.AUDIO_LIVE_STREAM),
+ )
+
+ AppTheme {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ RelatedItems(info)
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.kt b/app/src/main/java/org/schabi/newpipe/util/Constants.kt
index 054aadd7078..2160272912a 100644
--- a/app/src/main/java/org/schabi/newpipe/util/Constants.kt
+++ b/app/src/main/java/org/schabi/newpipe/util/Constants.kt
@@ -9,6 +9,7 @@ const val DEFAULT_THROTTLE_TIMEOUT = 120L
const val KEY_SERVICE_ID = "key_service_id"
const val KEY_URL = "key_url"
+const val KEY_INFO = "info"
const val KEY_TITLE = "key_title"
const val KEY_LINK_TYPE = "key_link_type"
const val KEY_OPEN_SEARCH = "key_open_search"
diff --git a/app/src/main/res/layout/fragment_related_items.xml b/app/src/main/res/layout/fragment_related_items.xml
deleted file mode 100644
index 3591cdfd196..00000000000
--- a/app/src/main/res/layout/fragment_related_items.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/related_items_header.xml b/app/src/main/res/layout/related_items_header.xml
deleted file mode 100644
index b50a8484894..00000000000
--- a/app/src/main/res/layout/related_items_header.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bff35e5d9ea..938a2497d00 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -855,4 +855,5 @@
Show more
Show less
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.
+ Next
diff --git a/build.gradle b/build.gradle
index 6d19a6f8a84..1acfb6f4a2f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.9.10'
+ ext.kotlin_version = '2.0.0'
repositories {
google()
mavenCentral()
From bd304eee379036d4d2d7447b85139bbe534bec48 Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Thu, 1 Aug 2024 09:15:19 +0530
Subject: [PATCH 03/15] Specify mode parameter explicitly
---
.../java/org/schabi/newpipe/ui/components/video/RelatedItems.kt | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt
index d92d7e74a81..dd42fcafa51 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt
@@ -25,6 +25,7 @@ import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamType
+import org.schabi.newpipe.info_list.ItemViewMode
import org.schabi.newpipe.ui.components.items.ItemList
import org.schabi.newpipe.ui.components.items.stream.StreamInfoItem
import org.schabi.newpipe.ui.theme.AppTheme
@@ -41,6 +42,7 @@ fun RelatedItems(info: StreamInfo) {
ItemList(
items = info.relatedItems,
+ mode = ItemViewMode.LIST,
listHeader = {
item {
Row(
From dce84ea83f0e2352c4c5eef0ca23abd2c7c1f9e2 Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Thu, 1 Aug 2024 09:16:12 +0530
Subject: [PATCH 04/15] Rm unused class
---
.../list/videos/RelatedItemsInfo.java | 22 -------------------
1 file changed, 22 deletions(-)
delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsInfo.java
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsInfo.java b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsInfo.java
deleted file mode 100644
index bbc7e1ed001..00000000000
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsInfo.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.schabi.newpipe.fragments.list.videos;
-
-import org.schabi.newpipe.extractor.InfoItem;
-import org.schabi.newpipe.extractor.ListInfo;
-import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
-import org.schabi.newpipe.extractor.stream.StreamInfo;
-
-import java.util.ArrayList;
-import java.util.Collections;
-
-public final class RelatedItemsInfo extends ListInfo {
- /**
- * This class is used to wrap the related items of a StreamInfo into a ListInfo object.
- *
- * @param info the stream info from which to get related items
- */
- public RelatedItemsInfo(final StreamInfo info) {
- super(info.getServiceId(), new ListLinkHandler(info.getOriginalUrl(), info.getUrl(),
- info.getId(), Collections.emptyList(), null), info.getName());
- setRelatedItems(new ArrayList<>(info.getRelatedItems()));
- }
-}
From 994688b6ef6a426036d88febb5750847296bface Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Fri, 2 Aug 2024 06:10:38 +0530
Subject: [PATCH 05/15] Fix list item size
---
.../org/schabi/newpipe/ui/components/items/ItemThumbnail.kt | 2 +-
.../newpipe/ui/components/items/playlist/PlaylistListItem.kt | 4 ++--
.../newpipe/ui/components/items/stream/StreamListItem.kt | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt
index 58b39955150..7c395ab223c 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt
@@ -33,7 +33,7 @@ fun ItemThumbnail(
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit
) {
- Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) {
+ Box(contentAlignment = Alignment.BottomEnd) {
AsyncImage(
model = ImageStrategy.choosePreferredImage(item.thumbnails),
contentDescription = null,
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt
index 0cc8e68dd0f..9bfa793f110 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt
@@ -38,7 +38,7 @@ fun PlaylistListItem(
) {
ItemThumbnail(
item = playlist,
- modifier = Modifier.size(width = 98.dp, height = 55.dp)
+ modifier = Modifier.size(width = 140.dp, height = 78.dp)
)
Column {
@@ -46,7 +46,7 @@ fun PlaylistListItem(
text = playlist.name,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleSmall,
- maxLines = 1
+ maxLines = 2
)
Text(
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
index 12cb1245494..39ad15a482c 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
@@ -47,7 +47,7 @@ fun StreamListItem(
) {
ItemThumbnail(
item = stream,
- modifier = Modifier.size(width = 98.dp, height = 55.dp)
+ modifier = Modifier.size(width = 140.dp, height = 78.dp)
)
Column {
@@ -55,7 +55,7 @@ fun StreamListItem(
text = stream.name,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleSmall,
- maxLines = 1
+ maxLines = 2
)
Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall)
From 34fd5da584187973bbbcc4ed76ee4003f815e63b Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Fri, 2 Aug 2024 08:56:26 +0530
Subject: [PATCH 06/15] Added stream progress bar, separate stream and playlist
thumbnails
---
app/build.gradle | 3 +
.../newpipe/database/stream/dao/StreamDAO.kt | 3 +-
.../database/stream/dao/StreamStateDAO.java | 9 +-
.../holder/StreamMiniInfoItemHolder.java | 5 +-
.../local/history/HistoryRecordManager.java | 40 ++-------
.../newpipe/ui/components/items/ItemList.kt | 7 +-
.../ui/components/items/ItemThumbnail.kt | 83 -----------------
.../items/playlist/PlaylistListItem.kt | 5 +-
.../items/playlist/PlaylistThumbnail.kt | 65 ++++++++++++++
.../components/items/stream/StreamListItem.kt | 11 +--
.../items/stream/StreamThumbnail.kt | 89 +++++++++++++++++++
.../newpipe/viewmodels/StreamViewModel.kt | 16 ++++
12 files changed, 205 insertions(+), 131 deletions(-)
delete mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt
create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt
create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt
create mode 100644 app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt
diff --git a/app/build.gradle b/app/build.gradle
index c3699669861..e6d01798c42 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -296,6 +296,9 @@ dependencies {
implementation 'androidx.paging:paging-compose:3.3.1'
implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0'
+ // Coroutines interop
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.1'
+
/** Debugging **/
// Memory leak detection
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt
index d8c19c1e979..0015c8e0aaa 100644
--- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt
+++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt
@@ -8,6 +8,7 @@ import androidx.room.Query
import androidx.room.Transaction
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
+import io.reactivex.rxjava3.core.Maybe
import org.schabi.newpipe.database.BasicDAO
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
@@ -27,7 +28,7 @@ abstract class StreamDAO : BasicDAO {
abstract override fun listByService(serviceId: Int): Flowable>
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
- abstract fun getStream(serviceId: Long, url: String): Flowable>
+ abstract fun getStream(serviceId: Long, url: String): Maybe
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java
index 06371248d62..6f1ecf173d8 100644
--- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java
+++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java
@@ -1,5 +1,8 @@
package org.schabi.newpipe.database.stream.dao;
+import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
+import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
+
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
@@ -12,9 +15,7 @@
import java.util.List;
import io.reactivex.rxjava3.core.Flowable;
-
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
-import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
+import io.reactivex.rxjava3.core.Maybe;
@Dao
public interface StreamStateDAO extends BasicDAO {
@@ -32,7 +33,7 @@ default Flowable> listByService(final int serviceId) {
}
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
- Flowable> getState(long streamId);
+ Maybe getState(long streamId);
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
int deleteState(long streamId);
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
index 32fa8bf608b..642738630c3 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
@@ -64,8 +64,7 @@ public void updateFromItem(final InfoItem infoItem,
StreamStateEntity state2 = null;
if (DependentPreferenceHelper
.getPositionsInListsEnabled(itemProgressView.getContext())) {
- state2 = historyRecordManager.loadStreamState(infoItem)
- .blockingGet()[0];
+ state2 = historyRecordManager.loadStreamState(infoItem).blockingGet();
}
if (state2 != null) {
itemProgressView.setVisibility(View.VISIBLE);
@@ -120,7 +119,7 @@ public void updateState(final InfoItem infoItem,
if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext())) {
state = historyRecordManager
.loadStreamState(infoItem)
- .blockingGet()[0];
+ .blockingGet();
}
if (state != null && item.getDuration() > 0
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
index ed3cf548f96..0dcf007bbe9 100644
--- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
+++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
@@ -221,7 +221,7 @@ public Single deleteCompleteSearchHistory() {
public Flowable> getRelatedSearches(final String query,
final int similarQueryLimit,
final int uniqueQueryLimit) {
- return query.length() > 0
+ return !query.isEmpty()
? searchHistoryTable.getSimilarEntries(query, similarQueryLimit)
: searchHistoryTable.getUniqueEntries(uniqueQueryLimit);
}
@@ -236,47 +236,31 @@ private boolean isSearchHistoryEnabled() {
public Maybe loadStreamState(final PlayQueueItem queueItem) {
return queueItem.getStream()
- .map(info -> streamTable.upsert(new StreamEntity(info)))
- .flatMapPublisher(streamStateTable::getState)
- .firstElement()
- .flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
+ .flatMapMaybe(this::loadStreamState)
.filter(state -> state.isValid(queueItem.getDuration()))
.subscribeOn(Schedulers.io());
}
public Maybe loadStreamState(final StreamInfo info) {
return Single.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
- .flatMapPublisher(streamStateTable::getState)
- .firstElement()
- .flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
- .filter(state -> state.isValid(info.getDuration()))
+ .flatMapMaybe(streamStateTable::getState)
.subscribeOn(Schedulers.io());
}
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressMillis) {
return Completable.fromAction(() -> database.runInTransaction(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info));
- final StreamStateEntity state = new StreamStateEntity(streamId, progressMillis);
+ final var state = new StreamStateEntity(streamId, progressMillis);
if (state.isValid(info.getDuration())) {
streamStateTable.upsert(state);
}
})).subscribeOn(Schedulers.io());
}
- public Single loadStreamState(final InfoItem info) {
- return Single.fromCallable(() -> {
- final List entities = streamTable
- .getStream(info.getServiceId(), info.getUrl()).blockingFirst();
- if (entities.isEmpty()) {
- return new StreamStateEntity[]{null};
- }
- final List states = streamStateTable
- .getState(entities.get(0).getUid()).blockingFirst();
- if (states.isEmpty()) {
- return new StreamStateEntity[]{null};
- }
- return new StreamStateEntity[]{states.get(0)};
- }).subscribeOn(Schedulers.io());
+ public Maybe loadStreamState(final InfoItem info) {
+ return streamTable.getStream(info.getServiceId(), info.getUrl())
+ .flatMap(entity -> streamStateTable.getState(entity.getUid()))
+ .subscribeOn(Schedulers.io());
}
public Single> loadLocalStreamStateBatch(
@@ -295,13 +279,7 @@ public Single> loadLocalStreamStateBatch(
result.add(null);
continue;
}
- final List states = streamStateTable.getState(streamId)
- .blockingFirst();
- if (states.isEmpty()) {
- result.add(null);
- } else {
- result.add(states.get(0));
- }
+ result.add(streamStateTable.getState(streamId).blockingGet());
}
return result;
}).subscribeOn(Schedulers.io());
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
index 5bcb79d51f7..42696bc2bd9 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
@@ -22,6 +22,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.info_list.ItemViewMode
import org.schabi.newpipe.ui.components.items.playlist.PlaylistListItem
import org.schabi.newpipe.ui.components.items.stream.StreamListItem
+import org.schabi.newpipe.util.DependentPreferenceHelper
import org.schabi.newpipe.util.NavigationHelper
@Composable
@@ -60,6 +61,8 @@ fun ItemList(
}
}
+ val showProgress = DependentPreferenceHelper.getPositionsInListsEnabled(context)
+
if (mode == ItemViewMode.GRID) {
// TODO: Implement grid layout using LazyVerticalGrid and LazyVerticalGridScrollbar.
} else {
@@ -76,7 +79,9 @@ fun ItemList(
// TODO: Implement card layouts.
if (item is StreamInfoItem) {
val isSelected = selectedStream == item
- StreamListItem(item, isSelected, onClick, onLongClick, onDismissPopup)
+ StreamListItem(
+ item, showProgress, isSelected, onClick, onLongClick, onDismissPopup
+ )
} else if (item is PlaylistInfoItem) {
PlaylistListItem(item, onClick)
}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt
deleted file mode 100644
index 7c395ab223c..00000000000
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemThumbnail.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-package org.schabi.newpipe.ui.components.items
-
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-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.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import coil.compose.AsyncImage
-import org.schabi.newpipe.R
-import org.schabi.newpipe.extractor.InfoItem
-import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
-import org.schabi.newpipe.extractor.stream.StreamInfoItem
-import org.schabi.newpipe.util.Localization
-import org.schabi.newpipe.util.StreamTypeUtil
-import org.schabi.newpipe.util.image.ImageStrategy
-
-@Composable
-fun ItemThumbnail(
- item: InfoItem,
- modifier: Modifier = Modifier,
- contentScale: ContentScale = ContentScale.Fit
-) {
- Box(contentAlignment = Alignment.BottomEnd) {
- AsyncImage(
- model = ImageStrategy.choosePreferredImage(item.thumbnails),
- contentDescription = null,
- placeholder = painterResource(R.drawable.placeholder_thumbnail_video),
- error = painterResource(R.drawable.placeholder_thumbnail_video),
- contentScale = contentScale,
- modifier = modifier
- )
-
- val isLive = item is StreamInfoItem && StreamTypeUtil.isLiveStream(item.streamType)
- val background = if (isLive) Color.Red else Color.Black
- val nestedModifier = Modifier
- .padding(2.dp)
- .background(background.copy(alpha = 0.5f))
- .padding(2.dp)
-
- if (item is StreamInfoItem) {
- Text(
- text = if (isLive) {
- stringResource(R.string.duration_live)
- } else {
- Localization.getDurationString(item.duration)
- },
- color = Color.White,
- style = MaterialTheme.typography.bodySmall,
- modifier = nestedModifier
- )
- } else if (item is PlaylistInfoItem) {
- Row(modifier = nestedModifier, verticalAlignment = Alignment.CenterVertically) {
- Image(
- painter = painterResource(R.drawable.ic_playlist_play),
- contentDescription = null,
- colorFilter = ColorFilter.tint(Color.White),
- modifier = Modifier.size(18.dp)
- )
-
- val context = LocalContext.current
- Text(
- text = Localization.localizeStreamCountMini(context, item.streamCount),
- color = Color.White,
- style = MaterialTheme.typography.bodySmall,
- modifier = Modifier.padding(start = 4.dp)
- )
- }
- }
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt
index 9bfa793f110..f282f9030c0 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt
@@ -19,7 +19,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.schabi.newpipe.extractor.InfoItem
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
-import org.schabi.newpipe.ui.components.items.ItemThumbnail
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.util.NO_SERVICE_ID
@@ -36,8 +35,8 @@ fun PlaylistListItem(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
- ItemThumbnail(
- item = playlist,
+ PlaylistThumbnail(
+ playlist = playlist,
modifier = Modifier.size(width = 140.dp, height = 78.dp)
)
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt
new file mode 100644
index 00000000000..3fc13911924
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt
@@ -0,0 +1,65 @@
+package org.schabi.newpipe.ui.components.items.playlist
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+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.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import org.schabi.newpipe.R
+import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
+import org.schabi.newpipe.util.Localization
+import org.schabi.newpipe.util.image.ImageStrategy
+
+@Composable
+fun PlaylistThumbnail(
+ playlist: PlaylistInfoItem,
+ modifier: Modifier = Modifier,
+ contentScale: ContentScale = ContentScale.Fit
+) {
+ Box(contentAlignment = Alignment.BottomEnd) {
+ AsyncImage(
+ model = ImageStrategy.choosePreferredImage(playlist.thumbnails),
+ contentDescription = null,
+ placeholder = painterResource(R.drawable.placeholder_thumbnail_playlist),
+ error = painterResource(R.drawable.placeholder_thumbnail_playlist),
+ contentScale = contentScale,
+ modifier = modifier
+ )
+
+ Row(
+ modifier = Modifier
+ .padding(2.dp)
+ .background(Color.Black.copy(alpha = 0.5f))
+ .padding(2.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(R.drawable.ic_playlist_play),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(Color.White),
+ modifier = Modifier.size(18.dp)
+ )
+
+ val context = LocalContext.current
+ Text(
+ text = Localization.localizeStreamCountMini(context, playlist.streamCount),
+ color = Color.White,
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.padding(start = 4.dp)
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
index 39ad15a482c..4dc132c520d 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
@@ -21,14 +21,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import org.schabi.newpipe.extractor.stream.StreamInfoItem
-import org.schabi.newpipe.ui.components.items.ItemThumbnail
import org.schabi.newpipe.ui.theme.AppTheme
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun StreamListItem(
stream: StreamInfoItem,
- isSelected: Boolean = false,
+ showProgress: Boolean,
+ isSelected: Boolean,
onClick: (StreamInfoItem) -> Unit = {},
onLongClick: (StreamInfoItem) -> Unit = {},
onDismissPopup: () -> Unit = {}
@@ -45,8 +45,9 @@ fun StreamListItem(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
- ItemThumbnail(
- item = stream,
+ StreamThumbnail(
+ stream = stream,
+ showProgress = showProgress,
modifier = Modifier.size(width = 140.dp, height = 78.dp)
)
@@ -81,7 +82,7 @@ private fun StreamListItemPreview(
) {
AppTheme {
Surface(color = MaterialTheme.colorScheme.background) {
- StreamListItem(stream)
+ StreamListItem(stream, showProgress = false, isSelected = false)
}
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt
new file mode 100644
index 00000000000..b81b6a71e8a
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt
@@ -0,0 +1,89 @@
+package org.schabi.newpipe.ui.components.items.stream
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import org.schabi.newpipe.R
+import org.schabi.newpipe.extractor.stream.StreamInfoItem
+import org.schabi.newpipe.util.Localization
+import org.schabi.newpipe.util.StreamTypeUtil
+import org.schabi.newpipe.util.image.ImageStrategy
+import org.schabi.newpipe.viewmodels.StreamViewModel
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+
+@Composable
+fun StreamThumbnail(
+ stream: StreamInfoItem,
+ showProgress: Boolean,
+ modifier: Modifier = Modifier,
+ contentScale: ContentScale = ContentScale.Fit
+) {
+ Column(modifier = modifier) {
+ Box(contentAlignment = Alignment.BottomEnd) {
+ AsyncImage(
+ model = ImageStrategy.choosePreferredImage(stream.thumbnails),
+ contentDescription = null,
+ placeholder = painterResource(R.drawable.placeholder_thumbnail_video),
+ error = painterResource(R.drawable.placeholder_thumbnail_video),
+ contentScale = contentScale,
+ modifier = modifier
+ )
+
+ val isLive = StreamTypeUtil.isLiveStream(stream.streamType)
+ Text(
+ modifier = Modifier
+ .padding(2.dp)
+ .background((if (isLive) Color.Red else Color.Black).copy(alpha = 0.5f))
+ .padding(2.dp),
+ text = if (isLive) {
+ stringResource(R.string.duration_live)
+ } else {
+ Localization.getDurationString(stream.duration)
+ },
+ color = Color.White,
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+
+ if (showProgress) {
+ val streamViewModel = viewModel()
+ var progress by rememberSaveable { mutableLongStateOf(0L) }
+
+ LaunchedEffect(stream) {
+ progress = streamViewModel.getStreamState(stream)?.progressMillis ?: 0L
+ }
+
+ if (progress != 0L) {
+ LinearProgressIndicator(
+ modifier = Modifier.requiredHeight(2.dp),
+ progress = {
+ (progress.milliseconds / stream.duration.seconds).toFloat()
+ },
+ gapSize = 0.dp,
+ drawStopIndicator = {} // Hide stop indicator
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt
new file mode 100644
index 00000000000..a1402329044
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt
@@ -0,0 +1,16 @@
+package org.schabi.newpipe.viewmodels
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import kotlinx.coroutines.rx3.awaitSingleOrNull
+import org.schabi.newpipe.database.stream.model.StreamStateEntity
+import org.schabi.newpipe.extractor.InfoItem
+import org.schabi.newpipe.local.history.HistoryRecordManager
+
+class StreamViewModel(application: Application) : AndroidViewModel(application) {
+ private val historyRecordManager = HistoryRecordManager(application)
+
+ suspend fun getStreamState(infoItem: InfoItem): StreamStateEntity? {
+ return historyRecordManager.loadStreamState(infoItem).awaitSingleOrNull()
+ }
+}
From a2679531f4a640205e20d0dfe40fbb904d21cf79 Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Fri, 2 Aug 2024 09:11:10 +0530
Subject: [PATCH 07/15] Display message if no related streams are available
---
.../ui/components/common/NoItemsMessage.kt | 34 +++++++++++
.../ui/components/video/RelatedItems.kt | 59 ++++++++++---------
2 files changed, 66 insertions(+), 27 deletions(-)
create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/common/NoItemsMessage.kt
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/NoItemsMessage.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/NoItemsMessage.kt
new file mode 100644
index 00000000000..7f217b57d83
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/NoItemsMessage.kt
@@ -0,0 +1,34 @@
+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.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.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(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(text = "(╯°-°)╯", fontSize = 35.sp)
+ Text(text = stringResource(id = message), fontSize = 24.sp)
+ }
+}
+
+@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
+@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun NoItemsMessagePreview() {
+ AppTheme {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ NoItemsMessage(message = R.string.no_videos)
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt
index dd42fcafa51..6eee01bc031 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt
@@ -26,6 +26,7 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.info_list.ItemViewMode
+import org.schabi.newpipe.ui.components.common.NoItemsMessage
import org.schabi.newpipe.ui.components.items.ItemList
import org.schabi.newpipe.ui.components.items.stream.StreamInfoItem
import org.schabi.newpipe.ui.theme.AppTheme
@@ -40,39 +41,43 @@ fun RelatedItems(info: StreamInfo) {
mutableStateOf(sharedPreferences.getBoolean(key, false))
}
- ItemList(
- items = info.relatedItems,
- mode = ItemViewMode.LIST,
- listHeader = {
- item {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(start = 12.dp, end = 12.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(text = stringResource(R.string.auto_queue_description))
-
+ if (info.relatedItems.isEmpty()) {
+ NoItemsMessage(message = R.string.no_videos)
+ } else {
+ ItemList(
+ items = info.relatedItems,
+ mode = ItemViewMode.LIST,
+ listHeader = {
+ item {
Row(
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- verticalAlignment = Alignment.CenterVertically
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 12.dp, end = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
) {
- Text(text = stringResource(R.string.auto_queue_toggle))
- Switch(
- checked = isAutoQueueEnabled,
- onCheckedChange = {
- isAutoQueueEnabled = it
- sharedPreferences.edit {
- putBoolean(key, it)
+ Text(text = stringResource(R.string.auto_queue_description))
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(text = stringResource(R.string.auto_queue_toggle))
+ Switch(
+ checked = isAutoQueueEnabled,
+ onCheckedChange = {
+ isAutoQueueEnabled = it
+ sharedPreferences.edit {
+ putBoolean(key, it)
+ }
}
- }
- )
+ )
+ }
}
}
}
- }
- )
+ )
+ }
}
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
From 12d614dee0efed52ef27848fec2320448d25f2a4 Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Fri, 2 Aug 2024 15:20:52 +0530
Subject: [PATCH 08/15] Dispose of related items when closing the video player
---
.../newpipe/fragments/list/videos/RelatedItemsFragment.kt | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.kt
index 503179cc103..b48347f4fb7 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedItemsFragment.kt
@@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import org.schabi.newpipe.extractor.stream.StreamInfo
@@ -22,6 +23,7 @@ class RelatedItemsFragment : Fragment() {
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
AppTheme {
Surface(color = MaterialTheme.colorScheme.background) {
From 94450aba6bb5846306f39fc02ba2cd01126f2bdc Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Fri, 2 Aug 2024 15:58:31 +0530
Subject: [PATCH 09/15] Add modifiers for no items message function
---
.../newpipe/ui/components/common/NoItemsMessage.kt | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/NoItemsMessage.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/NoItemsMessage.kt
index 7f217b57d83..afb53fdf422 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/common/NoItemsMessage.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/NoItemsMessage.kt
@@ -3,11 +3,14 @@ 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
@@ -16,7 +19,12 @@ import org.schabi.newpipe.ui.theme.AppTheme
@Composable
fun NoItemsMessage(@StringRes message: Int) {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentSize(Alignment.Center),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
Text(text = "(╯°-°)╯", fontSize = 35.sp)
Text(text = stringResource(id = message), fontSize = 24.sp)
}
From 6b14811cf5e92e3fa72e873f84bd898ab793bf00 Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Sat, 3 Aug 2024 05:49:30 +0530
Subject: [PATCH 10/15] Implement remaining stream menu items
---
.../dialog/StreamDialogDefaultEntry.java | 9 +-
.../local/history/HistoryRecordManager.java | 70 +++++-----
.../ui/components/items/stream/StreamMenu.kt | 129 ++++++++++++++----
.../schabi/newpipe/util/NavigationHelper.java | 7 +-
.../newpipe/viewmodels/StreamViewModel.kt | 6 +
5 files changed, 151 insertions(+), 70 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java
index 948a8274cd1..7ed5a20371f 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java
@@ -41,10 +41,11 @@
*
*/
public enum StreamDialogDefaultEntry {
- SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) ->
- fetchUploaderUrlIfSparse(fragment.requireContext(), item.getServiceId(), item.getUrl(),
- item.getUploaderUrl(), url -> openChannelFragment(fragment, item, url))
- ),
+ SHOW_CHANNEL_DETAILS(R.string.show_channel_details, (fragment, item) -> {
+ final var activity = fragment.requireActivity();
+ fetchUploaderUrlIfSparse(activity, item.getServiceId(), item.getUrl(),
+ item.getUploaderUrl(), url -> openChannelFragment(activity, item, url));
+ }),
/**
* Enqueues the stream automatically to the current PlayerType.
diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
index 0dcf007bbe9..f2fdf9eba63 100644
--- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
+++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
@@ -18,10 +18,13 @@
* along with NewPipe. If not, see .
*/
+import static org.schabi.newpipe.util.ExtractorHelper.getStreamInfo;
+
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
+import androidx.collection.LongLongPair;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.NewPipeDatabase;
@@ -45,7 +48,6 @@
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.local.feed.FeedViewModel;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
-import org.schabi.newpipe.util.ExtractorHelper;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@@ -91,47 +93,39 @@ public HistoryRecordManager(final Context context) {
* @param info the item to mark as watched
* @return a Maybe containing the ID of the item if successful
*/
- public Maybe markAsWatched(final StreamInfoItem info) {
+ public Completable markAsWatched(final StreamInfoItem info) {
if (!isStreamHistoryEnabled()) {
- return Maybe.empty();
+ return Completable.complete();
}
- final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC);
- return Maybe.fromCallable(() -> database.runInTransaction(() -> {
- final long streamId;
- final long duration;
- // Duration will not exist if the item was loaded with fast mode, so fetch it if empty
- if (info.getDuration() < 0) {
- final StreamInfo completeInfo = ExtractorHelper.getStreamInfo(
- info.getServiceId(),
- info.getUrl(),
- false
- )
- .subscribeOn(Schedulers.io())
- .blockingGet();
- duration = completeInfo.getDuration();
- streamId = streamTable.upsert(new StreamEntity(completeInfo));
- } else {
- duration = info.getDuration();
- streamId = streamTable.upsert(new StreamEntity(info));
- }
-
- // Update the stream progress to the full duration of the video
- final StreamStateEntity entity = new StreamStateEntity(
- streamId,
- duration * 1000
- );
- streamStateTable.upsert(entity);
+ final var remoteInfo = getStreamInfo(info.getServiceId(), info.getUrl(), false)
+ .map(item ->
+ new LongLongPair(item.getDuration(), streamTable.upsert(new StreamEntity(item))));
- // Add a history entry
- final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
- if (latestEntry == null) {
- // never actually viewed: add history entry but with 0 views
- return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime, 0));
- } else {
- return 0L;
- }
- })).subscribeOn(Schedulers.io());
+ return Single.just(info)
+ .filter(item -> item.getDuration() >= 0)
+ .map(item ->
+ new LongLongPair(item.getDuration(), streamTable.upsert(new StreamEntity(item)))
+ )
+ .switchIfEmpty(remoteInfo)
+ .flatMapCompletable(pair -> Completable.fromRunnable(() -> {
+ final long duration = pair.getFirst();
+ final long streamId = pair.getSecond();
+
+ // Update the stream progress to the full duration of the video
+ final var entity = new StreamStateEntity(streamId, duration * 1000);
+ streamStateTable.upsert(entity);
+
+ // Add a history entry
+ final var latestEntry = streamHistoryTable.getLatestEntry(streamId);
+ if (latestEntry == null) {
+ final var currentTime = OffsetDateTime.now(ZoneOffset.UTC);
+ // never actually viewed: add history entry but with 0 views
+ final var entry = new StreamHistoryEntity(streamId, currentTime, 0);
+ streamHistoryTable.insert(entry);
+ }
+ }))
+ .subscribeOn(Schedulers.io());
}
public Maybe onViewed(final StreamInfo info) {
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
index a2c84647d60..7705ea710c7 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
@@ -1,18 +1,28 @@
package org.schabi.newpipe.ui.components.items.stream
+import androidx.annotation.StringRes
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.viewmodel.compose.viewModel
+import kotlinx.coroutines.launch
import org.schabi.newpipe.R
+import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.download.DownloadDialog
-import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
+import org.schabi.newpipe.local.dialog.PlaylistAppendDialog
+import org.schabi.newpipe.local.dialog.PlaylistDialog
+import org.schabi.newpipe.player.helper.PlayerHolder
+import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.SparseItemUtil
import org.schabi.newpipe.util.external_communication.ShareUtils
+import org.schabi.newpipe.viewmodels.StreamViewModel
@Composable
fun StreamMenu(
@@ -20,24 +30,60 @@ fun StreamMenu(
onDismissRequest: () -> Unit
) {
val context = LocalContext.current
+ val coroutineScope = rememberCoroutineScope()
+ val streamViewModel = viewModel()
+ val playerHolder = PlayerHolder.getInstance()
- // TODO: Implement remaining click actions
DropdownMenu(expanded = true, onDismissRequest = onDismissRequest) {
- DropdownMenuItem(
- text = { Text(text = stringResource(R.string.start_here_on_background)) },
- onClick = onDismissRequest
+ if (playerHolder.isPlayQueueReady) {
+ StreamMenuItem(
+ text = R.string.enqueue_stream,
+ onClick = {
+ onDismissRequest()
+ SparseItemUtil.fetchItemInfoIfSparse(context, stream) {
+ NavigationHelper.enqueueOnPlayer(context, it)
+ }
+ }
+ )
+
+ if (playerHolder.queuePosition < playerHolder.queueSize - 1) {
+ StreamMenuItem(
+ text = R.string.enqueue_next_stream,
+ onClick = {
+ onDismissRequest()
+ SparseItemUtil.fetchItemInfoIfSparse(context, stream) {
+ NavigationHelper.enqueueNextOnPlayer(context, it)
+ }
+ }
+ )
+ }
+ }
+
+ StreamMenuItem(
+ text = R.string.start_here_on_background,
+ onClick = {
+ onDismissRequest()
+ SparseItemUtil.fetchItemInfoIfSparse(context, stream) {
+ NavigationHelper.playOnBackgroundPlayer(context, it, true)
+ }
+ }
)
- DropdownMenuItem(
- text = { Text(text = stringResource(R.string.start_here_on_popup)) },
- onClick = onDismissRequest
+ StreamMenuItem(
+ text = R.string.start_here_on_popup,
+ onClick = {
+ onDismissRequest()
+ SparseItemUtil.fetchItemInfoIfSparse(context, stream) {
+ NavigationHelper.playOnPopupPlayer(context, it, true)
+ }
+ }
)
- DropdownMenuItem(
- text = { Text(text = stringResource(R.string.download)) },
+ StreamMenuItem(
+ text = R.string.download,
onClick = {
onDismissRequest()
SparseItemUtil.fetchStreamInfoAndSaveToDatabase(
context, stream.serviceId, stream.url
- ) { info: StreamInfo ->
+ ) { info ->
// TODO: Use an AlertDialog composable instead.
val downloadDialog = DownloadDialog(context, info)
val fragmentManager = (context as FragmentActivity).supportFragmentManager
@@ -45,31 +91,66 @@ fun StreamMenu(
}
}
)
- DropdownMenuItem(
- text = { Text(text = stringResource(R.string.add_to_playlist)) },
- onClick = onDismissRequest
+ StreamMenuItem(
+ text = R.string.add_to_playlist,
+ onClick = {
+ onDismissRequest()
+ val list = listOf(StreamEntity(stream))
+ PlaylistDialog.createCorrespondingDialog(context, list) { dialog ->
+ val tag = if (dialog is PlaylistAppendDialog) "append" else "create"
+ dialog.show(
+ (context as FragmentActivity).supportFragmentManager,
+ "StreamDialogEntry@${tag}_playlist"
+ )
+ }
+ }
)
- DropdownMenuItem(
- text = { Text(text = stringResource(R.string.share)) },
+ StreamMenuItem(
+ text = R.string.share,
onClick = {
onDismissRequest()
ShareUtils.shareText(context, stream.name, stream.url, stream.thumbnails)
}
)
- DropdownMenuItem(
- text = { Text(text = stringResource(R.string.open_in_browser)) },
+ StreamMenuItem(
+ text = R.string.open_in_browser,
onClick = {
onDismissRequest()
ShareUtils.openUrlInBrowser(context, stream.url)
}
)
- DropdownMenuItem(
- text = { Text(text = stringResource(R.string.mark_as_watched)) },
- onClick = onDismissRequest
+ StreamMenuItem(
+ text = R.string.mark_as_watched,
+ onClick = {
+ onDismissRequest()
+ coroutineScope.launch {
+ streamViewModel.markAsWatched(stream)
+ }
+ }
)
- DropdownMenuItem(
- text = { Text(text = stringResource(R.string.show_channel_details)) },
- onClick = onDismissRequest
+ StreamMenuItem(
+ text = R.string.show_channel_details,
+ onClick = {
+ onDismissRequest()
+ SparseItemUtil.fetchUploaderUrlIfSparse(
+ context, stream.serviceId, stream.url, stream.uploaderUrl
+ ) { url ->
+ NavigationHelper.openChannelFragment(context as FragmentActivity, stream, url)
+ }
+ }
)
}
}
+
+@Composable
+private fun StreamMenuItem(
+ @StringRes text: Int,
+ onClick: () -> Unit
+) {
+ DropdownMenuItem(
+ text = {
+ Text(text = stringResource(text), color = MaterialTheme.colorScheme.onBackground)
+ },
+ onClick = onClick
+ )
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
index 5dee32371b5..534d7085bb7 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -472,13 +472,12 @@ public static void openChannelFragment(final FragmentManager fragmentManager,
.commit();
}
- public static void openChannelFragment(@NonNull final Fragment fragment,
+ public static void openChannelFragment(@NonNull final FragmentActivity activity,
@NonNull final StreamInfoItem item,
final String uploaderUrl) {
// For some reason `getParentFragmentManager()` doesn't work, but this does.
- openChannelFragment(
- fragment.requireActivity().getSupportFragmentManager(),
- item.getServiceId(), uploaderUrl, item.getUploaderName());
+ openChannelFragment(activity.getSupportFragmentManager(), item.getServiceId(), uploaderUrl,
+ item.getUploaderName());
}
/**
diff --git a/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt
index a1402329044..1fae34d1b38 100644
--- a/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt
+++ b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt
@@ -2,9 +2,11 @@ package org.schabi.newpipe.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
+import kotlinx.coroutines.rx3.await
import kotlinx.coroutines.rx3.awaitSingleOrNull
import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.extractor.InfoItem
+import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.history.HistoryRecordManager
class StreamViewModel(application: Application) : AndroidViewModel(application) {
@@ -13,4 +15,8 @@ class StreamViewModel(application: Application) : AndroidViewModel(application)
suspend fun getStreamState(infoItem: InfoItem): StreamStateEntity? {
return historyRecordManager.loadStreamState(infoItem).awaitSingleOrNull()
}
+
+ suspend fun markAsWatched(stream: StreamInfoItem) {
+ historyRecordManager.markAsWatched(stream).await()
+ }
}
From fac92cd6285e74ad7df52942dd4b075fe1c5d3f4 Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Sat, 3 Aug 2024 18:50:32 +0530
Subject: [PATCH 11/15] Improved stream composables
---
.../components/items/stream/StreamListItem.kt | 19 ++++++++-----------
.../ui/components/items/stream/StreamMenu.kt | 3 ++-
2 files changed, 10 insertions(+), 12 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
index 4dc132c520d..ee6bde28d88 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt
@@ -33,15 +33,14 @@ fun StreamListItem(
onLongClick: (StreamInfoItem) -> Unit = {},
onDismissPopup: () -> Unit = {}
) {
- Box {
+ // Box serves as an anchor for the dropdown menu
+ Box(
+ modifier = Modifier
+ .combinedClickable(onLongClick = { onLongClick(stream) }, onClick = { onClick(stream) })
+ .fillMaxWidth()
+ .padding(12.dp)
+ ) {
Row(
- modifier = Modifier
- .combinedClickable(
- onLongClick = { onLongClick(stream) },
- onClick = { onClick(stream) }
- )
- .fillMaxWidth()
- .padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
@@ -68,9 +67,7 @@ fun StreamListItem(
}
}
- if (isSelected) {
- StreamMenu(stream, onDismissPopup)
- }
+ StreamMenu(stream, isSelected, onDismissPopup)
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
index 7705ea710c7..50b42defebe 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
@@ -27,6 +27,7 @@ import org.schabi.newpipe.viewmodels.StreamViewModel
@Composable
fun StreamMenu(
stream: StreamInfoItem,
+ expanded: Boolean,
onDismissRequest: () -> Unit
) {
val context = LocalContext.current
@@ -34,7 +35,7 @@ fun StreamMenu(
val streamViewModel = viewModel()
val playerHolder = PlayerHolder.getInstance()
- DropdownMenu(expanded = true, onDismissRequest = onDismissRequest) {
+ DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
if (playerHolder.isPlayQueueReady) {
StreamMenuItem(
text = R.string.enqueue_stream,
From 99bf052ec7ad29dbc87c6c678842a8766f722358 Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Mon, 5 Aug 2024 05:07:13 +0530
Subject: [PATCH 12/15] Use view model lifecycle scope
---
.../newpipe/ui/components/items/stream/StreamMenu.kt | 7 +------
.../java/org/schabi/newpipe/viewmodels/StreamViewModel.kt | 8 ++++++--
2 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
index 50b42defebe..2bb143db8e1 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt
@@ -6,12 +6,10 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.viewmodel.compose.viewModel
-import kotlinx.coroutines.launch
import org.schabi.newpipe.R
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.download.DownloadDialog
@@ -31,7 +29,6 @@ fun StreamMenu(
onDismissRequest: () -> Unit
) {
val context = LocalContext.current
- val coroutineScope = rememberCoroutineScope()
val streamViewModel = viewModel()
val playerHolder = PlayerHolder.getInstance()
@@ -124,9 +121,7 @@ fun StreamMenu(
text = R.string.mark_as_watched,
onClick = {
onDismissRequest()
- coroutineScope.launch {
- streamViewModel.markAsWatched(stream)
- }
+ streamViewModel.markAsWatched(stream)
}
)
StreamMenuItem(
diff --git a/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt
index 1fae34d1b38..fff8d6b71fa 100644
--- a/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt
+++ b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt
@@ -2,6 +2,8 @@ package org.schabi.newpipe.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await
import kotlinx.coroutines.rx3.awaitSingleOrNull
import org.schabi.newpipe.database.stream.model.StreamStateEntity
@@ -16,7 +18,9 @@ class StreamViewModel(application: Application) : AndroidViewModel(application)
return historyRecordManager.loadStreamState(infoItem).awaitSingleOrNull()
}
- suspend fun markAsWatched(stream: StreamInfoItem) {
- historyRecordManager.markAsWatched(stream).await()
+ fun markAsWatched(stream: StreamInfoItem) {
+ viewModelScope.launch {
+ historyRecordManager.markAsWatched(stream).await()
+ }
}
}
From 99d0d2d196f51779b4fec1741aa47955fb5d8369 Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Mon, 5 Aug 2024 05:37:14 +0530
Subject: [PATCH 13/15] Make live color solid red
---
.../newpipe/ui/components/items/stream/StreamThumbnail.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt
index b81b6a71e8a..bcccd3217cf 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt
@@ -54,7 +54,7 @@ fun StreamThumbnail(
Text(
modifier = Modifier
.padding(2.dp)
- .background((if (isLive) Color.Red else Color.Black).copy(alpha = 0.5f))
+ .background(if (isLive) Color.Red else Color.Black.copy(alpha = 0.5f))
.padding(2.dp),
text = if (isLive) {
stringResource(R.string.duration_live)
From 6f5187b34fe8573a699b33ed760e56c13ddefa20 Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Sat, 10 Aug 2024 06:25:44 +0530
Subject: [PATCH 14/15] Use nested scroll modifier
---
app/build.gradle | 4 ++--
.../schabi/newpipe/ui/components/items/ItemList.kt | 12 +++++++-----
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index e6d01798c42..92fd2a7e960 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -291,9 +291,9 @@ dependencies {
implementation 'androidx.compose.material3.adaptive:adaptive:1.0.0-beta04'
implementation 'androidx.activity:activity-compose'
implementation 'androidx.compose.ui:ui-tooling-preview'
- implementation 'androidx.compose.ui:ui-text:1.7.0-beta06' // Needed for parsing HTML to AnnotatedString
+ implementation 'androidx.compose.ui:ui-text:1.7.0-beta07' // Needed for parsing HTML to AnnotatedString
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose'
- implementation 'androidx.paging:paging-compose:3.3.1'
+ implementation 'androidx.paging:paging-compose:3.3.2'
implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0'
// Coroutines interop
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
index 42696bc2bd9..d3e1882edf9 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
@@ -9,7 +9,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.stringResource
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager
@@ -62,21 +65,20 @@ fun ItemList(
}
val showProgress = DependentPreferenceHelper.getPositionsInListsEnabled(context)
+ val nestedScrollModifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())
if (mode == ItemViewMode.GRID) {
// TODO: Implement grid layout using LazyVerticalGrid and LazyVerticalGridScrollbar.
} else {
- // Card or list views
- val listState = rememberLazyListState()
+ val state = rememberLazyListState()
- LazyColumnScrollbar(state = listState) {
- LazyColumn(state = listState) {
+ LazyColumnScrollbar(state = state) {
+ LazyColumn(modifier = nestedScrollModifier, state = state) {
listHeader()
items(items.size) {
val item = items[it]
- // TODO: Implement card layouts.
if (item is StreamInfoItem) {
val isSelected = selectedStream == item
StreamListItem(
From 03b47b82da6cb924079102e89c38e931cd1920ef Mon Sep 17 00:00:00 2001
From: Isira Seneviratne
Date: Sat, 10 Aug 2024 06:52:59 +0530
Subject: [PATCH 15/15] Simplify determineItemViewMode()
---
.../newpipe/ui/components/items/ItemList.kt | 18 +++++++-----------
1 file changed, 7 insertions(+), 11 deletions(-)
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
index d3e1882edf9..506687d1276 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt
@@ -95,18 +95,13 @@ fun ItemList(
@Composable
private fun determineItemViewMode(): ItemViewMode {
- val listMode = PreferenceManager.getDefaultSharedPreferences(LocalContext.current)
- .getString(
- stringResource(R.string.list_view_mode_key),
- stringResource(R.string.list_view_mode_value)
- )
+ val prefValue = PreferenceManager.getDefaultSharedPreferences(LocalContext.current)
+ .getString(stringResource(R.string.list_view_mode_key), null)
+ val viewMode = prefValue?.let { ItemViewMode.valueOf(it.uppercase()) } ?: ItemViewMode.AUTO
- return when (listMode) {
- stringResource(R.string.list_view_mode_list_key) -> ItemViewMode.LIST
- stringResource(R.string.list_view_mode_grid_key) -> ItemViewMode.GRID
- stringResource(R.string.list_view_mode_card_key) -> ItemViewMode.CARD
- else -> {
- // Auto mode - evaluate whether to use Grid based on screen real estate.
+ return when (viewMode) {
+ ItemViewMode.AUTO -> {
+ // Evaluate whether to use Grid based on screen real estate.
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) {
ItemViewMode.GRID
@@ -114,5 +109,6 @@ private fun determineItemViewMode(): ItemViewMode {
ItemViewMode.LIST
}
}
+ else -> viewMode
}
}