diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml
index 335375bdf..579c27a7c 100644
--- a/shared/src/commonMain/composeResources/values/strings.xml
+++ b/shared/src/commonMain/composeResources/values/strings.xml
@@ -89,4 +89,67 @@
Best resolution
No limit
+
+ Player
+ Use external video player
+ Removes audio at some resolutions
+ Use external audio player
+ Show \"Play with Kodi\" option
+ Display an option to play a video via Kodi media center
+ Seekbar thumbnail preview
+ High quality (larger)
+ Low quality (smaller)
+ Don\'t show
+
+
+ Behavior
+ Preferred \'open\' action
+ Default action when opening content — %1$s
+ Show info
+ Video player
+ Background player
+ Popup player
+ Download
+ Add to playlist
+ Enqueue
+ Always ask
+ Minimize on app switch
+ Action when switching to other app from main video player — %1$s
+ None
+ Minimize to background player
+ Minimize to popup player
+ Start main player in fullscreen
+ Do not start videos in the mini player, but turn to fullscreen mode directly, if auto rotation is locked. You can still access the mini player by exiting fullscreen
+ Autoplay
+ Start playback automatically — %1$s
+ Always
+ Only on Wi-Fi
+ Never
+ Auto-enqueue next stream
+ Continue ending (non-repeating) playback queue by appending a related stream
+ Resume playing
+ Continue playing after interruptions (e.g. phonecalls)
+ Left gesture action
+ Choose gesture for left half of player screen
+ Right gesture action
+ Choose gesture for right half of player screen
+ Brightness
+ Volume
+ None
+ Remember popup properties
+ Remember last size and position of popup
+ Use fast inexact seek
+ Inexact seek allows the player to seek to positions faster with reduced precision. Seeking for 5, 15 or 25 seconds doesn\'t work with this
+ Fast-forward/-rewind seek duration
+ Due to ExoPlayer constraints the seek duration was set to %1$d seconds
+ Ask for confirmation before clearing a queue
+ Switching from one player to another may replace your queue
+ Ignore hardware media button events
+ Useful, for instance, if you are using a headset with broken physical buttons
+
+ - %1$d second
+ - %1$d seconds
+
+
+
diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/composable/PreferenceCategoryTitle.kt b/shared/src/commonMain/kotlin/net/newpipe/app/composable/PreferenceCategoryTitle.kt
new file mode 100644
index 000000000..f6d2235bb
--- /dev/null
+++ b/shared/src/commonMain/kotlin/net/newpipe/app/composable/PreferenceCategoryTitle.kt
@@ -0,0 +1,2 @@
+package net.newpipe.app.composable
+
diff --git a/shared/src/commonMain/kotlin/net/newpipe/app/preferences/VideoAudioPreferences.kt b/shared/src/commonMain/kotlin/net/newpipe/app/preferences/VideoAudioPreferences.kt
index 7d8e65e2e..26a2818f3 100644
--- a/shared/src/commonMain/kotlin/net/newpipe/app/preferences/VideoAudioPreferences.kt
+++ b/shared/src/commonMain/kotlin/net/newpipe/app/preferences/VideoAudioPreferences.kt
@@ -19,6 +19,27 @@ object VideoAudioPreferences {
const val KEY_PREFER_ORIGINAL_AUDIO = "prefer_original_audio"
const val KEY_PREFER_DESCRIPTIVE_AUDIO = "prefer_descriptive_audio"
+ // Player section
+ const val KEY_USE_EXTERNAL_VIDEO_PLAYER = "use_external_video_player"
+ const val KEY_USE_EXTERNAL_AUDIO_PLAYER = "use_external_audio_player"
+ const val KEY_SHOW_PLAY_WITH_KODI = "show_play_with_kodi"
+ const val KEY_SEEKBAR_PREVIEW_THUMBNAIL = "seekbar_preview_thumbnail_key"
+
+ // Behavior section
+ const val KEY_PREFERRED_OPEN_ACTION = "preferred_open_action_key"
+ const val KEY_MINIMIZE_ON_EXIT = "minimize_on_exit_key"
+ const val KEY_START_MAIN_PLAYER_FULLSCREEN = "start_main_player_fullscreen_key"
+ const val KEY_AUTOPLAY = "autoplay_key"
+ const val KEY_AUTO_QUEUE = "auto_queue_key"
+ const val KEY_RESUME_ON_AUDIO_FOCUS_GAIN = "resume_on_audio_focus_gain"
+ const val KEY_LEFT_GESTURE_CONTROL = "left_gesture_control"
+ const val KEY_RIGHT_GESTURE_CONTROL = "right_gesture_control"
+ const val KEY_POPUP_REMEMBER_SIZE_POS = "popup_remember_size_pos_key"
+ const val KEY_USE_INEXACT_SEEK = "use_inexact_seek_key"
+ const val KEY_SEEK_DURATION = "seek_duration"
+ const val KEY_CLEAR_QUEUE_CONFIRMATION = "clear_queue_confirmation_key"
+ const val KEY_IGNORE_HARDWARE_MEDIA_BUTTONS = "ignore_hardware_media_buttons_key"
+
// Special resolution values that need a localized label
const val BEST_RESOLUTION = "best_resolution"
const val LIMIT_DATA_USAGE_NONE = "limit_data_usage_none"
@@ -30,6 +51,36 @@ object VideoAudioPreferences {
const val AUDIO_M4A = "audio_m4a"
const val AUDIO_WEBM = "audio_webm"
+ // Seekbar preview thumbnail values.
+ const val SEEKBAR_THUMBNAIL_HIGH_QUALITY = "seekbar_preview_thumbnail_high_quality"
+ const val SEEKBAR_THUMBNAIL_LOW_QUALITY = "seekbar_preview_thumbnail_low_quality"
+ const val SEEKBAR_THUMBNAIL_NONE = "seekbar_preview_thumbnail_none"
+
+ // Preferred open action values.
+ const val OPEN_ACTION_SHOW_INFO = "show_info"
+ const val OPEN_ACTION_VIDEO_PLAYER = "video_player"
+ const val OPEN_ACTION_BACKGROUND_PLAYER = "background_player"
+ const val OPEN_ACTION_POPUP_PLAYER = "popup_player"
+ const val OPEN_ACTION_DOWNLOAD = "download"
+ const val OPEN_ACTION_ADD_TO_PLAYLIST = "add_to_playlist"
+ const val OPEN_ACTION_ENQUEUE = "enqueue"
+ const val OPEN_ACTION_ALWAYS_ASK = "always_ask_player"
+
+ // Minimize on exit values.
+ const val MINIMIZE_ON_EXIT_NONE = "minimize_on_exit_none_key"
+ const val MINIMIZE_ON_EXIT_BACKGROUND = "minimize_on_exit_background_key"
+ const val MINIMIZE_ON_EXIT_POPUP = "minimize_on_exit_popup_key"
+
+ // Autoplay values.
+ const val AUTOPLAY_ALWAYS = "autoplay_always_key"
+ const val AUTOPLAY_WIFI = "autoplay_wifi_key"
+ const val AUTOPLAY_NEVER = "autoplay_never_key"
+
+ // Player gesture values.
+ const val GESTURE_BRIGHTNESS = "brightness_control"
+ const val GESTURE_VOLUME = "volume_control"
+ const val GESTURE_NONE = "none_control"
+
const val DEFAULT_RESOLUTION = "720p60"
const val DEFAULT_POPUP_RESOLUTION = "480p"
const val DEFAULT_LIMIT_MOBILE_DATA_USAGE = LIMIT_DATA_USAGE_NONE
@@ -38,6 +89,25 @@ object VideoAudioPreferences {
const val DEFAULT_AUDIO_FORMAT = AUDIO_M4A
const val DEFAULT_PREFER_ORIGINAL_AUDIO = true
const val DEFAULT_PREFER_DESCRIPTIVE_AUDIO = false
+ const val DEFAULT_USE_EXTERNAL_VIDEO_PLAYER = false
+ const val DEFAULT_USE_EXTERNAL_AUDIO_PLAYER = false
+ const val DEFAULT_SHOW_PLAY_WITH_KODI = false
+ const val DEFAULT_SEEKBAR_PREVIEW_THUMBNAIL = SEEKBAR_THUMBNAIL_HIGH_QUALITY
+ const val DEFAULT_PREFERRED_OPEN_ACTION = OPEN_ACTION_ALWAYS_ASK
+ const val DEFAULT_MINIMIZE_ON_EXIT = MINIMIZE_ON_EXIT_BACKGROUND
+ const val DEFAULT_START_MAIN_PLAYER_FULLSCREEN = false
+ const val DEFAULT_AUTOPLAY = AUTOPLAY_WIFI
+ const val DEFAULT_AUTO_QUEUE = false
+ const val DEFAULT_RESUME_ON_AUDIO_FOCUS_GAIN = false
+ const val DEFAULT_LEFT_GESTURE_CONTROL = GESTURE_BRIGHTNESS
+ const val DEFAULT_RIGHT_GESTURE_CONTROL = GESTURE_VOLUME
+ const val DEFAULT_POPUP_REMEMBER_SIZE_POS = true
+ const val DEFAULT_USE_INEXACT_SEEK = false
+ const val DEFAULT_SEEK_DURATION_MS = "10000"
+ const val DEFAULT_CLEAR_QUEUE_CONFIRMATION = false
+ const val DEFAULT_IGNORE_HARDWARE_MEDIA_BUTTONS = false
+
+ private const val MILLIS_PER_SECOND = 1000
/** Higher resolutions, hidden behind the "show higher resolutions" toggle. */
val HIGH_RESOLUTIONS = listOf("2160p", "1440p")
@@ -56,10 +126,26 @@ object VideoAudioPreferences {
val AUDIO_FORMATS = listOf(AUDIO_M4A to "M4A", AUDIO_WEBM to "WebM")
+ /** Seek durations in milliseconds, stored as strings like the legacy app. */
+ val SEEK_DURATIONS_MS = listOf("5000", "10000", "15000", "20000", "25000", "30000")
+
fun resolutions(showHigher: Boolean): List = insertHighResolutions(BASE_RESOLUTIONS, showHigher)
fun mobileDataResolutions(showHigher: Boolean): List = insertHighResolutions(BASE_MOBILE_DATA_RESOLUTIONS, showHigher)
- private fun insertHighResolutions(base: List, showHigher: Boolean): List =
- if (!showHigher) base else base.toMutableList().apply { addAll(1, HIGH_RESOLUTIONS) }
+ fun seekDurationSeconds(durationMs: String): Int = durationMs.toInt() / MILLIS_PER_SECOND
+
+ /** ExoPlayer can't inexact-seek in 5-second steps in audio, so those durations are hidden. */
+ fun seekDurationsMs(inexactSeek: Boolean): List = if (!inexactSeek) SEEK_DURATIONS_MS else SEEK_DURATIONS_MS.filter { seekDurationSeconds(it) % 10 != 5 }
+
+ /**
+ * The seek duration to switch to when [inexactSeek] hides the currently selected one or null when no adjustment is needed.
+ */
+ fun adjustedSeekDurationMs(currentMs: String, inexactSeek: Boolean): String? {
+ if (!inexactSeek) return null
+ val seconds = seekDurationSeconds(currentMs)
+ return if (seconds % 10 == 5) ((seconds + 5) * MILLIS_PER_SECOND).toString() else null
+ }
+
+ private fun insertHighResolutions(base: List, showHigher: Boolean): List = if (!showHigher) base else base.toMutableList().apply { addAll(1, HIGH_RESOLUTIONS) }
}