diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index b09593c1739..dd2cef04054 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -122,6 +122,7 @@
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.CoilHelper;
+import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import java.util.stream.IntStream;
@@ -263,6 +264,9 @@ public final class Player implements PlaybackListener, Listener {
@NonNull
private final HistoryRecordManager recordManager;
+ java.util.Timer sleepTimer;
+ java.time.Instant sleepTimerEnd;
+
/*//////////////////////////////////////////////////////////////////////////
// Constructor
@@ -2262,6 +2266,44 @@ public void setAudioTrack(@Nullable final String audioTrackId) {
}
+ public void setSleepTimer(@Nullable final long sleepMinutes) {
+ if (sleepTimer != null) {
+ sleepTimer.cancel();
+ sleepTimer.purge();
+ sleepTimer = null;
+ }
+
+ sleepTimerEnd = java.time.Instant.now().plus(sleepMinutes, ChronoUnit.MINUTES);
+
+ sleepTimer = new java.util.Timer();
+ //final Player thisPlayer = this;
+ final java.util.TimerTask task = new java.util.TimerTask() {
+
+
+ @Override
+ public void run() {
+ if (java.time.Instant.now().compareTo(sleepTimerEnd) >= 0) {
+ UIs.call(playerUi -> playerUi.onSleepTimerUpdate(0));
+ cancelSleepTimer();
+ return;
+ }
+
+ final long remainingMinutes = java.time.Instant.now().until(sleepTimerEnd,
+ ChronoUnit.MINUTES);
+ UIs.call(playerUi -> playerUi.onSleepTimerUpdate(remainingMinutes + 1));
+ }
+ };
+ sleepTimer.schedule(task, 1000, 1000);
+ }
+
+ public void cancelSleepTimer() {
+ if (sleepTimer != null) {
+ sleepTimer.cancel();
+ sleepTimer.purge();
+ sleepTimer = null;
+ }
+ }
+
@NonNull
public Context getContext() {
return context;
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
index 57e2ec2a2cf..418436b2d26 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
@@ -209,4 +209,11 @@ public void onPlayQueueEdited() {
*/
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
}
+
+ /**
+ * @param remainingTime the remaining sleep timer time, set to 0 to pause the player and
+ * disable the sleep timer
+ */
+ public void onSleepTimerUpdate(final long remainingTime) {
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
index e96873de52c..91d9400932c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
@@ -128,12 +128,14 @@ private enum PlayButtonAction {
private static final int POPUP_MENU_ID_AUDIO_TRACK = 70;
private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
private static final int POPUP_MENU_ID_CAPTION = 89;
+ private static final int POPUP_MENU_ID_SLEEP_TIMER = 90; // TODO is 90 still available?
protected boolean isSomePopupMenuVisible = false;
private PopupMenu qualityPopupMenu;
private PopupMenu audioTrackPopupMenu;
protected PopupMenu playbackSpeedPopupMenu;
private PopupMenu captionPopupMenu;
+ private PopupMenu sleepTimerPopupMenu;
/*//////////////////////////////////////////////////////////////////////////
@@ -186,6 +188,8 @@ private void initViews() {
audioTrackPopupMenu = new PopupMenu(themeWrapper, binding.audioTrackTextView);
playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
+ sleepTimerPopupMenu = new PopupMenu(themeWrapper, binding.sleepTimer);
+ buildSleepTimerMenu();
binding.progressBarLoadingPanel.getIndeterminateDrawable()
.setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY));
@@ -204,6 +208,9 @@ protected void initListeners() {
binding.audioTrackTextView.setOnClickListener(
makeOnClickListener(this::onAudioTracksClicked));
binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked));
+ binding.sleepTimer.setOnClickListener(makeOnClickListener(this::onSleepTimerClicked));
+ binding.sleepTimerCancel.setOnClickListener(
+ makeOnClickListener(this::onSleepTimerCancelClicked));
binding.playbackSeekBar.setOnSeekBarChangeListener(this);
binding.captionTextView.setOnClickListener(makeOnClickListener(this::onCaptionClicked));
@@ -1239,6 +1246,49 @@ private void buildCaptionMenu(@NonNull final List availableLanguages) {
}
}
+ private void buildSleepTimerMenu() {
+ if (sleepTimerPopupMenu == null) {
+ return;
+ }
+ qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_SLEEP_TIMER);
+
+ final Resources res = context.getResources();
+ sleepTimerPopupMenu.getMenu().add(POPUP_MENU_ID_SLEEP_TIMER, 0, 0,
+ res.getString(R.string.sleep_timer_popup_title));
+
+ final String[] descriptions = context.getResources().getStringArray(
+ R.array.sleep_timer_description);
+ final int[] values = context.getResources().getIntArray(
+ R.array.sleep_timer_value);
+ for (int i = 0; i < descriptions.length && i < values.length; i++) {
+ String description = "";
+ try {
+ final int hours = values[i] / 60;
+ final int minutes = values[i] % 60;
+ if (hours != 0) {
+ description += String.format(res.getQuantityString(R.plurals.hours, hours),
+ hours);
+ }
+
+ if (minutes != 0) {
+ if (hours != 0) {
+ description += " ";
+ }
+ description += String.format(res.getQuantityString(R.plurals.minutes, minutes),
+ minutes);
+ }
+ } catch (final Resources.NotFoundException ignored) {
+ // if this happens, the translation is missing,
+ // and the english string will be displayed instead
+ description = descriptions[i];
+ }
+ sleepTimerPopupMenu.getMenu().add(POPUP_MENU_ID_SLEEP_TIMER, i + 1, i + 1, description);
+ }
+
+ sleepTimerPopupMenu.setOnMenuItemClickListener(this);
+ sleepTimerPopupMenu.setOnDismissListener(this);
+ }
+
protected abstract void onPlaybackSpeedClicked();
private void onQualityClicked() {
@@ -1255,6 +1305,19 @@ private void onAudioTracksClicked() {
isSomePopupMenuVisible = true;
}
+ private void onSleepTimerClicked() {
+ sleepTimerPopupMenu.show();
+ isSomePopupMenuVisible = true;
+ }
+
+ private void onSleepTimerCancelClicked() {
+ player.cancelSleepTimer();
+
+ binding.sleepTimerCancel.setVisibility(View.INVISIBLE);
+ binding.sleepTimerTextView.setVisibility(View.INVISIBLE);
+ binding.sleepTimerTextView.setText("0:00");
+ }
+
/**
* Called when an item of the quality selector or the playback speed selector is selected.
*/
@@ -1278,8 +1341,12 @@ public boolean onMenuItemClick(@NonNull final MenuItem menuItem) {
player.setPlaybackSpeed(speed);
binding.playbackSpeed.setText(formatSpeed(speed));
+ } else if (menuItem.getGroupId() == POPUP_MENU_ID_SLEEP_TIMER) {
+ onSleepTimerItemClick(menuItem);
+ return true;
}
+
return false;
}
@@ -1324,6 +1391,24 @@ private void onAudioTrackItemClick(@NonNull final MenuItem menuItem) {
binding.audioTrackTextView.setText(menuItem.getTitle());
}
+ private void onSleepTimerItemClick(@NonNull final MenuItem menuItem) {
+ final int menuItemIndex = menuItem.getItemId();
+ if (menuItemIndex == 0) {
+ return;
+ }
+
+ final int index = menuItemIndex - 1;
+ final int sleepTime = context.getResources().getIntArray(R.array.sleep_timer_value)[index];
+ final long remainingTimeHours = sleepTime / 60;
+ final long remainingTimeMinutes = sleepTime % 60;
+ final String text = String.format("%d:%02d", remainingTimeHours, remainingTimeMinutes);
+
+ player.setSleepTimer(sleepTime);
+ binding.sleepTimerCancel.setVisibility(View.VISIBLE);
+ binding.sleepTimerTextView.setVisibility(View.VISIBLE);
+ binding.sleepTimerTextView.setText(text);
+ }
+
/**
* Called when some popup menu is dismissed.
*/
@@ -1561,6 +1646,32 @@ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
}
//endregion
+ @Override
+ public void onSleepTimerUpdate(final long remainingTime) {
+ if (remainingTime == 0) {
+ binding.sleepTimerTextView.post(new Runnable() {
+ public void run() {
+ player.pause();
+ binding.sleepTimerCancel.setVisibility(View.INVISIBLE);
+ binding.sleepTimerTextView.setVisibility(View.INVISIBLE);
+ binding.sleepTimerTextView.setText("0:00");
+ }
+ });
+ return;
+ }
+
+ final long remainingTimeHours = remainingTime / 60;
+ final long remainingTimeMinutes = remainingTime % 60;
+ final String text = String.format("%d:%02d", remainingTimeHours, remainingTimeMinutes);
+
+ // Since this callback can/will be called from a different thread, we need to run set
+ // the code in the UI thread
+ binding.sleepTimerTextView.post(new Runnable() {
+ public void run() {
+ binding.sleepTimerTextView.setText(text);
+ }
+ });
+ }
/*//////////////////////////////////////////////////////////////////////////
// SurfaceHolderCallback helpers
diff --git a/app/src/main/res/drawable/ic_alarm.xml b/app/src/main/res/drawable/ic_alarm.xml
new file mode 100644
index 00000000000..dfc893e1e96
--- /dev/null
+++ b/app/src/main/res/drawable/ic_alarm.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_alarm_off.xml b/app/src/main/res/drawable/ic_alarm_off.xml
new file mode 100644
index 00000000000..156a4320ccc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_alarm_off.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml
index 99b514bb090..289cfa21c38 100644
--- a/app/src/main/res/layout/player.xml
+++ b/app/src/main/res/layout/player.xml
@@ -262,124 +262,188 @@
android:layout_height="wrap_content"
android:gravity="top"
android:visibility="invisible"
- tools:ignore="RtlHardcoded"
- tools:visibility="visible">
+ android:orientation="vertical">
-
-
-
+ android:gravity="top"
+ tools:ignore="RtlHardcoded">
+ tools:ignore="HardcodedText,RtlHardcoded"
+ tools:text="FIT" />
-
+
+
+
+
+
+
+
-
+
-
+
-
+
+
+
+
-
+
-
+
-
+
+
+
+
+
+
Gefällt mir
SoundCloud-Top-50-Seite entfernt
SoundCloud hat die ursprünglichen Top-50-Charts abgeschafft. Der entsprechende Tab wurde von deiner Hauptseite entfernt.
+ Sleep Timer
+ Sleep Timer stellen
+ Sleep Timer beenden
diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml
index d95d1270cc9..fec2403a6dc 100644
--- a/app/src/main/res/values/settings_keys.xml
+++ b/app/src/main/res/values/settings_keys.xml
@@ -50,6 +50,27 @@
- 30000
+
+
+ - 5 minutes
+ - 10 minutes
+ - 20 minutes
+ - 30 minutes
+ - 45 minutes
+ - 1 hour
+ - 2 hours
+
+
+ - 5
+ - 10
+ - 20
+ - 30
+ - 45
+ - 60
+ - 120
+
+
progressive_load_interval
64
exoplayer_default
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f28a9958da0..281aba8f8f8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -889,4 +889,7 @@
Trending podcasts
Trending movies and shows
Trending music
+ Sleep timer
+ Set sleep timer
+ Cancel sleep timer