diff --git a/app/build.gradle b/app/build.gradle index c1c66ddc..18448f8f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,6 +30,7 @@ android { release { debuggable false + minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } @@ -99,6 +100,7 @@ dependencies { implementation 'com.squareup.moshi:moshi:1.15.0' implementation 'com.google.code.gson:gson:2.10' implementation 'com.github.Blatzar:NiceHttp:0.4.4' + implementation 'org.mozilla:rhino:1.7.14' // Aniyomi implementation 'io.reactivex:rxjava:1.3.8' diff --git a/app/src/main/java/com/mrboomdev/awery/catalog/provider/AywaExtensionProvider.java b/app/src/main/java/com/mrboomdev/awery/catalog/provider/AywaExtensionProvider.java deleted file mode 100644 index 8e35a565..00000000 --- a/app/src/main/java/com/mrboomdev/awery/catalog/provider/AywaExtensionProvider.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.mrboomdev.awery.catalog.provider; - -import java.io.File; - -public class AywaExtensionProvider extends ExtensionProvider { - - public AywaExtensionProvider(File directory) { - - } - - @Override - public String getName() { - return null; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/mrboomdev/awery/catalog/provider/JsExtensionProvider.java b/app/src/main/java/com/mrboomdev/awery/catalog/provider/JsExtensionProvider.java new file mode 100644 index 00000000..8471d81b --- /dev/null +++ b/app/src/main/java/com/mrboomdev/awery/catalog/provider/JsExtensionProvider.java @@ -0,0 +1,29 @@ +package com.mrboomdev.awery.catalog.provider; + +import androidx.annotation.NonNull; + +import com.mrboomdev.awery.catalog.template.CatalogEpisode; +import com.mrboomdev.awery.catalog.template.CatalogMedia; + +import org.mozilla.javascript.Context; + +import java.util.List; + +public class JsExtensionProvider extends ExtensionProvider { + + @Override + public void getEpisodes(int page, CatalogMedia media, @NonNull ResponseCallback> callback) { + super.getEpisodes(page, media, callback); + } + + public JsExtensionProvider(String script) { + var context = Context.enter(); + var scope = context.initSafeStandardObjects(); + context.evaluateString(scope, script, null, 1,null); + } + + @Override + public String getName() { + return "JsExtensionProvider"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mrboomdev/awery/ui/activity/PlayerActivity.java b/app/src/main/java/com/mrboomdev/awery/ui/activity/PlayerActivity.java index 9d5429e4..da6e3644 100644 --- a/app/src/main/java/com/mrboomdev/awery/ui/activity/PlayerActivity.java +++ b/app/src/main/java/com/mrboomdev/awery/ui/activity/PlayerActivity.java @@ -1,7 +1,9 @@ package com.mrboomdev.awery.ui.activity; import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; import android.os.Bundle; +import android.view.MotionEvent; import android.view.View; import androidx.annotation.NonNull; @@ -17,23 +19,37 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.ui.AspectRatioFrameLayout; +import androidx.media3.ui.TimeBar; import com.bumptech.glide.Glide; -import com.google.android.material.slider.Slider; import com.mrboomdev.awery.AweryApp; import com.mrboomdev.awery.ui.ThemeManager; +import com.mrboomdev.awery.util.CallbackUtil; +import com.mrboomdev.awery.util.ui.ViewUtil; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import ani.awery.R; import ani.awery.databinding.LayoutActivityPlayerBinding; public class PlayerActivity extends AppCompatActivity implements Player.Listener { + private final int SHOW_UI_AFTER_MILLIS = 200; + private final int UI_INSETS = WindowInsetsCompat.Type.displayCutout() + | WindowInsetsCompat.Type.systemGestures() + | WindowInsetsCompat.Type.statusBars() + | WindowInsetsCompat.Type.navigationBars(); + private final List buttons = new ArrayList<>(); private LayoutActivityPlayerBinding binding; - private Runnable hideUiRunnable; - private boolean isVideoPaused; + private CallbackUtil.Callback1 hideUiRunnable; + private Runnable hideUiRunnableWrapper, showUiRunnableFromLeft, showUiRunnableFromRight; + private boolean areButtonsClickable, isSliderDragging; + private boolean isVideoPaused, isVideoBuffering = true; + private int forwardFastClicks, backwardFastClicks; private ExoPlayer player; + @SuppressLint("ClickableViewAccessibility") @Override @OptIn(markerClass = UnstableApi.class) protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -41,12 +57,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = LayoutActivityPlayerBinding.inflate(getLayoutInflater()); - toggleUiElementsClickability(false); setContentView(binding.getRoot()); - - var controller = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); - controller.hide(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); - controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + applyFullscreen(); binding.aspectRatioFrame.setAspectRatio(16f / 9f); binding.aspectRatioFrame.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); @@ -59,109 +71,212 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { player.addListener(this); player.setMediaItem(item); - binding.pause.setOnClickListener(view -> { - if(!isVideoPaused) { - Glide.with(this).load(R.drawable.anim_pause_to_play).into(binding.pause); - player.pause(); + binding.title.setText(getIntent().getStringExtra("title")); - if(hideUiRunnable != null) { - AweryApp.cancelDelayed(hideUiRunnable); - } - } else { - Glide.with(this).load(R.drawable.anim_play_to_pause).into(binding.pause); - player.play(); + binding.doubleTapBackward.setOnClickListener(view -> { + backwardFastClicks++; - if(hideUiRunnable != null) { - AweryApp.runDelayed(hideUiRunnable, 3_000); + if(backwardFastClicks >= 2) { + if(showUiRunnableFromLeft != null) { + AweryApp.cancelDelayed(showUiRunnableFromLeft); + showUiRunnableFromLeft = null; } + + binding.doubleTapBackward.setBackgroundResource(R.drawable.ripple_circle_white); + player.seekTo(player.getCurrentPosition() - 10_000); + updateTimers(); + } else { + showUiRunnableFromLeft = this::toggleUiVisibility; + AweryApp.runDelayed(showUiRunnableFromLeft, SHOW_UI_AFTER_MILLIS); } - isVideoPaused = !isVideoPaused; - }); + AweryApp.runDelayed(() -> { + backwardFastClicks--; - binding.slider.addOnChangeListener((slider, value, fromUser) -> { - if(!fromUser) return; - player.seekTo((long) value * 1000); + if(backwardFastClicks == 0) { + binding.doubleTapBackward.setBackground(null); + } + }, 500); }); - binding.slider.setLabelFormatter(value -> { - var hours = (int) value / 3600; + binding.doubleTapForward.setOnClickListener(view -> { + forwardFastClicks++; + + if(forwardFastClicks >= 2) { + if(showUiRunnableFromRight != null) { + AweryApp.cancelDelayed(showUiRunnableFromRight); + showUiRunnableFromRight = null; + } - if(hours >= 1) { - return String.format(Locale.ENGLISH, "%02d:%02d:%02d", - hours, (int) value / 60, (int) value % 60); + binding.doubleTapForward.setBackgroundResource(R.drawable.ripple_circle_white); + player.seekTo(player.getCurrentPosition() + 10_000); + updateTimers(); + } else { + showUiRunnableFromRight = this::toggleUiVisibility; + AweryApp.runDelayed(showUiRunnableFromRight, SHOW_UI_AFTER_MILLIS); } - return String.format(Locale.ENGLISH, "%02d:%02d", - (int) value / 60, (int) value % 60); + AweryApp.runDelayed(() -> { + forwardFastClicks--; + + if(forwardFastClicks == 0) { + binding.doubleTapForward.setBackground(null); + } + }, 500); }); - binding.slider.addOnSliderTouchListener(new Slider.OnSliderTouchListener() { + binding.slider.addListener(new TimeBar.OnScrubListener() { @Override - public void onStartTrackingTouch(@NonNull Slider slider) { + public void onScrubStart(@NonNull TimeBar timeBar, long position) { + isSliderDragging = true; + + player.seekTo(position); + updateTimers(); + player.pause(); } @Override - public void onStopTrackingTouch(@NonNull Slider slider) { + public void onScrubMove(@NonNull TimeBar timeBar, long position) { + player.seekTo(position); + updateTimers(); + } + + @Override + public void onScrubStop(@NonNull TimeBar timeBar, long position, boolean canceled) { + isSliderDragging = false; + + player.seekTo(position); + updateTimers(); + if(!isVideoPaused) { player.play(); + + if(hideUiRunnable != null) { + hideUiRunnable.run(false); + } } } }); - binding.uiOverlay.setOnClickListener(view -> { - if(hideUiRunnable != null) { - AweryApp.cancelDelayed(hideUiRunnable); - hideUiRunnable.run(); - hideUiRunnable = null; - return; - } - - toggleUiElementsClickability(true); - ObjectAnimator.ofFloat(binding.uiOverlay, "alpha", 0, 1).start(); - - hideUiRunnable = () -> { - toggleUiElementsClickability(false); - ObjectAnimator.ofFloat(binding.uiOverlay, "alpha", 1, 0).start(); - hideUiRunnable = null; - }; + binding.getRoot().setOnTouchListener((view, event) -> { + if(event.getAction() == MotionEvent.ACTION_MOVE) return true; + if(event.getAction() == MotionEvent.ACTION_DOWN) return true; - AweryApp.runDelayed(hideUiRunnable, 3_000); + toggleUiVisibility(); + return true; }); Runnable updateProgress = new Runnable() { @Override public void run() { if(isDestroyed()) return; - - binding.slider.setValue(player.getCurrentPosition() / 1000f); + updateTimers(); AweryApp.runDelayed(this, 1_000); } }; AweryApp.runDelayed(updateProgress, 1_000); + setButtonsClickability(false); + binding.getRoot().performClick(); + + setupButton(binding.exit, this::finish); + + setupButton(binding.quickSkip, () -> { + player.seekTo(player.getCurrentPosition() + 60_000); + updateTimers(); + }); + + setupButton(binding.pause, () -> { + if(isVideoBuffering) return; + + if(!isVideoPaused) { + Glide.with(this).load(R.drawable.anim_pause_to_play).into(binding.pause); + player.pause(); + + if(hideUiRunnableWrapper != null) { + AweryApp.cancelDelayed(hideUiRunnableWrapper); + } + } else { + Glide.with(this).load(R.drawable.anim_play_to_pause).into(binding.pause); + player.play(); + + if(hideUiRunnableWrapper != null) { + AweryApp.runDelayed(hideUiRunnableWrapper, 3_000); + } + } + + isVideoPaused = !isVideoPaused; + }); + + toggleUiVisibility(); } - private void toggleUiElementsClickability(boolean isClickable) { - binding.pause.setClickable(isClickable); + private void toggleUiVisibility() { + if(hideUiRunnableWrapper != null) { + AweryApp.cancelDelayed(hideUiRunnableWrapper); + hideUiRunnable.run(true); + setHideUiRunnable(null); + return; + } + + setButtonsClickability(true); + ObjectAnimator.ofFloat(binding.uiOverlay, "alpha", 0, 1).start(); + ObjectAnimator.ofFloat(binding.darkOverlay, "alpha", 0, .6f).start(); + + setHideUiRunnable(isForced -> { + if(!isForced && (isVideoPaused || isSliderDragging)) return; + + setButtonsClickability(false); + ObjectAnimator.ofFloat(binding.uiOverlay, "alpha", 1, 0).start(); + ObjectAnimator.ofFloat(binding.darkOverlay, "alpha", .6f, 0).start(); + setHideUiRunnable(null); + }); + + AweryApp.runDelayed(hideUiRunnableWrapper, 3_000); + } + + @SuppressLint("ClickableViewAccessibility") + private void setupButton(@NonNull View view, Runnable clickListener) { + buttons.add(view); + + view.setOnClickListener(v -> { + if(!areButtonsClickable) { + return; + } + + clickListener.run(); + }); + } + + @OptIn(markerClass = UnstableApi.class) + private void setButtonsClickability(boolean isClickable) { + this.areButtonsClickable = isClickable; binding.slider.setEnabled(isClickable); + + for(var view : buttons) { + view.setClickable(isClickable); + } } @Override + @OptIn(markerClass = UnstableApi.class) public void onPlaybackStateChanged(int playbackState) { switch(playbackState) { case Player.STATE_READY -> { - var seconds = player.getDuration() / 1000L; - binding.slider.setValueTo(seconds); + isVideoBuffering = false; + + binding.slider.setDuration(player.getDuration()); binding.loadingCircle.setVisibility(View.GONE); binding.pause.setVisibility(View.VISIBLE); } case Player.STATE_BUFFERING -> { + isVideoBuffering = true; + binding.loadingCircle.setVisibility(View.VISIBLE); - binding.pause.setVisibility(View.GONE); + binding.pause.setVisibility(View.INVISIBLE); } case Player.STATE_ENDED -> finish(); @@ -177,6 +292,12 @@ public void onPlayerError(@NonNull PlaybackException e) { AweryApp.toast(switch(e.errorCode) { case PlaybackException.ERROR_CODE_TIMEOUT -> "Connection timeout has occurred, please try again later"; case PlaybackException.ERROR_CODE_DECODING_FAILED -> "Video decoding failed, please try again later"; + case PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND -> "Video not found, please try again later"; + + case PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, + PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED -> + "Connection error has occurred, please try again later"; + default -> "Unknown error has occurred, please try again later"; }); @@ -195,7 +316,10 @@ protected void onStart() { @Override protected void onResume() { super.onResume(); - player.play(); + + if(!isVideoPaused) { + player.play(); + } } @Override @@ -211,4 +335,50 @@ protected void onDestroy() { player.release(); player = null; } + + @SuppressLint("SetTextI18n") + @OptIn(markerClass = UnstableApi.class) + private void updateTimers() { + binding.slider.setPosition(player.getCurrentPosition()); + binding.timer.setText(formatTime(player.getCurrentPosition()) + "/" + formatTime(player.getDuration())); + } + + @NonNull + private String formatTime(long value) { + value /= 1000; + + var hours = (int) value / 3600; + + if(hours >= 1) { + return String.format(Locale.ENGLISH, "%02d:%02d:%02d", + hours, (int) value / 60, (int) value % 60); + } + + return String.format(Locale.ENGLISH, "%02d:%02d", + (int) value / 60, (int) value % 60); + } + + private void applyFullscreen() { + var controller = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); + controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + controller.hide(UI_INSETS); + + ViewUtil.setOnApplyInsetsListener(binding.uiOverlay, insets -> { + var systemInsets = insets.getInsets(UI_INSETS); + var margin = ViewUtil.dpPx(10); + + ViewUtil.setLeftMargin(binding.exit, margin + systemInsets.left); + ViewUtil.setRightMargin(binding.settings, systemInsets.right + margin); + + ViewUtil.setBottomMargin(binding.slider, systemInsets.bottom + margin); + ViewUtil.setHorizontalMargin(binding.slider, margin + systemInsets.left, margin + systemInsets.right); + }); + } + + private void setHideUiRunnable(CallbackUtil.Callback1 runnable) { + hideUiRunnable = runnable; + + hideUiRunnableWrapper = (runnable != null) + ? (() -> runnable.run(false)) : null; + } } \ No newline at end of file diff --git a/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaPlayFragment.java b/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaPlayFragment.java index 43aa8b7f..17fb507e 100644 --- a/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaPlayFragment.java +++ b/app/src/main/java/com/mrboomdev/awery/ui/fragments/MediaPlayFragment.java @@ -55,11 +55,11 @@ public class MediaPlayFragment extends Fragment implements MediaPlayEpisodesAdap private final String TAG = "MediaPlayFragment"; private final Map sourceStatuses = new HashMap<>(); private ArrayList>> groupedByLangEntries; - private CatalogMedia media; - private MediaPlayEpisodesAdapter episodesAdapter; - private ExtensionProvider selectedSource; private SingleViewAdapter.BindingSingleViewAdapter placeholderAdapter; private SingleViewAdapter.BindingSingleViewAdapter variantsAdapter; + private MediaPlayEpisodesAdapter episodesAdapter; + private ExtensionProvider selectedSource; + private CatalogMedia media; private boolean autoChangeSource = true; private int currentSourceIndex = 0; @@ -102,8 +102,8 @@ public void onEpisodeSelected(@NonNull CatalogEpisode episode) { selectedSource.getVideos(episode, new ExtensionProvider.ResponseCallback<>() { @Override public void onSuccess(List catalogVideos) { - Context context; var video = catalogVideos.get(0); + Context context; try { context = requireContext(); @@ -113,6 +113,7 @@ public void onSuccess(List catalogVideos) { var intent = new Intent(context, PlayerActivity.class); intent.putExtra("url", video.getUrl()); + intent.putExtra("title", episode.getTitle()); intent.putExtra("headers", video.getHeaders()); startActivity(intent); } @@ -226,6 +227,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat binding.sourceDropdown.setOnItemClickListener((parent, _view, position, id) -> { var group = groupedByLangEntries.get(position); selectProvider(group, _view.getContext()); + autoChangeSource = false; }); }); diff --git a/app/src/main/java/com/mrboomdev/awery/util/CallbackUtil.java b/app/src/main/java/com/mrboomdev/awery/util/CallbackUtil.java new file mode 100644 index 00000000..9c7d9e7a --- /dev/null +++ b/app/src/main/java/com/mrboomdev/awery/util/CallbackUtil.java @@ -0,0 +1,10 @@ +package com.mrboomdev.awery.util; + +public class CallbackUtil { + + private CallbackUtil() {} + + public interface Callback1 { + void run(T t); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mrboomdev/awery/util/ui/ViewUtil.java b/app/src/main/java/com/mrboomdev/awery/util/ui/ViewUtil.java index bdd196cd..26ca8a09 100644 --- a/app/src/main/java/com/mrboomdev/awery/util/ui/ViewUtil.java +++ b/app/src/main/java/com/mrboomdev/awery/util/ui/ViewUtil.java @@ -61,6 +61,15 @@ public static void setHorizontalMargin(View view, int margin) { view.setLayoutParams(margins); } + public static void setHorizontalMargin(View view, int left, int right) { + var margins = getMargins(view); + if(margins == null) return; + + margins.leftMargin = left; + margins.rightMargin = right; + view.setLayoutParams(margins); + } + public static void setLeftPadding(@NonNull View view, int padding) { view.setPadding(padding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); } diff --git a/app/src/main/res/drawable/outline_fast_forward_24.xml b/app/src/main/res/drawable/outline_fast_forward_24.xml new file mode 100644 index 00000000..40a4d118 --- /dev/null +++ b/app/src/main/res/drawable/outline_fast_forward_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ripple_circle_white.xml b/app/src/main/res/drawable/ripple_circle_white.xml new file mode 100644 index 00000000..8d2218c6 --- /dev/null +++ b/app/src/main/res/drawable/ripple_circle_white.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_activity_player.xml b/app/src/main/res/layout/layout_activity_player.xml index 10179726..0d3ff64a 100644 --- a/app/src/main/res/layout/layout_activity_player.xml +++ b/app/src/main/res/layout/layout_activity_player.xml @@ -20,43 +20,197 @@ - + android:background="#000" /> + + - - + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintDimensionRatio="H,1" /> - + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintDimensionRatio="H,1" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file