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 LANGUAGE = "language"
const val REGION = "region" const val REGION = "region"
const val ORIENTATION = "orientation" 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 SAVE_FEED = "save_feed"
const val NAVBAR_ITEMS = "navbar_items" const val NAVBAR_ITEMS = "navbar_items"
const val START_FRAGMENT = "start_fragment" 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.PlayerViewModel
import com.github.libretube.ui.models.SearchViewModel import com.github.libretube.ui.models.SearchViewModel
import com.github.libretube.ui.models.SubscriptionsViewModel import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.ui.tools.SleepTimer
import com.google.android.material.elevation.SurfaceColors import com.google.android.material.elevation.SurfaceColors
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
@ -138,8 +137,6 @@ class MainActivity : BaseActivity() {
ErrorDialog().show(supportFragmentManager, null) ErrorDialog().show(supportFragmentManager, null)
} }
SleepTimer.setup(this)
setupSubscriptionsBadge() setupSubscriptionsBadge()
// new way of handling back presses // new way of handling back presses

View File

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

View File

@ -32,25 +32,6 @@ class GeneralSettings : BasePreferenceFragment() {
RequireRestartDialog().show(childFragmentManager, RequireRestartDialog::class.java.name) RequireRestartDialog().show(childFragmentManager, RequireRestartDialog::class.java.name)
true 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) { 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 android.os.Process
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import com.github.libretube.R 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.github.libretube.ui.activities.MainActivity
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import java.util.Timer
import kotlin.concurrent.scheduleAtFixedRate
object SleepTimer { object SleepTimer {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private const val REACTION_INTERVAL = 5L 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 * 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) { fun setup(context: Context, delayInMinutes: Long) {
if (!PreferenceHelper.getBoolean(PreferenceKeys.SLEEP_TIMER, false)) return if (delayInMinutes == 0L) return
val breakReminderPref = PreferenceHelper.getString( timeLeftMillis = delayInMinutes * 60 * 1000
PreferenceKeys.SLEEP_TIMER_DELAY,
""
).ifEmpty { return }
handler.postDelayed(breakReminderPref.toLong() * 60 * 1000) { timer = Timer()
var killApp = true timer?.scheduleAtFixedRate(TIMER_DELAY, TIMER_DELAY) {
val mainActivity = context as? MainActivity ?: return@postDelayed timeLeftMillis -= TIMER_DELAY
val snackBar = Snackbar.make( if (timeLeftMillis == 0L) showTimerEndedSnackBar(context)
mainActivity.binding.root, }
R.string.take_a_break, }
Snackbar.LENGTH_INDEFINITE
) /**
.setAction(R.string.cancel) { * Disable the scheduled sleep timer
killApp = false */
} fun disableSleepTimer() {
snackBar.show() timer?.cancel()
for (i in 0..REACTION_INTERVAL) { timeLeftMillis = 0L
handler.postDelayed(i * 1000) { }
val remainingTime = " (${REACTION_INTERVAL - i})"
snackBar.setText(context.getString(R.string.take_a_break) + remainingTime) 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) { snackBar.show()
if (killApp) { for (i in 0..REACTION_INTERVAL) {
// kill the application handler.postDelayed(i * 1000) {
mainActivity.finishAffinity() val remainingTime = " (${REACTION_INTERVAL - i})"
mainActivity.finish() snackBar.setText(context.getString(R.string.take_a_break) + remainingTime)
Process.killProcess(Process.myPid()) }
} }
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.listeners.PlayerGestureController
import com.github.libretube.ui.sheets.BaseBottomSheet import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.PlaybackOptionsSheet import com.github.libretube.ui.sheets.PlaybackOptionsSheet
import com.github.libretube.ui.sheets.SleepTimerSheet
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ -347,6 +348,12 @@ open class CustomExoPlayerView(
} }
) { ) {
onPlaybackSpeedClicked() onPlaybackSpeedClicked()
},
BottomSheetItem(
context.getString(R.string.sleep_timer),
R.drawable.ic_sleep
) {
onSleepTimerClicked()
} }
) )
@ -552,6 +559,10 @@ open class CustomExoPlayerView(
.show(supportFragmentManager) .show(supportFragmentManager)
} }
override fun onSleepTimerClicked() {
SleepTimerSheet().show(supportFragmentManager)
}
open fun isFullscreen() = open fun isFullscreen() =
resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE 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="watched">Watched</string>
<string name="no_segments_found">There are no segments for this video yet.</string> <string name="no_segments_found">There are no segments for this video yet.</string>
<string name="bottom_reached">Bottom reached!</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 --> <!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string> <string name="download_channel_name">Download Service</string>

View File

@ -54,22 +54,4 @@
</PreferenceCategory> </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> </PreferenceScreen>