Merge branch 'refs/heads/refactor' into About-Compose
# Conflicts: # app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt # build.gradle
This commit is contained in:
commit
226b6de34f
|
@ -47,10 +47,10 @@ jobs:
|
|||
BRANCH: ${{ github.head_ref }}
|
||||
run: git checkout -B "$BRANCH"
|
||||
|
||||
- name: set up JDK 17
|
||||
- name: set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
|
@ -88,10 +88,10 @@ jobs:
|
|||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: set up JDK 17
|
||||
- name: set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
|
@ -121,10 +121,10 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
cache: 'gradle'
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ captures/
|
|||
*.class
|
||||
app/debug/
|
||||
app/release/
|
||||
.kotlin/
|
||||
|
||||
# vscode / eclipse files
|
||||
*.classpath
|
||||
|
|
|
@ -96,6 +96,7 @@ android {
|
|||
buildFeatures {
|
||||
viewBinding true
|
||||
compose true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
|
@ -116,7 +117,7 @@ ext {
|
|||
androidxRoomVersion = '2.6.1'
|
||||
androidxWorkVersion = '2.8.1'
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
stateSaverVersion = '1.4.1'
|
||||
exoPlayerVersion = '2.18.7'
|
||||
googleAutoServiceVersion = '1.1.1'
|
||||
groupieVersion = '2.10.1'
|
||||
|
@ -206,7 +207,8 @@ dependencies {
|
|||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.teamnewpipe:newpipeextractor:v0.24.2'
|
||||
// WORKAROUND: v0.24.2 can't be resolved by jitpack -> use git commit hash instead
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:176da72cb4c3ec4679211339b0e59f6b01bf2f52'
|
||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||
|
||||
/** Checkstyle **/
|
||||
|
@ -222,7 +224,7 @@ dependencies {
|
|||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.fragment:fragment-compose:1.8.2'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||
|
@ -239,8 +241,9 @@ dependencies {
|
|||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
implementation "frankiesardo:icepick:${icepickVersion}"
|
||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||
implementation 'com.github.livefront:bridge:v2.0.2'
|
||||
implementation "com.evernote:android-state:$stateSaverVersion"
|
||||
kapt "com.evernote:android-state-processor:$stateSaverVersion"
|
||||
|
||||
// HTML parser
|
||||
implementation "org.jsoup:jsoup:1.17.2"
|
||||
|
@ -288,18 +291,19 @@ dependencies {
|
|||
// Date and time formatting
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final"
|
||||
|
||||
// Jetpack Compose BOM group
|
||||
implementation(platform('androidx.compose:compose-bom:2024.09.03'))
|
||||
// Jetpack Compose
|
||||
implementation(platform('androidx.compose:compose-bom:2024.10.01'))
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
implementation 'androidx.compose.material3.adaptive:adaptive'
|
||||
implementation 'androidx.activity:activity-compose'
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose'
|
||||
implementation 'androidx.compose.ui:ui-text' // Needed for parsing HTML to AnnotatedString
|
||||
implementation 'androidx.compose.material:material-icons-extended'
|
||||
|
||||
// Jetpack Compose related dependencies
|
||||
implementation 'androidx.compose.material3.adaptive:adaptive:1.0.0'
|
||||
implementation 'androidx.activity:activity-compose:1.9.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6'
|
||||
implementation 'androidx.paging:paging-compose:3.3.2'
|
||||
implementation "androidx.navigation:navigation-compose:2.8.2"
|
||||
implementation "androidx.navigation:navigation-compose:2.8.3"
|
||||
|
||||
// Coroutines interop
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.1'
|
||||
|
|
|
@ -13,15 +13,6 @@
|
|||
## Rules for ExoPlayer
|
||||
-keep class com.google.android.exoplayer2.** { *; }
|
||||
|
||||
## Rules for Icepick. Copy pasted from https://github.com/frankiesardo/icepick
|
||||
-dontwarn icepick.**
|
||||
-keep class icepick.** { *; }
|
||||
-keep class **$$Icepick { *; }
|
||||
-keepclasseswithmembernames class * {
|
||||
@icepick.* <fields>;
|
||||
}
|
||||
-keepnames class * { @icepick.State *;}
|
||||
|
||||
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.BridgeStateSaverInitializer;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
@ -107,6 +108,7 @@ public class App extends Application implements ImageLoaderFactory {
|
|||
Localization.getPreferredContentCountry(this));
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
||||
|
||||
BridgeStateSaverInitializer.init(this);
|
||||
StateSaver.init(this);
|
||||
initNotificationChannels();
|
||||
|
||||
|
|
|
@ -10,8 +10,9 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
|
@ -48,7 +49,7 @@ public abstract class BaseFragment extends Fragment {
|
|||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
|
@ -70,7 +71,7 @@ public abstract class BaseFragment extends Fragment {
|
|||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||
|
|
|
@ -44,7 +44,6 @@ import android.widget.FrameLayout;
|
|||
import android.widget.Spinner;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
@ -52,7 +51,6 @@ import androidx.core.app.ActivityCompat;
|
|||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentContainerView;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
|
@ -66,13 +64,11 @@ import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
|||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
|
@ -557,39 +553,27 @@ public class MainActivity extends AppCompatActivity {
|
|||
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||
// handled by it
|
||||
if (bottomSheetHiddenOrCollapsed()) {
|
||||
final FragmentManager fm = getSupportFragmentManager();
|
||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (fragment instanceof BackPressable) {
|
||||
if (((BackPressable) fragment).onBackPressed()) {
|
||||
return;
|
||||
}
|
||||
} else if (fragment instanceof CommentRepliesFragment) {
|
||||
// expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// to show the top level comments again
|
||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// and no other CommentRepliesFragments are on top of the back stack
|
||||
// to show the top level comments again.
|
||||
openDetailFragmentFromCommentReplies(fm, false);
|
||||
}
|
||||
final var fragmentManager = getSupportFragmentManager();
|
||||
|
||||
} else {
|
||||
final Fragment fragmentPlayer = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
if (bottomSheetHiddenOrCollapsed()) {
|
||||
final var fragment = fragmentManager.findFragmentById(R.id.fragment_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (fragmentPlayer instanceof BackPressable) {
|
||||
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
if (fragment instanceof BackPressable backPressable && backPressable.onBackPressed()) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final var player = fragmentManager.findFragmentById(R.id.fragment_player_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (player instanceof BackPressable backPressable && !backPressable.onBackPressed()) {
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
if (fragmentManager.getBackStackEntryCount() == 1) {
|
||||
finish();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
|
@ -648,15 +632,9 @@ public class MainActivity extends AppCompatActivity {
|
|||
* </pre>
|
||||
*/
|
||||
private void onHomeButtonPressed() {
|
||||
final FragmentManager fm = getSupportFragmentManager();
|
||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||
final var fm = getSupportFragmentManager();
|
||||
|
||||
if (fragment instanceof CommentRepliesFragment) {
|
||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// and no other CommentRepliesFragments are on top of the back stack
|
||||
// to show the top level comments again.
|
||||
openDetailFragmentFromCommentReplies(fm, true);
|
||||
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||
if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||
// If search fragment wasn't found in the backstack go to the main fragment
|
||||
NavigationHelper.gotoMainFragment(fm);
|
||||
}
|
||||
|
@ -854,68 +832,6 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private void openDetailFragmentFromCommentReplies(
|
||||
@NonNull final FragmentManager fm,
|
||||
final boolean popBackStack
|
||||
) {
|
||||
// obtain the name of the fragment under the replies fragment that's going to be popped
|
||||
@Nullable final String fragmentUnderEntryName;
|
||||
if (fm.getBackStackEntryCount() < 2) {
|
||||
fragmentUnderEntryName = null;
|
||||
} else {
|
||||
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
|
||||
.getName();
|
||||
}
|
||||
|
||||
// the root comment is the comment for which the user opened the replies page
|
||||
@Nullable final CommentRepliesFragment repliesFragment =
|
||||
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
|
||||
@Nullable final CommentsInfoItem rootComment =
|
||||
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
|
||||
|
||||
// sometimes this function pops the backstack, other times it's handled by the system
|
||||
if (popBackStack) {
|
||||
fm.popBackStackImmediate();
|
||||
}
|
||||
|
||||
// only expand the bottom sheet back if there are no more nested comment replies fragments
|
||||
// stacked under the one that is currently being popped
|
||||
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
|
||||
.from(mainBinding.fragmentPlayerHolder);
|
||||
// do not return to the comment if the details fragment was closed
|
||||
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
// scroll to the root comment once the bottom sheet expansion animation is finished
|
||||
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull final View bottomSheet,
|
||||
final int newState) {
|
||||
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
final Fragment detailFragment = fm.findFragmentById(
|
||||
R.id.fragment_player_holder);
|
||||
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
|
||||
// should always be the case
|
||||
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
|
||||
}
|
||||
behavior.removeBottomSheetCallback(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
||||
// not needed, listener is removed once the sheet is expanded
|
||||
}
|
||||
});
|
||||
|
||||
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
}
|
||||
|
||||
private boolean bottomSheetHiddenOrCollapsed() {
|
||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||
|
|
|
@ -41,6 +41,9 @@ import androidx.lifecycle.Lifecycle;
|
|||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
|
@ -98,8 +101,6 @@ import java.util.List;
|
|||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
@ -152,7 +153,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||
getWindow().setAttributes(params);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
// FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
|
||||
// We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
|
||||
|
@ -197,7 +198,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -39,6 +39,8 @@ import androidx.documentfile.provider.DocumentFile;
|
|||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
|
@ -59,6 +61,8 @@ import org.schabi.newpipe.settings.NewPipeSettings;
|
|||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
|
@ -67,8 +71,6 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
|
|||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter;
|
||||
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -79,8 +81,6 @@ import java.util.Locale;
|
|||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
|
@ -214,7 +214,7 @@ public class DownloadDialog extends DialogFragment
|
|||
context = getContext();
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
this.audioTrackAdapter = new AudioTrackAdapter(wrappedAudioTracks);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(wrappedSubtitleStreams);
|
||||
|
@ -372,7 +372,7 @@ public class DownloadDialog extends DialogFragment
|
|||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -185,10 +185,8 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
|||
final int abuseEnd = url.indexOf("+path");
|
||||
|
||||
try {
|
||||
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
|
||||
abuseCookie = Utils.decodeUrlUtf8(abuseCookie);
|
||||
handleCookies(abuseCookie);
|
||||
} catch (IllegalArgumentException | StringIndexOutOfBoundsException e) {
|
||||
handleCookies(Utils.decodeUrlUtf8(url.substring(abuseStart + 13, abuseEnd)));
|
||||
} catch (final StringIndexOutOfBoundsException e) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.e(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
||||
+ abuseStart + " and ending at " + abuseEnd + " for url " + url, e);
|
||||
|
|
|
@ -13,6 +13,8 @@ import androidx.annotation.Nullable;
|
|||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
|
@ -22,8 +24,6 @@ import org.schabi.newpipe.util.InfoCache;
|
|||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||
@State
|
||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||
|
@ -134,6 +134,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
|||
hideErrorPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
isLoading.set(false);
|
||||
if (emptyStateView != null) {
|
||||
|
|
|
@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
|
@ -19,8 +21,6 @@ import org.schabi.newpipe.util.Localization;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||
|
||||
@State
|
||||
|
@ -31,7 +31,7 @@ public class DescriptionFragment extends BaseDescriptionFragment {
|
|||
}
|
||||
|
||||
public DescriptionFragment() {
|
||||
// keep empty constructor for IcePick when resuming fragment from memory
|
||||
// keep empty constructor for State when resuming fragment from memory
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ import androidx.core.content.ContextCompat;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
|
@ -73,7 +74,6 @@ import org.schabi.newpipe.error.ReCaptchaActivity;
|
|||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.Image;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
|
@ -128,7 +128,6 @@ import java.util.concurrent.TimeUnit;
|
|||
import java.util.function.Consumer;
|
||||
|
||||
import coil.util.CoilUtils;
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
@ -881,8 +880,7 @@ public final class VideoDetailFragment
|
|||
tabContentDescriptions.clear();
|
||||
|
||||
if (shouldShowComments()) {
|
||||
pageAdapter.addFragment(
|
||||
CommentsFragment.getInstance(serviceId, url, title), COMMENTS_TAB_TAG);
|
||||
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url), COMMENTS_TAB_TAG);
|
||||
tabIcons.add(R.drawable.ic_comment);
|
||||
tabContentDescriptions.add(R.string.comments_tab_description);
|
||||
}
|
||||
|
@ -1012,20 +1010,6 @@ public final class VideoDetailFragment
|
|||
updateTabLayoutVisibility();
|
||||
}
|
||||
|
||||
public void scrollToComment(final CommentsInfoItem comment) {
|
||||
final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG);
|
||||
final Fragment fragment = pageAdapter.getItem(commentsTabPos);
|
||||
if (!(fragment instanceof CommentsFragment)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// unexpand the app bar only if scrolling to the comment succeeded
|
||||
if (((CommentsFragment) fragment).scrollToComment(comment)) {
|
||||
binding.appBarLayout.setExpanded(false, false);
|
||||
binding.viewPager.setCurrentItem(commentsTabPos, false);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Play Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
|
@ -9,6 +9,8 @@ import android.view.View;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
|
@ -24,7 +26,6 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
@ -143,7 +144,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
|||
currentWorker = loadResult(forceLoad)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull L result) -> {
|
||||
.subscribe((@NonNull final L result) -> {
|
||||
isLoading.set(false);
|
||||
currentInfo = result;
|
||||
currentNextPage = result.getNextPage();
|
||||
|
|
|
@ -10,6 +10,8 @@ import android.widget.LinearLayout;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
|
@ -20,8 +22,6 @@ import org.schabi.newpipe.util.Localization;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public class ChannelAboutFragment extends BaseDescriptionFragment {
|
||||
@State
|
||||
protected ChannelInfo channelInfo;
|
||||
|
@ -31,7 +31,7 @@ public class ChannelAboutFragment extends BaseDescriptionFragment {
|
|||
}
|
||||
|
||||
public ChannelAboutFragment() {
|
||||
// keep empty constructor for IcePick when resuming fragment from memory
|
||||
// keep empty constructor for State when resuming fragment from memory
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -25,6 +25,7 @@ import androidx.core.graphics.ColorUtils;
|
|||
import androidx.core.view.MenuProvider;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
@ -60,7 +61,6 @@ import java.util.Queue;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import coil.util.CoilUtils;
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
@ -249,7 +249,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void monitorSubscription(final ChannelInfo info) {
|
||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||
final Consumer<Throwable> onError = (final Throwable throwable) -> {
|
||||
animate(binding.channelSubscribeButton, false, 100);
|
||||
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
|
||||
"Get subscription status", currentInfo));
|
||||
|
@ -284,14 +284,14 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||
}
|
||||
|
||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||
return (@NonNull Object o) -> {
|
||||
return (@NonNull final Object o) -> {
|
||||
subscriptionManager.insertSubscription(subscription);
|
||||
return o;
|
||||
};
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
||||
return (@NonNull Object o) -> {
|
||||
return (@NonNull final Object o) -> {
|
||||
subscriptionManager.deleteSubscription(subscription);
|
||||
return o;
|
||||
};
|
||||
|
@ -318,7 +318,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||
}
|
||||
|
||||
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
|
||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
||||
final Consumer<Object> onNext = (@NonNull final Object o) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Changed subscription status to this channel!");
|
||||
}
|
||||
|
@ -338,7 +338,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||
}
|
||||
|
||||
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
||||
return (List<SubscriptionEntity> subscriptionEntities) -> {
|
||||
return (final List<SubscriptionEntity> subscriptionEntities) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
|
||||
+ "subscriptionEntities = [" + subscriptionEntities + "]");
|
||||
|
|
|
@ -9,6 +9,8 @@ import android.view.ViewGroup;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
|
@ -32,13 +34,12 @@ import java.util.List;
|
|||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
|
||||
implements PlaylistControlViewHolder {
|
||||
|
||||
// states must be protected and not private for IcePick being able to access them
|
||||
// states must be protected and not private for State being able to access them
|
||||
@State
|
||||
protected ListLinkHandler tabHandler;
|
||||
@State
|
||||
|
@ -156,6 +157,7 @@ public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTa
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayQueue getPlayQueue() {
|
||||
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||
.filter(StreamInfoItem.class::isInstance)
|
||||
|
|
|
@ -1,170 +0,0 @@
|
|||
package org.schabi.newpipe.fragments.list.comments;
|
||||
|
||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.CommentRepliesHeaderBinding;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public final class CommentRepliesFragment
|
||||
extends BaseListInfoFragment<CommentsInfoItem, CommentRepliesInfo> {
|
||||
|
||||
public static final String TAG = CommentRepliesFragment.class.getSimpleName();
|
||||
|
||||
@State
|
||||
CommentsInfoItem commentsInfoItem; // the comment to show replies of
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Constructors and lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
// only called by the Android framework, after which readFrom is called and restores all data
|
||||
public CommentRepliesFragment() {
|
||||
super(UserAction.REQUESTED_COMMENT_REPLIES);
|
||||
}
|
||||
|
||||
public CommentRepliesFragment(@NonNull final CommentsInfoItem commentsInfoItem) {
|
||||
this();
|
||||
this.commentsInfoItem = commentsInfoItem;
|
||||
// setting "" as title since the title will be properly set right after
|
||||
setInitialData(commentsInfoItem.getServiceId(), commentsInfoItem.getUrl(), "");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
disposables.clear();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<View> getListHeaderSupplier() {
|
||||
return () -> {
|
||||
final CommentRepliesHeaderBinding binding = CommentRepliesHeaderBinding
|
||||
.inflate(activity.getLayoutInflater(), itemsList, false);
|
||||
final CommentsInfoItem item = commentsInfoItem;
|
||||
|
||||
// load the author avatar
|
||||
CoilHelper.INSTANCE.loadAvatar(binding.authorAvatar, item.getUploaderAvatars());
|
||||
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
|
||||
? View.VISIBLE : View.GONE);
|
||||
|
||||
// setup author name and comment date
|
||||
binding.authorName.setText(item.getUploaderName());
|
||||
binding.uploadDate.setText(Localization.relativeTimeOrTextual(
|
||||
getContext(), item.getUploadDate(), item.getTextualUploadDate()));
|
||||
binding.authorTouchArea.setOnClickListener(
|
||||
v -> NavigationHelper.openCommentAuthorIfPresent(requireActivity(), item));
|
||||
|
||||
// setup like count, hearted and pinned
|
||||
binding.thumbsUpCount.setText(
|
||||
Localization.likeCount(requireContext(), item.getLikeCount()));
|
||||
// for heartImage goneMarginEnd was used, but there is no way to tell ConstraintLayout
|
||||
// not to use a different margin only when both the next two views are gone
|
||||
((ConstraintLayout.LayoutParams) binding.thumbsUpCount.getLayoutParams())
|
||||
.setMarginEnd(DeviceUtils.dpToPx(
|
||||
(item.isHeartedByUploader() || item.isPinned() ? 8 : 16),
|
||||
requireContext()));
|
||||
binding.heartImage.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||
binding.pinnedImage.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||
|
||||
// setup comment content
|
||||
TextLinkifier.fromDescription(binding.commentContent, item.getCommentText(),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY, getServiceById(item.getServiceId()),
|
||||
item.getUrl(), disposables, null);
|
||||
|
||||
return binding.getRoot();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// State saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void writeTo(final Queue<Object> objectsToSave) {
|
||||
super.writeTo(objectsToSave);
|
||||
objectsToSave.add(commentsInfoItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
||||
super.readFrom(savedObjects);
|
||||
commentsInfoItem = (CommentsInfoItem) savedObjects.poll();
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Data loading
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<CommentRepliesInfo> loadResult(final boolean forceLoad) {
|
||||
return Single.fromCallable(() -> new CommentRepliesInfo(commentsInfoItem,
|
||||
// the reply count string will be shown as the activity title
|
||||
Localization.replyCount(requireContext(), commentsInfoItem.getReplyCount())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
||||
// commentsInfoItem.getUrl() should contain the url of the original
|
||||
// ListInfo<CommentsInfoItem>, which should be the stream url
|
||||
return ExtractorHelper.getMoreCommentItems(
|
||||
serviceId, commentsInfoItem.getUrl(), currentNextPage);
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected ItemViewMode getItemViewMode() {
|
||||
return ItemViewMode.LIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the comment to which the replies are shown
|
||||
*/
|
||||
public CommentsInfoItem getCommentsInfoItem() {
|
||||
return commentsInfoItem;
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package org.schabi.newpipe.fragments.list.comments;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public final class CommentRepliesInfo extends ListInfo<CommentsInfoItem> {
|
||||
/**
|
||||
* This class is used to wrap the comment replies page into a ListInfo object.
|
||||
*
|
||||
* @param comment the comment from which to get replies
|
||||
* @param name will be shown as the fragment title
|
||||
*/
|
||||
public CommentRepliesInfo(final CommentsInfoItem comment, final String name) {
|
||||
super(comment.getServiceId(),
|
||||
new ListLinkHandler("", "", "", Collections.emptyList(), null), name);
|
||||
setNextPage(comment.getReplies());
|
||||
setRelatedItems(Collections.emptyList()); // since it must be non-null
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
package org.schabi.newpipe.fragments.list.comments;
|
||||
|
||||
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 android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.ItemViewMode;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, CommentsInfo> {
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private TextView emptyStateDesc;
|
||||
|
||||
public static CommentsFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
final CommentsFragment instance = new CommentsFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public CommentsFragment() {
|
||||
super(UserAction.REQUESTED_COMMENTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
emptyStateDesc = rootView.findViewById(R.id.empty_state_desc);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<CommentsInfo> loadResult(final boolean forceLoad) {
|
||||
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull final CommentsInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
emptyStateDesc.setText(
|
||||
result.isCommentsDisabled()
|
||||
? R.string.comments_are_disabled
|
||||
: R.string.no_comments);
|
||||
|
||||
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setTitle(final String title) { }
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) { }
|
||||
|
||||
@Override
|
||||
protected ItemViewMode getItemViewMode() {
|
||||
return ItemViewMode.LIST;
|
||||
}
|
||||
|
||||
public boolean scrollToComment(final CommentsInfoItem comment) {
|
||||
final int position = infoListAdapter.getItemsList().indexOf(comment);
|
||||
if (position < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
itemsList.scrollToPosition(position);
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package org.schabi.newpipe.fragments.list.comments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.compose.content
|
||||
import org.schabi.newpipe.ui.components.video.comment.CommentSection
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.KEY_SERVICE_ID
|
||||
import org.schabi.newpipe.util.KEY_URL
|
||||
|
||||
class CommentsFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = content {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
CommentSection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(serviceId: Int, url: String?) = CommentsFragment().apply {
|
||||
arguments = bundleOf(KEY_SERVICE_ID to serviceId, KEY_URL to url)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
|
@ -29,7 +31,6 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
|||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
/**
|
||||
|
|
|
@ -40,6 +40,8 @@ import androidx.preference.PreferenceManager;
|
|||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentSearchBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
|
@ -77,7 +79,6 @@ import java.util.Queue;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
@ -550,7 +551,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
|
||||
searchEditText.setOnFocusChangeListener((final View v, final boolean hasFocus) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onFocusChange() called with: "
|
||||
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||
|
@ -611,7 +612,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
};
|
||||
searchEditText.addTextChangedListener(textWatcher);
|
||||
searchEditText.setOnEditorActionListener(
|
||||
(TextView v, int actionId, KeyEvent event) -> {
|
||||
(final TextView v, final int actionId, final KeyEvent event) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], "
|
||||
+ "actionId = [" + actionId + "], event = [" + event + "]");
|
||||
|
|
|
@ -2,14 +2,12 @@ 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.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.compose.content
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.ktx.serializable
|
||||
import org.schabi.newpipe.ui.components.video.RelatedItems
|
||||
|
@ -21,15 +19,10 @@ class RelatedItemsFragment : Fragment() {
|
|||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
RelatedItems(requireArguments().serializable<StreamInfo>(KEY_INFO)!!)
|
||||
}
|
||||
}
|
||||
) = content {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
RelatedItems(requireArguments().serializable<StreamInfo>(KEY_INFO)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
|||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||
|
@ -75,21 +74,16 @@ public class InfoItemBuilder {
|
|||
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
||||
@NonNull final InfoItem.InfoType infoType,
|
||||
final boolean useMiniVariant) {
|
||||
switch (infoType) {
|
||||
case STREAM:
|
||||
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
||||
: new StreamInfoItemHolder(this, parent);
|
||||
case CHANNEL:
|
||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
||||
: new ChannelInfoItemHolder(this, parent);
|
||||
case PLAYLIST:
|
||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||
: new PlaylistInfoItemHolder(this, parent);
|
||||
case COMMENT:
|
||||
return new CommentInfoItemHolder(this, parent);
|
||||
default:
|
||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
||||
}
|
||||
return switch (infoType) {
|
||||
case STREAM -> useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
||||
: new StreamInfoItemHolder(this, parent);
|
||||
case CHANNEL -> useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
||||
: new ChannelInfoItemHolder(this, parent);
|
||||
case PLAYLIST -> useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||
: new PlaylistInfoItemHolder(this, parent);
|
||||
case COMMENT ->
|
||||
throw new IllegalArgumentException("Comments should be rendered using Compose");
|
||||
};
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
|
|
|
@ -21,7 +21,6 @@ import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
|
|||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||
|
@ -283,46 +282,32 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||
Log.d(TAG, "onCreateViewHolder() called with: "
|
||||
+ "parent = [" + parent + "], type = [" + type + "]");
|
||||
}
|
||||
switch (type) {
|
||||
return switch (type) {
|
||||
// #4475 and #3368
|
||||
// Always create a new instance otherwise the same instance
|
||||
// is sometimes reused which causes a crash
|
||||
case HEADER_TYPE:
|
||||
return new HFHolder(headerSupplier.get());
|
||||
case FOOTER_TYPE:
|
||||
return new HFHolder(PignateFooterBinding
|
||||
.inflate(layoutInflater, parent, false)
|
||||
.getRoot()
|
||||
);
|
||||
case MINI_STREAM_HOLDER_TYPE:
|
||||
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case STREAM_HOLDER_TYPE:
|
||||
return new StreamInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_STREAM_HOLDER_TYPE:
|
||||
return new StreamGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_STREAM_HOLDER_TYPE:
|
||||
return new StreamCardInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelCardInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_PLAYLIST_HOLDER_TYPE:
|
||||
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
||||
case COMMENT_HOLDER_TYPE:
|
||||
return new CommentInfoItemHolder(infoItemBuilder, parent);
|
||||
default:
|
||||
return new FallbackViewHolder(new View(parent.getContext()));
|
||||
}
|
||||
case HEADER_TYPE -> new HFHolder(headerSupplier.get());
|
||||
case FOOTER_TYPE -> new HFHolder(PignateFooterBinding
|
||||
.inflate(layoutInflater, parent, false)
|
||||
.getRoot()
|
||||
);
|
||||
case MINI_STREAM_HOLDER_TYPE -> new StreamMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case STREAM_HOLDER_TYPE -> new StreamInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_STREAM_HOLDER_TYPE -> new StreamGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_STREAM_HOLDER_TYPE -> new StreamCardInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_CHANNEL_HOLDER_TYPE -> new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case CHANNEL_HOLDER_TYPE -> new ChannelInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_CHANNEL_HOLDER_TYPE -> new ChannelCardInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_CHANNEL_HOLDER_TYPE -> new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_PLAYLIST_HOLDER_TYPE ->
|
||||
new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case PLAYLIST_HOLDER_TYPE -> new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_PLAYLIST_HOLDER_TYPE ->
|
||||
new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_PLAYLIST_HOLDER_TYPE ->
|
||||
new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
|
||||
default -> new FallbackViewHolder(new View(parent.getContext()));
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,208 +0,0 @@
|
|||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
|
||||
import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.image.CoilHelper;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
||||
|
||||
public class CommentInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
private static final int COMMENT_DEFAULT_LINES = 2;
|
||||
private final int commentHorizontalPadding;
|
||||
private final int commentVerticalPadding;
|
||||
|
||||
private final RelativeLayout itemRoot;
|
||||
private final ImageView itemThumbnailView;
|
||||
private final TextView itemContentView;
|
||||
private final ImageView itemThumbsUpView;
|
||||
private final TextView itemLikesCountView;
|
||||
private final TextView itemTitleView;
|
||||
private final ImageView itemHeartView;
|
||||
private final ImageView itemPinnedView;
|
||||
private final Button repliesButton;
|
||||
|
||||
@NonNull
|
||||
private final TextEllipsizer textEllipsizer;
|
||||
|
||||
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
||||
final ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_comment_item, parent);
|
||||
|
||||
itemRoot = itemView.findViewById(R.id.itemRoot);
|
||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||
itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view);
|
||||
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
|
||||
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
|
||||
repliesButton = itemView.findViewById(R.id.replies_button);
|
||||
|
||||
commentHorizontalPadding = (int) infoItemBuilder.getContext()
|
||||
.getResources().getDimension(R.dimen.comments_horizontal_padding);
|
||||
commentVerticalPadding = (int) infoItemBuilder.getContext()
|
||||
.getResources().getDimension(R.dimen.comments_vertical_padding);
|
||||
|
||||
textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
|
||||
textEllipsizer.setStateChangeListener(isEllipsized -> {
|
||||
if (Boolean.TRUE.equals(isEllipsized)) {
|
||||
denyLinkFocus();
|
||||
} else {
|
||||
determineMovementMethod();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFromItem(final InfoItem infoItem,
|
||||
final HistoryRecordManager historyRecordManager) {
|
||||
if (!(infoItem instanceof CommentsInfoItem item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// load the author avatar
|
||||
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getUploaderAvatars());
|
||||
if (ImageStrategy.shouldLoadImages()) {
|
||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
||||
commentVerticalPadding, commentVerticalPadding);
|
||||
} else {
|
||||
itemThumbnailView.setVisibility(View.GONE);
|
||||
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
|
||||
commentHorizontalPadding, commentVerticalPadding);
|
||||
}
|
||||
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
|
||||
|
||||
|
||||
// setup the top row, with pinned icon, author name and comment date
|
||||
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
|
||||
itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(),
|
||||
Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(),
|
||||
item.getTextualUploadDate())));
|
||||
|
||||
|
||||
// setup bottom row, with likes, heart and replies button
|
||||
itemLikesCountView.setText(
|
||||
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
|
||||
|
||||
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
|
||||
|
||||
final boolean hasReplies = item.getReplies() != null;
|
||||
repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null);
|
||||
repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE);
|
||||
repliesButton.setText(hasReplies
|
||||
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : "");
|
||||
((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin =
|
||||
hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext());
|
||||
|
||||
|
||||
// setup comment content and click listeners to expand/ellipsize it
|
||||
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
|
||||
textEllipsizer.setStreamUrl(item.getUrl());
|
||||
textEllipsizer.setContent(item.getCommentText());
|
||||
textEllipsizer.ellipsize();
|
||||
|
||||
//noinspection ClickableViewAccessibility
|
||||
itemContentView.setOnTouchListener((v, event) -> {
|
||||
final CharSequence text = itemContentView.getText();
|
||||
if (text instanceof Spanned buffer) {
|
||||
final int action = event.getAction();
|
||||
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
|
||||
final int offset = getOffsetForHorizontalLine(itemContentView, event);
|
||||
final var links = buffer.getSpans(offset, offset, ClickableSpan.class);
|
||||
|
||||
if (links.length != 0) {
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
links[0].onClick(itemContentView);
|
||||
}
|
||||
// we handle events that intersect links, so return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
textEllipsizer.toggle();
|
||||
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
||||
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
||||
}
|
||||
});
|
||||
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
||||
openCommentAuthor(item);
|
||||
} else {
|
||||
final CharSequence text = itemContentView.getText();
|
||||
if (text != null) {
|
||||
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void openCommentAuthor(@NonNull final CommentsInfoItem item) {
|
||||
NavigationHelper.openCommentAuthorIfPresent((FragmentActivity) itemBuilder.getContext(),
|
||||
item);
|
||||
}
|
||||
|
||||
private void openCommentReplies(@NonNull final CommentsInfoItem item) {
|
||||
NavigationHelper.openCommentRepliesFragment((FragmentActivity) itemBuilder.getContext(),
|
||||
item);
|
||||
}
|
||||
|
||||
private void allowLinkFocus() {
|
||||
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
private void denyLinkFocus() {
|
||||
itemContentView.setMovementMethod(null);
|
||||
}
|
||||
|
||||
private boolean shouldFocusLinks() {
|
||||
if (itemView.isInTouchMode()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final URLSpan[] urls = itemContentView.getUrls();
|
||||
|
||||
return urls != null && urls.length != 0;
|
||||
}
|
||||
|
||||
private void determineMovementMethod() {
|
||||
if (shouldFocusLinks()) {
|
||||
allowLinkFocus();
|
||||
} else {
|
||||
denyLinkFocus();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.schabi.newpipe.ktx
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
tailrec fun Context.findFragmentActivity(): FragmentActivity {
|
||||
return when (this) {
|
||||
is FragmentActivity -> this
|
||||
is ContextWrapper -> baseContext.findFragmentActivity()
|
||||
else -> throw IllegalStateException("Unable to find FragmentActivity")
|
||||
}
|
||||
}
|
|
@ -19,6 +19,8 @@ import androidx.fragment.app.FragmentManager;
|
|||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
|
@ -36,16 +38,15 @@ import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
|
|||
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
|
|
@ -44,11 +44,11 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.evernote.android.state.State
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.Item
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.OnItemLongClickListener
|
||||
import icepick.State
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
|
|
|
@ -15,6 +15,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
|
@ -45,7 +46,6 @@ import java.util.Comparator;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
@ -368,6 +368,7 @@ public class StatisticsPlaylistFragment
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@ import androidx.recyclerview.widget.ItemTouchHelper;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
|
@ -49,12 +51,12 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
|
|||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSavable;
|
||||
import org.schabi.newpipe.util.debounce.DebounceSaver;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -63,7 +65,6 @@ import java.util.List;
|
|||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
@ -843,6 +844,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
@ -10,13 +12,11 @@ import androidx.appcompat.app.AlertDialog;
|
|||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class ImportConfirmationDialog extends DialogFragment {
|
||||
@State
|
||||
protected Intent resultServiceIntent;
|
||||
|
@ -57,12 +57,12 @@ public class ImportConfirmationDialog extends DialogFragment {
|
|||
throw new IllegalStateException("Result intent is null");
|
||||
}
|
||||
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,11 +20,11 @@ import androidx.annotation.StringRes
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.evernote.android.state.State
|
||||
import com.xwray.groupie.Group
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import icepick.State
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
|
||||
|
|
|
@ -27,6 +27,8 @@ import androidx.annotation.StringRes;
|
|||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.text.util.LinkifyCompat;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
|
@ -44,8 +46,6 @@ import org.schabi.newpipe.util.ServiceHelper;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public class SubscriptionsImportFragment extends BaseFragment {
|
||||
@State
|
||||
int currentServiceId = Constants.NO_SERVICE_ID;
|
||||
|
|
|
@ -18,11 +18,11 @@ import androidx.lifecycle.Observer
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.evernote.android.state.State
|
||||
import com.livefront.bridge.Bridge
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.Section
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
|
||||
|
@ -78,7 +78,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Icepick.restoreInstanceState(this, savedInstanceState)
|
||||
Bridge.restoreInstanceState(this, savedInstanceState)
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
|
||||
groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED
|
||||
|
@ -115,7 +115,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||
iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState()
|
||||
subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState()
|
||||
|
||||
Icepick.saveInstanceState(this, outState)
|
||||
Bridge.saveInstanceState(this, outState)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -11,10 +11,10 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
|||
import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.evernote.android.state.State
|
||||
import com.livefront.bridge.Bridge
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.TouchCallback
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
|
||||
|
@ -23,10 +23,6 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
|
|||
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.util.Collections
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.map
|
||||
import kotlin.collections.sortedBy
|
||||
|
||||
class FeedGroupReorderDialog : DialogFragment() {
|
||||
private var _binding: DialogFeedGroupReorderBinding? = null
|
||||
|
@ -42,7 +38,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Icepick.restoreInstanceState(this, savedInstanceState)
|
||||
Bridge.restoreInstanceState(this, savedInstanceState)
|
||||
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
|
||||
}
|
||||
|
@ -80,7 +76,7 @@ class FeedGroupReorderDialog : DialogFragment() {
|
|||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
Icepick.saveInstanceState(this, outState)
|
||||
Bridge.saveInstanceState(this, outState)
|
||||
}
|
||||
|
||||
private fun handleGroups(list: List<FeedGroupEntity>) {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package org.schabi.newpipe.paging
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
|
||||
class CommentRepliesSource(
|
||||
private val commentInfo: CommentsInfoItem,
|
||||
) : PagingSource<Page, CommentsInfoItem>() {
|
||||
private val service = NewPipe.getService(commentInfo.serviceId)
|
||||
|
||||
override suspend fun load(params: LoadParams<Page>): LoadResult<Page, CommentsInfoItem> {
|
||||
// params.key is null the first time load() is called, and we need to return the first page
|
||||
val repliesPage = params.key ?: commentInfo.replies
|
||||
val info = withContext(Dispatchers.IO) {
|
||||
CommentsInfo.getMoreItems(service, commentInfo.url, repliesPage)
|
||||
}
|
||||
return LoadResult.Page(info.items, null, info.nextPage)
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package org.schabi.newpipe.paging
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
|
||||
|
||||
class CommentsSource(private val commentInfo: CommentInfo) : PagingSource<Page, CommentsInfoItem>() {
|
||||
private val service = NewPipe.getService(commentInfo.serviceId)
|
||||
|
||||
override suspend fun load(params: LoadParams<Page>): LoadResult<Page, CommentsInfoItem> {
|
||||
// params.key is null the first time the load() function is called, so we need to return the
|
||||
// first batch of already-loaded comments
|
||||
if (params.key == null) {
|
||||
return LoadResult.Page(commentInfo.comments, null, commentInfo.nextPage)
|
||||
} else {
|
||||
val info = withContext(Dispatchers.IO) {
|
||||
CommentsInfo.getMoreItems(service, commentInfo.url, params.key)
|
||||
}
|
||||
return LoadResult.Page(info.items, null, info.nextPage)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
|
||||
}
|
|
@ -24,6 +24,9 @@ import androidx.core.math.MathUtils;
|
|||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
|
||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||
|
@ -37,9 +40,6 @@ import java.util.function.DoubleConsumer;
|
|||
import java.util.function.DoubleFunction;
|
||||
import java.util.function.DoubleSupplier;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
public class PlaybackParameterDialog extends DialogFragment {
|
||||
private static final String TAG = "PlaybackParameterDialog";
|
||||
|
||||
|
@ -135,7 +135,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
@ -146,7 +146,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||
@Override
|
||||
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater());
|
||||
initUI();
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.schabi.newpipe.settings;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.decodeUrlUtf8;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Activity;
|
||||
|
@ -30,7 +29,6 @@ import org.schabi.newpipe.util.FilePickerActivityHelper;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||
public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true;
|
||||
|
@ -107,28 +105,15 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
|||
|
||||
private void showPathInSummary(final String prefKey, @StringRes final int defaultString,
|
||||
final Preference target) {
|
||||
String rawUri = defaultPreferences.getString(prefKey, null);
|
||||
if (rawUri == null || rawUri.isEmpty()) {
|
||||
final Uri uri = Uri.parse(defaultPreferences.getString(prefKey, ""));
|
||||
if (uri.equals(Uri.EMPTY)) {
|
||||
target.setSummary(getString(defaultString));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rawUri.charAt(0) == File.separatorChar) {
|
||||
target.setSummary(rawUri);
|
||||
return;
|
||||
}
|
||||
if (rawUri.startsWith(ContentResolver.SCHEME_FILE)) {
|
||||
target.setSummary(new File(URI.create(rawUri)).getPath());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
rawUri = decodeUrlUtf8(rawUri);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
target.setSummary(rawUri);
|
||||
final String summary = ContentResolver.SCHEME_FILE.equals(uri.getScheme())
|
||||
? uri.getPath() : uri.toString();
|
||||
target.setSummary(summary);
|
||||
}
|
||||
|
||||
private boolean isFileUri(final String path) {
|
||||
|
|
|
@ -21,7 +21,9 @@ import androidx.fragment.app.FragmentManager;
|
|||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.jakewharton.rxbinding4.widget.RxTextView;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
|
@ -41,9 +43,6 @@ import org.schabi.newpipe.views.FocusOverlayView;
|
|||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 31.08.15.
|
||||
*
|
||||
|
@ -93,7 +92,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
|||
assureCorrectAppLanguage(this);
|
||||
|
||||
super.onCreate(savedInstanceBundle);
|
||||
Icepick.restoreInstanceState(this, savedInstanceBundle);
|
||||
Bridge.restoreInstanceState(this, savedInstanceBundle);
|
||||
final boolean restored = savedInstanceBundle != null;
|
||||
|
||||
final SettingsLayoutBinding settingsLayoutBinding =
|
||||
|
@ -125,7 +124,7 @@ public class SettingsActivity extends AppCompatActivity implements
|
|||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
Bridge.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package org.schabi.newpipe.ui.components.common
|
||||
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
|
||||
@Composable
|
||||
fun DescriptionText(
|
||||
description: Description,
|
||||
modifier: Modifier = Modifier,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = rememberParsedDescription(description),
|
||||
maxLines = maxLines,
|
||||
style = style,
|
||||
overflow = overflow
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberParsedDescription(description: Description): AnnotatedString {
|
||||
// TODO: Handle links and hashtags, Markdown.
|
||||
return remember(description) {
|
||||
if (description.type == Description.HTML) {
|
||||
val styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||
AnnotatedString.fromHtml(description.content, styles)
|
||||
} else {
|
||||
AnnotatedString(description.content)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package org.schabi.newpipe.ui.components.common
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun LoadingIndicator(modifier: Modifier = Modifier) {
|
||||
CircularProgressIndicator(
|
||||
modifier = modifier.fillMaxSize().wrapContentSize(Alignment.Center),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package org.schabi.newpipe.ui.components.common
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import my.nanihadesuka.compose.ScrollbarSettings
|
||||
|
||||
@Composable
|
||||
fun defaultThemedScrollbarSettings(): ScrollbarSettings = ScrollbarSettings.Default.copy(
|
||||
thumbUnselectedColor = MaterialTheme.colorScheme.primary,
|
||||
thumbSelectedColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LazyColumnThemedScrollbar(
|
||||
state: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
settings: ScrollbarSettings = defaultThemedScrollbarSettings(),
|
||||
indicatorContent: (@Composable (index: Int, isThumbSelected: Boolean) -> Unit)? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
my.nanihadesuka.compose.LazyColumnScrollbar(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
settings = settings,
|
||||
indicatorContent = indicatorContent,
|
||||
content = content,
|
||||
)
|
||||
}
|
|
@ -14,15 +14,15 @@ 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
|
||||
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.ktx.findFragmentActivity
|
||||
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||
import org.schabi.newpipe.ui.components.items.playlist.PlaylistListItem
|
||||
import org.schabi.newpipe.ui.components.items.stream.StreamListItem
|
||||
import org.schabi.newpipe.util.DependentPreferenceHelper
|
||||
|
@ -37,7 +37,7 @@ fun ItemList(
|
|||
val context = LocalContext.current
|
||||
val onClick = remember {
|
||||
{ item: InfoItem ->
|
||||
val fragmentManager = (context as FragmentActivity).supportFragmentManager
|
||||
val fragmentManager = context.findFragmentActivity().supportFragmentManager
|
||||
if (item is StreamInfoItem) {
|
||||
NavigationHelper.openVideoDetailFragment(
|
||||
context, fragmentManager, item.serviceId, item.url, item.name, null, false
|
||||
|
@ -72,7 +72,7 @@ fun ItemList(
|
|||
} else {
|
||||
val state = rememberLazyListState()
|
||||
|
||||
LazyColumnScrollbar(state = state) {
|
||||
LazyColumnThemedScrollbar(state = state) {
|
||||
LazyColumn(modifier = nestedScrollModifier, state = state) {
|
||||
listHeader()
|
||||
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.PlaylistPlay
|
||||
import androidx.compose.material3.Icon
|
||||
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
|
||||
|
@ -46,10 +47,10 @@ fun PlaylistThumbnail(
|
|||
.padding(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_playlist_play),
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.PlaylistPlay,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(Color.White),
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
|
||||
|
|
|
@ -8,12 +8,12 @@ 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 androidx.lifecycle.viewmodel.compose.viewModel
|
||||
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.StreamInfoItem
|
||||
import org.schabi.newpipe.ktx.findFragmentActivity
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder
|
||||
|
@ -84,7 +84,7 @@ fun StreamMenu(
|
|||
) { info ->
|
||||
// TODO: Use an AlertDialog composable instead.
|
||||
val downloadDialog = DownloadDialog(context, info)
|
||||
val fragmentManager = (context as FragmentActivity).supportFragmentManager
|
||||
val fragmentManager = context.findFragmentActivity().supportFragmentManager
|
||||
downloadDialog.show(fragmentManager, "downloadDialog")
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ fun StreamMenu(
|
|||
PlaylistDialog.createCorrespondingDialog(context, list) { dialog ->
|
||||
val tag = if (dialog is PlaylistAppendDialog) "append" else "create"
|
||||
dialog.show(
|
||||
(context as FragmentActivity).supportFragmentManager,
|
||||
context.findFragmentActivity().supportFragmentManager,
|
||||
"StreamDialogEntry@${tag}_playlist"
|
||||
)
|
||||
}
|
||||
|
@ -131,7 +131,8 @@ fun StreamMenu(
|
|||
SparseItemUtil.fetchUploaderUrlIfSparse(
|
||||
context, stream.serviceId, stream.url, stream.uploaderUrl
|
||||
) { url ->
|
||||
NavigationHelper.openChannelFragment(context as FragmentActivity, stream, url)
|
||||
val activity = context.findFragmentActivity()
|
||||
NavigationHelper.openChannelFragment(activity, stream, url)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,278 @@
|
|||
package org.schabi.newpipe.ui.components.video.comment
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.filled.ThumbUp
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.ui.components.common.rememberParsedDescription
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.external_communication.copyToClipboardCallback
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun Comment(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
var isExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
var showReplies by rememberSaveable { mutableStateOf(false) }
|
||||
val parsedDescription = rememberParsedDescription(comment.commentText)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.animateContentSize()
|
||||
.combinedClickable(
|
||||
onLongClick = copyToClipboardCallback { parsedDescription },
|
||||
onClick = { isExpanded = !isExpanded },
|
||||
)
|
||||
.padding(start = 8.dp, top = 10.dp, end = 8.dp, bottom = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageStrategy.choosePreferredImage(comment.uploaderAvatars),
|
||||
contentDescription = null,
|
||||
placeholder = painterResource(R.drawable.placeholder_person),
|
||||
error = painterResource(R.drawable.placeholder_person),
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.size(42.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
NavigationHelper.openCommentAuthorIfPresent(context, comment)
|
||||
onCommentAuthorOpened()
|
||||
}
|
||||
)
|
||||
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (comment.isPinned) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PushPin,
|
||||
contentDescription = stringResource(R.string.detail_pinned_comment_view_description),
|
||||
modifier = Modifier
|
||||
.padding(end = 3.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
val nameAndDate = remember(comment) {
|
||||
val date = Localization.relativeTimeOrTextual(
|
||||
context, comment.uploadDate, comment.textualUploadDate
|
||||
)
|
||||
Localization.concatenateStrings(comment.uploaderName, date)
|
||||
}
|
||||
Text(
|
||||
text = nameAndDate,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = parsedDescription,
|
||||
// If the comment is expanded, we display all its content
|
||||
// otherwise we only display the first two lines
|
||||
maxLines = if (isExpanded) Int.MAX_VALUE else 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 6.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(start = 1.dp, top = 6.dp, end = 4.dp, bottom = 6.dp)
|
||||
) {
|
||||
// do not show anything if the like count is unknown
|
||||
if (comment.likeCount >= 0) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ThumbUp,
|
||||
contentDescription = stringResource(R.string.detail_likes_img_view_description),
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.size(20.dp),
|
||||
)
|
||||
Text(
|
||||
text = Localization.likeCount(context, comment.likeCount),
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (comment.isHeartedByUploader) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Favorite,
|
||||
contentDescription = stringResource(R.string.detail_heart_img_view_description),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (comment.replies != null) {
|
||||
// reduce LocalMinimumInteractiveComponentSize from 48dp to 44dp to slightly
|
||||
// reduce the button margin (which is still clickable but not visible)
|
||||
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 44.dp) {
|
||||
TextButton(
|
||||
onClick = { showReplies = true },
|
||||
modifier = Modifier.padding(end = 2.dp)
|
||||
) {
|
||||
val text = pluralStringResource(
|
||||
R.plurals.replies, comment.replyCount, comment.replyCount.toString()
|
||||
)
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showReplies) {
|
||||
CommentRepliesDialog(
|
||||
parentComment = comment,
|
||||
onDismissRequest = { showReplies = false },
|
||||
onCommentAuthorOpened = onCommentAuthorOpened,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun CommentsInfoItem(
|
||||
serviceId: Int = 1,
|
||||
url: String = "",
|
||||
name: String = "",
|
||||
commentText: Description,
|
||||
uploaderName: String,
|
||||
textualUploadDate: String = "5 months ago",
|
||||
likeCount: Int = 0,
|
||||
isHeartedByUploader: Boolean = false,
|
||||
isPinned: Boolean = false,
|
||||
replies: Page? = null,
|
||||
replyCount: Int = 0,
|
||||
) = CommentsInfoItem(serviceId, url, name).apply {
|
||||
this.commentText = commentText
|
||||
this.uploaderName = uploaderName
|
||||
this.textualUploadDate = textualUploadDate
|
||||
this.likeCount = likeCount
|
||||
this.isHeartedByUploader = isHeartedByUploader
|
||||
this.isPinned = isPinned
|
||||
this.replies = replies
|
||||
this.replyCount = replyCount
|
||||
}
|
||||
|
||||
private class CommentPreviewProvider : CollectionPreviewParameterProvider<CommentsInfoItem>(
|
||||
listOf(
|
||||
CommentsInfoItem(
|
||||
commentText = Description("Hello world!\n\nThis line should be hidden by default.", Description.PLAIN_TEXT),
|
||||
uploaderName = "Test",
|
||||
likeCount = 100,
|
||||
isPinned = false,
|
||||
isHeartedByUploader = true,
|
||||
replies = null,
|
||||
replyCount = 0
|
||||
),
|
||||
CommentsInfoItem(
|
||||
commentText = Description("Hello world, long long long text lorem ipsum dolor sit amet!<br><br>This line should be hidden by default.", Description.HTML),
|
||||
uploaderName = "Test",
|
||||
likeCount = 92847,
|
||||
isPinned = true,
|
||||
isHeartedByUploader = false,
|
||||
replies = Page(""),
|
||||
replyCount = 10
|
||||
),
|
||||
CommentsInfoItem(
|
||||
commentText = Description("Hello world, long long long text lorem ipsum dolor sit amet!<br><br>This line should be hidden by default.", Description.HTML),
|
||||
uploaderName = "Test really long long long long lorem ipsum dolor sit amet consectetur",
|
||||
likeCount = 92847,
|
||||
isPinned = true,
|
||||
isHeartedByUploader = true,
|
||||
replies = null,
|
||||
replyCount = 0
|
||||
),
|
||||
CommentsInfoItem(
|
||||
commentText = Description("Short comment", Description.HTML),
|
||||
uploaderName = "Test really long long long long lorem ipsum dolor sit amet consectetur",
|
||||
likeCount = 92847,
|
||||
isPinned = false,
|
||||
isHeartedByUploader = false,
|
||||
replies = Page(""),
|
||||
replyCount = 4283
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun CommentPreview(
|
||||
@PreviewParameter(CommentPreviewProvider::class) commentsInfoItem: CommentsInfoItem
|
||||
) {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Comment(commentsInfoItem) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun CommentListPreview() {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Column {
|
||||
for (comment in CommentPreviewProvider().values) {
|
||||
Comment(comment) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package org.schabi.newpipe.ui.components.video.comment
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
|
||||
@Immutable
|
||||
class CommentInfo(
|
||||
val serviceId: Int,
|
||||
val url: String,
|
||||
val comments: List<CommentsInfoItem>,
|
||||
val nextPage: Page?,
|
||||
val commentCount: Int,
|
||||
val isCommentsDisabled: Boolean
|
||||
) {
|
||||
constructor(commentsInfo: CommentsInfo) : this(
|
||||
commentsInfo.serviceId, commentsInfo.url, commentsInfo.relatedItems, commentsInfo.nextPage,
|
||||
commentsInfo.commentsCount, commentsInfo.isCommentsDisabled
|
||||
)
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
package org.schabi.newpipe.ui.components.video.comment
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.paging.CommentRepliesSource
|
||||
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||
import org.schabi.newpipe.ui.components.common.NoItemsMessage
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun CommentRepliesDialog(
|
||||
parentComment: CommentsInfoItem,
|
||||
onDismissRequest: () -> Unit,
|
||||
onCommentAuthorOpened: () -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val commentsFlow = remember {
|
||||
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
|
||||
CommentRepliesSource(parentComment)
|
||||
}
|
||||
.flow
|
||||
.cachedIn(coroutineScope)
|
||||
}
|
||||
|
||||
CommentRepliesDialog(parentComment, commentsFlow, onDismissRequest, onCommentAuthorOpened)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CommentRepliesDialog(
|
||||
parentComment: CommentsInfoItem,
|
||||
commentsFlow: Flow<PagingData<CommentsInfoItem>>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onCommentAuthorOpened: () -> Unit,
|
||||
) {
|
||||
val comments = commentsFlow.collectAsLazyPagingItems()
|
||||
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
val nestedOnCommentAuthorOpened: () -> Unit = {
|
||||
// also partialExpand any parent dialog
|
||||
onCommentAuthorOpened()
|
||||
coroutineScope.launch {
|
||||
sheetState.partialExpand()
|
||||
}
|
||||
}
|
||||
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
// contentColorFor(MaterialTheme.colorScheme.containerColor), i.e. ModalBottomSheet's
|
||||
// default background color, does not resolve correctly, so need to manually set the
|
||||
// content color for MaterialTheme.colorScheme.background instead
|
||||
LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
LazyColumnThemedScrollbar(state = listState) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
||||
state = listState
|
||||
) {
|
||||
item {
|
||||
CommentRepliesHeader(
|
||||
comment = parentComment,
|
||||
onCommentAuthorOpened = nestedOnCommentAuthorOpened,
|
||||
)
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (parentComment.replyCount >= 0) {
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 12.dp,
|
||||
vertical = 4.dp
|
||||
),
|
||||
text = pluralStringResource(
|
||||
R.plurals.replies,
|
||||
parentComment.replyCount,
|
||||
parentComment.replyCount,
|
||||
),
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (comments.itemCount == 0) {
|
||||
item {
|
||||
val refresh = comments.loadState.refresh
|
||||
if (refresh is LoadState.Loading) {
|
||||
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||
} else {
|
||||
val message = if (refresh is LoadState.Error) {
|
||||
R.string.error_unable_to_load_comments
|
||||
} else {
|
||||
R.string.no_comments
|
||||
}
|
||||
NoItemsMessage(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(comments.itemCount) {
|
||||
Comment(
|
||||
comment = comments[it]!!,
|
||||
onCommentAuthorOpened = nestedOnCommentAuthorOpened,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun CommentRepliesDialogPreview() {
|
||||
val comment = CommentsInfoItem(
|
||||
commentText = Description("Hello world!", Description.PLAIN_TEXT),
|
||||
uploaderName = "Test",
|
||||
likeCount = 100,
|
||||
isPinned = true,
|
||||
isHeartedByUploader = true
|
||||
)
|
||||
val replies = (1..10).map { i ->
|
||||
CommentsInfoItem(
|
||||
commentText = Description(
|
||||
"Reply $i: ${LoremIpsum(i * i).values.first()}",
|
||||
Description.PLAIN_TEXT,
|
||||
),
|
||||
uploaderName = LoremIpsum(11 - i).values.first()
|
||||
)
|
||||
}
|
||||
val flow = flowOf(PagingData.from(replies))
|
||||
|
||||
AppTheme {
|
||||
CommentRepliesDialog(comment, flow, onDismissRequest = {}, onCommentAuthorOpened = {})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package org.schabi.newpipe.ui.components.video.comment
|
||||
|
||||
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.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.filled.ThumbUp
|
||||
import androidx.compose.material3.Icon
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.ui.components.common.DescriptionText
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
|
||||
@Composable
|
||||
fun CommentRepliesHeader(comment: CommentsInfoItem, onCommentAuthorOpened: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
NavigationHelper.openCommentAuthorIfPresent(context, comment)
|
||||
onCommentAuthorOpened()
|
||||
}
|
||||
.weight(1.0f, true),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageStrategy.choosePreferredImage(comment.uploaderAvatars),
|
||||
contentDescription = null,
|
||||
placeholder = painterResource(R.drawable.placeholder_person),
|
||||
error = painterResource(R.drawable.placeholder_person),
|
||||
modifier = Modifier
|
||||
.size(42.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = comment.uploaderName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = Localization.relativeTimeOrTextual(
|
||||
context, comment.uploadDate, comment.textualUploadDate
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// do not show anything if the like count is unknown
|
||||
if (comment.likeCount >= 0) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ThumbUp,
|
||||
contentDescription = stringResource(R.string.detail_likes_img_view_description),
|
||||
)
|
||||
Text(
|
||||
text = Localization.likeCount(context, comment.likeCount),
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
if (comment.isHeartedByUploader) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Favorite,
|
||||
contentDescription = stringResource(R.string.detail_heart_img_view_description),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
if (comment.isPinned) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PushPin,
|
||||
contentDescription = stringResource(R.string.detail_pinned_comment_view_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DescriptionText(
|
||||
description = comment.commentText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
fun CommentRepliesHeaderPreview() {
|
||||
val comment = CommentsInfoItem(
|
||||
commentText = Description(LoremIpsum(50).values.first(), Description.PLAIN_TEXT),
|
||||
uploaderName = "Test really long lorem ipsum dolor sit",
|
||||
likeCount = 1000,
|
||||
isPinned = true,
|
||||
isHeartedByUploader = true
|
||||
)
|
||||
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
CommentRepliesHeader(comment) {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
package org.schabi.newpipe.ui.components.video.comment
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar
|
||||
import org.schabi.newpipe.ui.components.common.LoadingIndicator
|
||||
import org.schabi.newpipe.ui.components.common.NoItemsMessage
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.viewmodels.CommentsViewModel
|
||||
import org.schabi.newpipe.viewmodels.util.Resource
|
||||
|
||||
@Composable
|
||||
fun CommentSection(commentsViewModel: CommentsViewModel = viewModel()) {
|
||||
val state by commentsViewModel.uiState.collectAsStateWithLifecycle()
|
||||
CommentSection(state, commentsViewModel.comments)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CommentSection(
|
||||
uiState: Resource<CommentInfo>,
|
||||
commentsFlow: Flow<PagingData<CommentsInfoItem>>
|
||||
) {
|
||||
val comments = commentsFlow.collectAsLazyPagingItems()
|
||||
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
||||
val state = rememberLazyListState()
|
||||
|
||||
LazyColumnThemedScrollbar(state = state) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
||||
state = state
|
||||
) {
|
||||
when (uiState) {
|
||||
is Resource.Loading -> {
|
||||
item {
|
||||
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
is Resource.Success -> {
|
||||
val commentInfo = uiState.data
|
||||
val count = commentInfo.commentCount
|
||||
|
||||
if (commentInfo.isCommentsDisabled) {
|
||||
item {
|
||||
NoItemsMessage(R.string.comments_are_disabled)
|
||||
}
|
||||
} else if (count == 0) {
|
||||
item {
|
||||
NoItemsMessage(R.string.no_comments)
|
||||
}
|
||||
} else {
|
||||
// do not show anything if the comment count is unknown
|
||||
if (count >= 0) {
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 12.dp, bottom = 4.dp),
|
||||
text = pluralStringResource(R.plurals.comments, count, count),
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (comments.loadState.refresh) {
|
||||
is LoadState.Loading -> {
|
||||
item {
|
||||
LoadingIndicator(modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
is LoadState.Error -> {
|
||||
item {
|
||||
NoItemsMessage(R.string.error_unable_to_load_comments)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
items(comments.itemCount) {
|
||||
Comment(comment = comments[it]!!) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Resource.Error -> {
|
||||
item {
|
||||
NoItemsMessage(R.string.error_unable_to_load_comments)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun CommentSectionLoadingPreview() {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
CommentSection(uiState = Resource.Loading, commentsFlow = flowOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun CommentSectionSuccessPreview() {
|
||||
val comments = listOf(
|
||||
CommentsInfoItem(
|
||||
commentText = Description(
|
||||
"Comment 1\n\nThis line should be hidden by default.",
|
||||
Description.PLAIN_TEXT
|
||||
),
|
||||
uploaderName = "Test",
|
||||
replies = Page(""),
|
||||
replyCount = 10
|
||||
)
|
||||
) + (2..10).map {
|
||||
CommentsInfoItem(
|
||||
commentText = Description("Comment $it", Description.PLAIN_TEXT),
|
||||
uploaderName = "Test"
|
||||
)
|
||||
}
|
||||
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
CommentSection(
|
||||
uiState = Resource.Success(
|
||||
CommentInfo(
|
||||
serviceId = 1, url = "", comments = comments, nextPage = null,
|
||||
commentCount = 10, isCommentsDisabled = false
|
||||
)
|
||||
),
|
||||
commentsFlow = flowOf(PagingData.from(comments))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun CommentSectionErrorPreview() {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
CommentSection(uiState = Resource.Error(RuntimeException()), commentsFlow = flowOf())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.evernote.android.state.StateSaver;
|
||||
import com.livefront.bridge.Bridge;
|
||||
import com.livefront.bridge.SavedStateHandler;
|
||||
import com.livefront.bridge.ViewSavedStateHandler;
|
||||
|
||||
/**
|
||||
* Configures Bridge's state saver.
|
||||
*/
|
||||
public final class BridgeStateSaverInitializer {
|
||||
|
||||
public static void init(final Context context) {
|
||||
Bridge.initialize(
|
||||
context,
|
||||
new SavedStateHandler() {
|
||||
@Override
|
||||
public void saveInstanceState(
|
||||
@NonNull final Object target,
|
||||
@NonNull final Bundle state) {
|
||||
StateSaver.saveInstanceState(target, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreInstanceState(
|
||||
@NonNull final Object target,
|
||||
@Nullable final Bundle state) {
|
||||
StateSaver.restoreInstanceState(target, state);
|
||||
}
|
||||
},
|
||||
new ViewSavedStateHandler() {
|
||||
@NonNull
|
||||
@Override
|
||||
public <T extends View> Parcelable saveInstanceState(
|
||||
@NonNull final T target,
|
||||
@Nullable final Parcelable parentState) {
|
||||
return StateSaver.saveInstanceState(target, parentState);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <T extends View> Parcelable restoreInstanceState(
|
||||
@NonNull final T target,
|
||||
@Nullable final Parcelable state) {
|
||||
return StateSaver.restoreInstanceState(target, state);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private BridgeStateSaverInitializer() {
|
||||
}
|
||||
}
|
|
@ -42,8 +42,6 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
|
@ -146,33 +144,6 @@ public final class ExtractorHelper {
|
|||
listLinkHandler, nextPage));
|
||||
}
|
||||
|
||||
public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
|
||||
final String url,
|
||||
final boolean forceLoad) {
|
||||
checkServiceId(serviceId);
|
||||
return checkCache(forceLoad, serviceId, url, InfoCache.Type.COMMENTS,
|
||||
Single.fromCallable(() ->
|
||||
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage<CommentsInfoItem>> getMoreCommentItems(
|
||||
final int serviceId,
|
||||
final CommentsInfo info,
|
||||
final Page nextPage) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage));
|
||||
}
|
||||
|
||||
public static Single<InfoItemsPage<CommentsInfoItem>> getMoreCommentItems(
|
||||
final int serviceId,
|
||||
final String url,
|
||||
final Page nextPage) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
|
||||
}
|
||||
|
||||
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
|
||||
final String url,
|
||||
final boolean forceLoad) {
|
||||
|
|
|
@ -219,11 +219,6 @@ public final class Localization {
|
|||
deletedCount, shortCount(context, deletedCount));
|
||||
}
|
||||
|
||||
public static String replyCount(@NonNull final Context context, final int replyCount) {
|
||||
return getQuantity(context, R.plurals.replies, 0, replyCount,
|
||||
String.valueOf(replyCount));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context the Android context
|
||||
* @param likeCount the like count, possibly negative if unknown
|
||||
|
|
|
@ -46,10 +46,10 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
|
|||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
|
||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.ktx.ContextKt;
|
||||
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
|
||||
import org.schabi.newpipe.local.feed.FeedFragment;
|
||||
import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
|
||||
|
@ -486,31 +486,23 @@ public final class NavigationHelper {
|
|||
* Opens the comment author channel fragment, if the {@link CommentsInfoItem#getUploaderUrl()}
|
||||
* of {@code comment} is non-null. Shows a UI-error snackbar if something goes wrong.
|
||||
*
|
||||
* @param activity the activity with the fragment manager and in which to show the snackbar
|
||||
* @param context the context to use for opening the fragment
|
||||
* @param comment the comment whose uploader/author will be opened
|
||||
*/
|
||||
public static void openCommentAuthorIfPresent(@NonNull final FragmentActivity activity,
|
||||
public static void openCommentAuthorIfPresent(@NonNull final Context context,
|
||||
@NonNull final CommentsInfoItem comment) {
|
||||
if (isEmpty(comment.getUploaderUrl())) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final var activity = ContextKt.findFragmentActivity(context);
|
||||
openChannelFragment(activity.getSupportFragmentManager(), comment.getServiceId(),
|
||||
comment.getUploaderUrl(), comment.getUploaderName());
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(activity, "Opening channel fragment", e);
|
||||
ErrorUtil.showUiErrorSnackbar(context, "Opening channel fragment", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void openCommentRepliesFragment(@NonNull final FragmentActivity activity,
|
||||
@NonNull final CommentsInfoItem comment) {
|
||||
defaultTransaction(activity.getSupportFragmentManager())
|
||||
.replace(R.id.fragment_holder, new CommentRepliesFragment(comment),
|
||||
CommentRepliesFragment.TAG)
|
||||
.addToBackStack(CommentRepliesFragment.TAG)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static void openPlaylistFragment(final FragmentManager fragmentManager,
|
||||
final int serviceId, final String url,
|
||||
@NonNull final String name) {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package org.schabi.newpipe.util.external_communication
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import org.schabi.newpipe.R
|
||||
|
||||
fun ClipboardManager.setTextAndShowToast(context: Context, annotatedString: AnnotatedString) {
|
||||
setText(annotatedString)
|
||||
if (Build.VERSION.SDK_INT < 33) {
|
||||
// Android 13 has its own "copied to clipboard" dialog
|
||||
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun copyToClipboardCallback(annotatedString: () -> AnnotatedString): (() -> Unit) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
return {
|
||||
clipboardManager.setTextAndShowToast(context, annotatedString())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package org.schabi.newpipe.viewmodels
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||
import org.schabi.newpipe.paging.CommentsSource
|
||||
import org.schabi.newpipe.ui.components.video.comment.CommentInfo
|
||||
import org.schabi.newpipe.util.KEY_URL
|
||||
import org.schabi.newpipe.viewmodels.util.Resource
|
||||
|
||||
class CommentsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
|
||||
val uiState = savedStateHandle.getStateFlow(KEY_URL, "")
|
||||
.map {
|
||||
try {
|
||||
Resource.Success(CommentInfo(CommentsInfo.getInfo(it)))
|
||||
} catch (e: Exception) {
|
||||
Resource.Error(e)
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Resource.Loading)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val comments = uiState
|
||||
.filterIsInstance<Resource.Success<CommentInfo>>()
|
||||
.flatMapLatest {
|
||||
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
|
||||
CommentsSource(it.data)
|
||||
}.flow
|
||||
}
|
||||
.cachedIn(viewModelScope)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package org.schabi.newpipe.viewmodels.util
|
||||
|
||||
sealed class Resource<out T> {
|
||||
data object Loading : Resource<Nothing>()
|
||||
class Success<T>(val data: T) : Resource<T>()
|
||||
class Error(val throwable: Throwable) : Resource<Nothing>()
|
||||
}
|
|
@ -19,6 +19,9 @@
|
|||
|
||||
package org.schabi.newpipe.views;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.os.Parcelable;
|
||||
|
@ -29,18 +32,15 @@ import android.widget.LinearLayout;
|
|||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.evernote.android.state.State;
|
||||
import com.livefront.bridge.Bridge;
|
||||
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
/**
|
||||
* A view that can be fully collapsed and expanded.
|
||||
*/
|
||||
|
@ -207,12 +207,12 @@ public class CollapsibleView extends LinearLayout {
|
|||
@Nullable
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
return Icepick.saveInstanceState(this, super.onSaveInstanceState());
|
||||
return Bridge.saveInstanceState(this, super.onSaveInstanceState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(final Parcelable state) {
|
||||
super.onRestoreInstanceState(Icepick.restoreInstanceState(this, state));
|
||||
super.onRestoreInstanceState(Bridge.restoreInstanceState(this, state));
|
||||
|
||||
ready();
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../layout/list_stream_item.xml
|
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/itemRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/video_item_search_padding">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemThumbnailView"
|
||||
android:layout_width="@dimen/video_item_search_thumbnail_image_width"
|
||||
android:layout_height="@dimen/video_item_search_thumbnail_image_height"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/placeholder_thumbnail_video"
|
||||
app:layout_constraintBottom_toTopOf="@+id/itemProgressView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/itemDurationView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/video_item_search_duration_margin"
|
||||
android:layout_marginBottom="@dimen/video_item_search_duration_margin"
|
||||
android:background="@color/duration_background_color"
|
||||
android:paddingHorizontal="@dimen/video_item_search_duration_horizontal_padding"
|
||||
android:paddingVertical="@dimen/video_item_search_duration_vertical_padding"
|
||||
android:textAllCaps="true"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="@color/duration_text_color"
|
||||
android:textSize="@dimen/video_item_search_duration_text_size"
|
||||
app:layout_constraintBottom_toBottomOf="@id/itemThumbnailView"
|
||||
app:layout_constraintRight_toRightOf="@id/itemThumbnailView"
|
||||
tools:text="1:09:10" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/itemVideoTitleView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/video_item_search_image_right_margin"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textSize="@dimen/video_item_search_title_text_size"
|
||||
app:layout_constraintBottom_toTopOf="@+id/itemUploaderView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemThumbnailView"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/itemUploaderView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textSize="@dimen/video_item_search_uploader_text_size"
|
||||
app:layout_constraintBottom_toTopOf="@+id/itemAdditionalDetails"
|
||||
app:layout_constraintEnd_toEndOf="@+id/itemVideoTitleView"
|
||||
app:layout_constraintStart_toStartOf="@+id/itemVideoTitleView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemVideoTitleView"
|
||||
tools:text="Uploader" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/itemAdditionalDetails"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textSize="@dimen/video_item_search_upload_date_text_size"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/itemVideoTitleView"
|
||||
app:layout_constraintStart_toStartOf="@+id/itemVideoTitleView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemUploaderView"
|
||||
tools:text="2 years ago • 10M views" />
|
||||
|
||||
<org.schabi.newpipe.views.AnimatedProgressBar
|
||||
android:id="@+id/itemProgressView"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="4dp"
|
||||
android:progressDrawable="?progress_horizontal_drawable"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/itemThumbnailView"
|
||||
app:layout_constraintStart_toStartOf="@+id/itemThumbnailView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemThumbnailView" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,137 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/authorAvatar"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:focusable="false"
|
||||
android:src="@drawable/placeholder_person"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearance="@style/CircularImageView"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/authorName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/uploadDate"
|
||||
app:layout_constraintEnd_toStartOf="@+id/thumbsUpImage"
|
||||
app:layout_constraintStart_toEndOf="@+id/authorAvatar"
|
||||
app:layout_constraintTop_toTopOf="@+id/authorAvatar"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/uploadDate"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
|
||||
app:layout_constraintEnd_toStartOf="@+id/thumbsUpImage"
|
||||
app:layout_constraintStart_toEndOf="@+id/authorAvatar"
|
||||
app:layout_constraintTop_toBottomOf="@+id/authorName"
|
||||
tools:text="5 months ago" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbsUpImage"
|
||||
android:layout_width="21sp"
|
||||
android:layout_height="21sp"
|
||||
android:layout_marginEnd="@dimen/video_item_detail_like_margin"
|
||||
android:contentDescription="@string/detail_likes_img_view_description"
|
||||
android:src="@drawable/ic_thumb_up"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
|
||||
app:layout_constraintEnd_toStartOf="@+id/thumbsUpCount"
|
||||
app:layout_constraintTop_toTopOf="@+id/authorAvatar" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/thumbsUpCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center"
|
||||
android:lines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
|
||||
app:layout_constraintEnd_toStartOf="@+id/heartImage"
|
||||
app:layout_constraintTop_toTopOf="@+id/authorAvatar"
|
||||
tools:text="12M" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/heartImage"
|
||||
android:layout_width="21sp"
|
||||
android:layout_height="21sp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:contentDescription="@string/detail_heart_img_view_description"
|
||||
android:src="@drawable/ic_heart"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
|
||||
app:layout_constraintEnd_toStartOf="@+id/pinnedImage"
|
||||
app:layout_constraintTop_toTopOf="@+id/authorAvatar"
|
||||
app:layout_goneMarginEnd="16dp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/authorTouchArea"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="8dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
app:layout_constraintBottom_toTopOf="@+id/commentContent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/thumbsUpImage"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pinnedImage"
|
||||
android:layout_width="21sp"
|
||||
android:layout_height="21sp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/detail_pinned_comment_view_description"
|
||||
android:src="@drawable/ic_pin"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/authorAvatar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/authorAvatar" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/commentContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/authorAvatar"
|
||||
tools:text="@tools:sample/lorem/random[10]" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="?attr/separator_color"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/commentContent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,71 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeRecyclerView
|
||||
android:id="@+id/items_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
tools:listitem="@layout/list_comment_item" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading_progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/empty_state_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="85dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:fontFamily="monospace"
|
||||
android:text="(╯°-°)╯"
|
||||
android:textSize="35sp"
|
||||
tools:ignore="HardcodedText,UnusedAttribute" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/empty_state_desc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/empty_view_no_comments"
|
||||
android:textSize="24sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!--ERROR PANEL-->
|
||||
<include
|
||||
android:id="@+id/error_panel"
|
||||
layout="@layout/error_panel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="?attr/toolbar_shadow"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,104 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/itemRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/comments_vertical_padding">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/itemThumbnailView"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginEnd="@dimen/comment_item_avatar_right_margin"
|
||||
android:focusable="false"
|
||||
android:src="@drawable/placeholder_person"
|
||||
app:shapeAppearance="@style/CircularImageView"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/detail_pinned_view"
|
||||
android:layout_width="@dimen/video_item_detail_pinned_image_width"
|
||||
android:layout_height="@dimen/video_item_detail_pinned_image_height"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginEnd="@dimen/video_item_detail_pinned_right_margin"
|
||||
android:layout_toEndOf="@+id/itemThumbnailView"
|
||||
android:contentDescription="@string/detail_pinned_comment_view_description"
|
||||
android:src="@drawable/ic_pin" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/itemTitleView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toEndOf="@+id/detail_pinned_view"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textSize="@dimen/comment_item_title_text_size"
|
||||
tools:text="Author Name, Lorem ipsum • 5 months ago" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/itemCommentContentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/itemTitleView"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_toEndOf="@+id/itemThumbnailView"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textSize="@dimen/comment_item_content_text_size"
|
||||
tools:text="@tools:sample/lorem/random[1]" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/detail_thumbs_up_img_view"
|
||||
android:layout_width="@dimen/video_item_detail_like_image_width"
|
||||
android:layout_height="@dimen/video_item_detail_like_image_height"
|
||||
android:layout_below="@id/itemCommentContentView"
|
||||
android:layout_alignBottom="@+id/replies_button"
|
||||
android:layout_toEndOf="@+id/itemThumbnailView"
|
||||
android:contentDescription="@string/detail_likes_img_view_description"
|
||||
android:src="@drawable/ic_thumb_up" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/detail_thumbs_up_count_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@id/detail_thumbs_up_img_view"
|
||||
android:layout_alignBottom="@id/detail_thumbs_up_img_view"
|
||||
android:layout_marginStart="@dimen/video_item_detail_like_margin"
|
||||
android:layout_toEndOf="@id/detail_thumbs_up_img_view"
|
||||
android:gravity="center"
|
||||
android:lines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textSize="@dimen/video_item_detail_likes_text_size"
|
||||
tools:text="12M" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/detail_heart_image_view"
|
||||
android:layout_width="@dimen/video_item_detail_heart_image_size"
|
||||
android:layout_height="@dimen/video_item_detail_heart_image_size"
|
||||
android:layout_alignTop="@id/detail_thumbs_up_img_view"
|
||||
android:layout_alignBottom="@id/detail_thumbs_up_img_view"
|
||||
android:layout_marginStart="@dimen/video_item_detail_heart_margin"
|
||||
android:layout_toEndOf="@+id/detail_thumbs_up_count_view"
|
||||
android:contentDescription="@string/detail_heart_img_view_description"
|
||||
android:src="@drawable/ic_heart" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/replies_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/itemCommentContentView"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="@dimen/video_item_detail_heart_margin"
|
||||
android:minHeight="0dp"
|
||||
tools:text="543 replies" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -857,4 +857,8 @@
|
|||
<string name="show_less">Show less</string>
|
||||
<string name="import_settings_vulnerable_format">The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore.</string>
|
||||
<string name="auto_queue_description">Next</string>
|
||||
<plurals name="comments">
|
||||
<item quantity="one">%d comment</item>
|
||||
<item quantity="other">%d comments</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
buildscript {
|
||||
ext {
|
||||
kotlin_version = '2.0.0'
|
||||
kotlin_version = '2.0.21'
|
||||
about_libs = '11.2.2'
|
||||
}
|
||||
repositories {
|
||||
|
@ -11,7 +11,7 @@ buildscript {
|
|||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.0'
|
||||
classpath 'com.android.tools.build:gradle:8.7.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1'
|
||||
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.enableJetifier=false
|
||||
android.nonFinalResIds=false
|
||||
android.nonTransitiveRClass=true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
Loading…
Reference in New Issue