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) } }