Merge pull request #5204 from Bnyro/sleep-timer

feat: new sleep timer (integrated into the player UI)
This commit is contained in:
Bnyro 2023-11-19 15:14:35 +01:00 committed by GitHub
commit 61d85033ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 253 additions and 75 deletions

View File

@ -19,8 +19,6 @@ object PreferenceKeys {
const val LANGUAGE = "language"
const val REGION = "region"
const val ORIENTATION = "orientation"
const val SLEEP_TIMER = "sleep_timer_toggle"
const val SLEEP_TIMER_DELAY = "sleep_timer_delay"
const val SAVE_FEED = "save_feed"
const val NAVBAR_ITEMS = "navbar_items"
const val START_FRAGMENT = "start_fragment"

View File

@ -42,7 +42,6 @@ import com.github.libretube.ui.fragments.PlayerFragment
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.models.SearchViewModel
import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.ui.tools.SleepTimer
import com.google.android.material.elevation.SurfaceColors
class MainActivity : BaseActivity() {
@ -138,8 +137,6 @@ class MainActivity : BaseActivity() {
ErrorDialog().show(supportFragmentManager, null)
}
SleepTimer.setup(this)
setupSubscriptionsBadge()
// new way of handling back presses

View File

@ -7,4 +7,5 @@ interface PlayerOptions {
fun onResizeModeClicked()
fun onRepeatModeClicked()
fun onSleepTimerClicked()
}

View File

@ -32,25 +32,6 @@ class GeneralSettings : BasePreferenceFragment() {
RequireRestartDialog().show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
val breakReminder =
findPreference<SwitchPreferenceCompat>(PreferenceKeys.SLEEP_TIMER)
val breakReminderTime = findPreference<EditTextPreference>(PreferenceKeys.SLEEP_TIMER_DELAY)
breakReminderTime?.isEnabled = PreferenceHelper.getBoolean(
PreferenceKeys.SLEEP_TIMER,
false
)
breakReminder?.setOnPreferenceChangeListener { _, newValue ->
breakReminderTime?.isEnabled = newValue as Boolean
RequireRestartDialog().show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
breakReminderTime?.setOnPreferenceChangeListener { _, _ ->
RequireRestartDialog().show(childFragmentManager, RequireRestartDialog::class.java.name)
true
}
}
private fun setupRegionPref(preference: ListPreference) {

View File

@ -0,0 +1,73 @@
package com.github.libretube.ui.sheets
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.os.postDelayed
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.github.libretube.R
import com.github.libretube.databinding.SleepTimerSheetBinding
import com.github.libretube.ui.tools.SleepTimer
class SleepTimerSheet: ExpandedBottomSheet() {
private var _binding: SleepTimerSheetBinding? = null
private val binding get() = _binding!!
private val handler = Handler(Looper.getMainLooper())
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = SleepTimerSheetBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateTimeLeftText()
binding.startSleepTimer.setOnClickListener {
val time = binding.timeInput.text.toString().toLongOrNull()
if (time == null) {
Toast.makeText(context, R.string.invalid_input, Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
SleepTimer.setup(requireContext(), time)
updateTimeLeftText()
}
binding.stopSleepTimer.setOnClickListener {
SleepTimer.disableSleepTimer()
updateTimeLeftText()
}
}
private fun updateTimeLeftText() {
val binding = _binding ?: return
val isTimerRunning = SleepTimer.timeLeftMillis > 0
binding.timeLeft.isVisible = isTimerRunning
binding.stopSleepTimer.isVisible = isTimerRunning
binding.timeInputLayout.isGone = isTimerRunning
binding.startSleepTimer.isGone = isTimerRunning
if (!isTimerRunning) return
binding.timeLeft.text = DateUtils.formatElapsedTime(SleepTimer.timeLeftMillis / 1000)
handler.postDelayed(1000) {
updateTimeLeftText()
}
}
}

View File

@ -6,52 +6,67 @@ import android.os.Looper
import android.os.Process
import androidx.core.os.postDelayed
import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.activities.MainActivity
import com.google.android.material.snackbar.Snackbar
import java.util.Timer
import kotlin.concurrent.scheduleAtFixedRate
object SleepTimer {
private val handler = Handler(Looper.getMainLooper())
private const val REACTION_INTERVAL = 5L
private const val TIMER_DELAY = 1000L
private var timer: Timer? = null
var timeLeftMillis: Long = 0L
/**
* Kill the app after showing a warning after a certain amount of time
* @param context This must not be the applicationContext!
*
* @param context This must not be the applicationContext, but an activity context!
*/
fun setup(context: Context) {
if (!PreferenceHelper.getBoolean(PreferenceKeys.SLEEP_TIMER, false)) return
fun setup(context: Context, delayInMinutes: Long) {
if (delayInMinutes == 0L) return
val breakReminderPref = PreferenceHelper.getString(
PreferenceKeys.SLEEP_TIMER_DELAY,
""
).ifEmpty { return }
timeLeftMillis = delayInMinutes * 60 * 1000
handler.postDelayed(breakReminderPref.toLong() * 60 * 1000) {
var killApp = true
val mainActivity = context as? MainActivity ?: return@postDelayed
val snackBar = Snackbar.make(
mainActivity.binding.root,
R.string.take_a_break,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.cancel) {
killApp = false
}
snackBar.show()
for (i in 0..REACTION_INTERVAL) {
handler.postDelayed(i * 1000) {
val remainingTime = " (${REACTION_INTERVAL - i})"
snackBar.setText(context.getString(R.string.take_a_break) + remainingTime)
}
timer = Timer()
timer?.scheduleAtFixedRate(TIMER_DELAY, TIMER_DELAY) {
timeLeftMillis -= TIMER_DELAY
if (timeLeftMillis == 0L) showTimerEndedSnackBar(context)
}
}
/**
* Disable the scheduled sleep timer
*/
fun disableSleepTimer() {
timer?.cancel()
timeLeftMillis = 0L
}
private fun showTimerEndedSnackBar(context: Context) {
var killApp = true
val mainActivity = context as? MainActivity ?: return
val snackBar = Snackbar.make(
mainActivity.binding.root,
R.string.take_a_break,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.cancel) {
killApp = false
}
handler.postDelayed(REACTION_INTERVAL * 1000) {
if (killApp) {
// kill the application
mainActivity.finishAffinity()
mainActivity.finish()
Process.killProcess(Process.myPid())
}
snackBar.show()
for (i in 0..REACTION_INTERVAL) {
handler.postDelayed(i * 1000) {
val remainingTime = " (${REACTION_INTERVAL - i})"
snackBar.setText(context.getString(R.string.take_a_break) + remainingTime)
}
}
handler.postDelayed(REACTION_INTERVAL * 1000) {
if (killApp) {
// kill the application
mainActivity.finishAffinity()
mainActivity.finish()
Process.killProcess(Process.myPid())
}
}
}

View File

@ -56,6 +56,7 @@ import com.github.libretube.ui.interfaces.PlayerOptions
import com.github.libretube.ui.listeners.PlayerGestureController
import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
import com.github.libretube.ui.sheets.SleepTimerSheet
import com.github.libretube.util.PlayingQueue
@SuppressLint("ClickableViewAccessibility")
@ -347,6 +348,12 @@ open class CustomExoPlayerView(
}
) {
onPlaybackSpeedClicked()
},
BottomSheetItem(
context.getString(R.string.sleep_timer),
R.drawable.ic_sleep
) {
onSleepTimerClicked()
}
)
@ -552,6 +559,10 @@ open class CustomExoPlayerView(
.show(supportFragmentManager)
}
override fun onSleepTimerClicked() {
SleepTimerSheet().show(supportFragmentManager)
}
open fun isFullscreen() =
resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12.34,2.02C6.59,1.82 2,6.42 2,12c0,5.52 4.48,10 10,10c3.71,0 6.93,-2.02 8.66,-5.02C13.15,16.73 8.57,8.55 12.34,2.02z" />
</vector>

View File

@ -0,0 +1,106 @@
<?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="match_parent">
<FrameLayout
android:id="@+id/standard_bottom_sheet"
style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="20dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Drag handle for accessibility -->
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/bottom_sheet_title_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginBottom="5dp"
android:text="@string/sleep_timer"
android:textSize="27sp" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/time_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="10dp"
android:layout_marginVertical="10dp"
android:textColor="?colorPrimary"
android:textSize="40sp"
android:textStyle="bold"
android:visibility="gone"
tools:text="20:10" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/time_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/time_in_minutes">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/time_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginVertical="10dp"
android:inputType="number"
android:text="30"
tools:ignore="HardcodedText" />
</com.google.android.material.textfield.TextInputLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/start_sleep_timer"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/start_sleep_timer" />
<com.google.android.material.button.MaterialButton
android:id="@+id/stop_sleep_timer"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/disable_sleep_timer"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -496,6 +496,10 @@
<string name="watched">Watched</string>
<string name="no_segments_found">There are no segments for this video yet.</string>
<string name="bottom_reached">Bottom reached!</string>
<string name="start_sleep_timer">Start sleep timer</string>
<string name="disable_sleep_timer">Disable sleep timer</string>
<string name="time_in_minutes">Time in minutes</string>
<string name="invalid_input">Invalid input</string>
<!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string>

View File

@ -54,22 +54,4 @@
</PreferenceCategory>
<PreferenceCategory app:title="@string/misc">
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_notification"
app:key="sleep_timer_toggle"
app:title="@string/sleep_timer" />
<EditTextPreference
android:enabled="false"
android:icon="@drawable/ic_time"
app:defaultValue="60"
app:key="sleep_timer_delay"
app:title="@string/break_reminder_time"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>