diff --git a/app/src/main/java/com/github/libretube/api/ExternalApi.kt b/app/src/main/java/com/github/libretube/api/ExternalApi.kt index d3ba49f7a..89fbcba6a 100644 --- a/app/src/main/java/com/github/libretube/api/ExternalApi.kt +++ b/app/src/main/java/com/github/libretube/api/ExternalApi.kt @@ -1,9 +1,13 @@ package com.github.libretube.api import com.github.libretube.api.obj.Instances +import com.github.libretube.api.obj.SubmitSegmentResponse import com.github.libretube.constants.GITHUB_API_URL +import com.github.libretube.constants.SB_SUBMIT_API_URL import com.github.libretube.obj.update.UpdateInfo import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query import retrofit2.http.Url interface ExternalApi { @@ -14,4 +18,16 @@ interface ExternalApi { // fetch latest version info @GET(GITHUB_API_URL) suspend fun getUpdateInfo(): UpdateInfo + + @POST(SB_SUBMIT_API_URL) + suspend fun submitSegment( + @Query("videoID") videoId: String, + @Query("startTime") startTime: Float, + @Query("endTime") endTime: Float, + @Query("category") category: String, + @Query("userAgent") userAgent: String, + @Query("userID") userID: String, + @Query("duration") duration: Float? = null, + @Query("description") description: String = "" + ): List } diff --git a/app/src/main/java/com/github/libretube/api/obj/DeArrowTitle.kt b/app/src/main/java/com/github/libretube/api/obj/DeArrowTitle.kt index 0a6154911..d4594bf68 100644 --- a/app/src/main/java/com/github/libretube/api/obj/DeArrowTitle.kt +++ b/app/src/main/java/com/github/libretube/api/obj/DeArrowTitle.kt @@ -1,10 +1,11 @@ package com.github.libretube.api.obj +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class DeArrowTitle( - val UUID: String, + @SerialName("UUID") val uuid: String, val locked: Boolean, val original: Boolean, val title: String, diff --git a/app/src/main/java/com/github/libretube/api/obj/SubmitSegmentResponse.kt b/app/src/main/java/com/github/libretube/api/obj/SubmitSegmentResponse.kt new file mode 100644 index 000000000..78aaf258f --- /dev/null +++ b/app/src/main/java/com/github/libretube/api/obj/SubmitSegmentResponse.kt @@ -0,0 +1,11 @@ +package com.github.libretube.api.obj + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SubmitSegmentResponse( + @SerialName("UUID") val uuid: String, + val category: String, + val segment: List +) diff --git a/app/src/main/java/com/github/libretube/constants/Constants.kt b/app/src/main/java/com/github/libretube/constants/Constants.kt index a4e5037a5..36e6271b4 100644 --- a/app/src/main/java/com/github/libretube/constants/Constants.kt +++ b/app/src/main/java/com/github/libretube/constants/Constants.kt @@ -4,6 +4,7 @@ package com.github.libretube.constants * API link for the update checker */ const val GITHUB_API_URL = "https://api.github.com/repos/libre-tube/LibreTube/releases/latest" +const val SB_SUBMIT_API_URL = "https://sponsor.ajay.app/api/skipSegments" /** * Links for the about fragment diff --git a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt index 70dbb1d48..f5ebba6cb 100644 --- a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt +++ b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt @@ -154,4 +154,9 @@ object PreferenceKeys { * Error logs */ const val ERROR_LOG = "error_log" + + /** + * SponsorBlock UUID + */ + const val SB_USER_ID = "sb_user_id" } diff --git a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt index beb8debd1..61ef89222 100644 --- a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt @@ -24,6 +24,7 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.LoadControl import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.ui.CaptionStyleCompat +import com.github.libretube.LibreTubeApp import com.github.libretube.R import com.github.libretube.api.obj.ChapterSegment import com.github.libretube.api.obj.Segment @@ -41,17 +42,6 @@ import kotlinx.coroutines.runBlocking object PlayerHelper { private const val ACTION_MEDIA_CONTROL = "media_control" const val CONTROL_TYPE = "control_type" - private val SPONSOR_CATEGORIES = - arrayOf( - "intro", - "selfpromo", - "interaction", - "sponsor", - "outro", - "filler", - "music_offtopic", - "preview" - ) const val SPONSOR_HIGHLIGHT_CATEGORY = "poi_highlight" const val ROLE_FLAG_AUTO_GEN_SUBTITLE = C.ROLE_FLAG_SUPPLEMENTARY @@ -463,7 +453,9 @@ object PlayerHelper { fun getSponsorBlockCategories(): MutableMap { val categories: MutableMap = mutableMapOf() - for (category in SPONSOR_CATEGORIES) { + for (category in LibreTubeApp.instance.resources.getStringArray( + R.array.sponsorBlockSegments + )) { val state = PreferenceHelper.getString(category + "_category", "off").uppercase() if (SbSkipOptions.valueOf(state) != SbSkipOptions.OFF) { categories[category] = SbSkipOptions.valueOf(state) @@ -496,7 +488,7 @@ object PlayerHelper { if (sponsorBlockConfig[segment.category] == SbSkipOptions.AUTOMATIC || ( sponsorBlockConfig[segment.category] == SbSkipOptions.AUTOMATIC_ONCE && - segment.skipped == false + !segment.skipped ) ) { if (sponsorBlockNotifications) { @@ -510,7 +502,7 @@ object PlayerHelper { } else if (sponsorBlockConfig[segment.category] == SbSkipOptions.MANUAL || ( sponsorBlockConfig[segment.category] == SbSkipOptions.AUTOMATIC_ONCE && - segment.skipped == true + segment.skipped ) ) { return segment diff --git a/app/src/main/java/com/github/libretube/helpers/PreferenceHelper.kt b/app/src/main/java/com/github/libretube/helpers/PreferenceHelper.kt index 4158901dc..439e5eaa5 100644 --- a/app/src/main/java/com/github/libretube/helpers/PreferenceHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/PreferenceHelper.kt @@ -6,6 +6,7 @@ import androidx.core.content.edit import androidx.preference.PreferenceManager import com.github.libretube.constants.PreferenceKeys import java.time.Instant +import java.util.UUID object PreferenceHelper { /** @@ -18,6 +19,11 @@ object PreferenceHelper { */ private lateinit var authSettings: SharedPreferences + /** + * Possible chars to use for the SB User ID + */ + private const val USER_ID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" + /** * set the context that is being used to access the shared preferences */ @@ -129,6 +135,16 @@ object PreferenceHelper { } } + fun getSponsorBlockUserID(): String { + var uuid = getString(PreferenceKeys.SB_USER_ID, "") + if (uuid.isEmpty()) { + // generate a new user id to use for submitting SponsorBlock segments + uuid = (0 until 30).map { USER_ID_CHARS.random() }.joinToString("") + putString(PreferenceKeys.SB_USER_ID, uuid) + } + return uuid + } + private fun getDefaultSharedPreferences(context: Context): SharedPreferences { return PreferenceManager.getDefaultSharedPreferences(context) } diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt index a1cc7aa6a..c08024c4f 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt @@ -1,13 +1,13 @@ package com.github.libretube.ui.dialogs import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import android.text.InputFilter import android.text.format.Formatter import android.util.Log import android.widget.ArrayAdapter import android.widget.Toast -import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.fragment.app.DialogFragment import androidx.lifecycle.Lifecycle @@ -65,7 +65,7 @@ class DownloadDialog( .setPositiveButton(R.string.download, null) .show() .apply { - getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { onDownloadConfirm.invoke() } } diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/SubmitSegmentDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/SubmitSegmentDialog.kt new file mode 100644 index 000000000..371a60c3d --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/dialogs/SubmitSegmentDialog.kt @@ -0,0 +1,97 @@ +package com.github.libretube.ui.dialogs + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.util.Log +import android.widget.ArrayAdapter +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import com.github.libretube.BuildConfig +import com.github.libretube.R +import com.github.libretube.api.RetrofitInstance +import com.github.libretube.databinding.DialogSubmitSegmentBinding +import com.github.libretube.extensions.TAG +import com.github.libretube.extensions.toastFromMainDispatcher +import com.github.libretube.helpers.PreferenceHelper +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.lang.Exception +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class SubmitSegmentDialog( + private val videoId: String, + private val currentPosition: Long, + private val duration: Long? +) : DialogFragment() { + private var _binding: DialogSubmitSegmentBinding? = null + private val binding = _binding!! + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + _binding = DialogSubmitSegmentBinding.inflate(layoutInflater) + + binding.startTime.setText((currentPosition.toFloat() / 1000).toString()) + + val categoryNames = resources.getStringArray(R.array.sponsorBlockSegmentNames) + binding.segmentCategory.adapter = ArrayAdapter( + requireContext(), + R.layout.dropdown_item, + categoryNames + ) + + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.sb_create_segment)) + .setView(binding.root) + .setPositiveButton(R.string.okay, null) + .setNegativeButton(R.string.cancel, null) + .show() + .apply { + getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { + requireDialog().hide() + + lifecycleScope.launch { + submitSegment() + dismiss() + } + } + } + } + + private suspend fun submitSegment() { + val context = requireContext().applicationContext + + val startTime = binding.startTime.text.toString().toFloatOrNull() + var endTime = binding.endTime.text.toString().toFloatOrNull() + if (endTime == null || startTime == null || startTime > endTime) { + context.toastFromMainDispatcher(R.string.sb_invalid_segment) + return + } + + if (duration != null) { + // the end time can't be greater than the video duration + endTime = minOf(endTime, duration.toFloat()) + } + + val categories = resources.getStringArray(R.array.sponsorBlockSegments) + val category = categories[binding.segmentCategory.selectedItemPosition] + val userAgent = "${context.packageName}/${BuildConfig.VERSION_NAME}" + val uuid = PreferenceHelper.getSponsorBlockUserID() + val duration = duration?.let { it.toFloat() / 1000 } + + try { + withContext(Dispatchers.IO) { + RetrofitInstance.externalApi + .submitSegment(videoId, startTime, endTime, category, userAgent, uuid, duration) + } + } catch (e: Exception) { + Log.e(TAG(), e.toString()) + context.toastFromMainDispatcher(e.localizedMessage.orEmpty()) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt b/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt index 120a1e893..00f31e977 100644 --- a/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt +++ b/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt @@ -5,16 +5,22 @@ import android.content.res.Configuration import android.util.AttributeSet import android.view.View import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner +import androidx.media3.common.C import androidx.media3.exoplayer.trackselection.TrackSelector import androidx.media3.ui.PlayerView.ControllerVisibilityListener import com.github.libretube.R +import com.github.libretube.extensions.toID import com.github.libretube.helpers.PlayerHelper import com.github.libretube.helpers.WindowHelper import com.github.libretube.obj.BottomSheetItem +import com.github.libretube.ui.base.BaseActivity +import com.github.libretube.ui.dialogs.SubmitSegmentDialog import com.github.libretube.ui.extensions.toggleSystemBars import com.github.libretube.ui.interfaces.OnlinePlayerOptions import com.github.libretube.ui.models.PlayerViewModel +import com.github.libretube.util.PlayingQueue class OnlinePlayerView( context: Context, @@ -157,6 +163,15 @@ class OnlinePlayerView( binding.autoPlay.setOnCheckedChangeListener { _, isChecked -> PlayerHelper.autoPlayEnabled = isChecked } + + binding.sbSubmit.isVisible = PlayerHelper.sponsorBlockEnabled + binding.sbSubmit.setOnClickListener { + val currentPosition = player?.currentPosition?.takeIf { it != C.TIME_UNSET } + val duration = player?.duration?.takeIf { it != C.TIME_UNSET } + val videoId = PlayingQueue.getCurrent()?.url?.toID() ?: return@setOnClickListener + SubmitSegmentDialog(videoId, currentPosition ?: 0, duration) + .show((context as BaseActivity).supportFragmentManager, null) + } } override fun hideController() { diff --git a/app/src/main/res/drawable/ic_upload_segment.xml b/app/src/main/res/drawable/ic_upload_segment.xml new file mode 100644 index 000000000..61c45fad8 --- /dev/null +++ b/app/src/main/res/drawable/ic_upload_segment.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/layout/dialog_submit_segment.xml b/app/src/main/res/layout/dialog_submit_segment.xml new file mode 100644 index 000000000..6ace23c9f --- /dev/null +++ b/app/src/main/res/layout/dialog_submit_segment.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/exo_styled_player_control_view.xml b/app/src/main/res/layout/exo_styled_player_control_view.xml index be215a963..5780e9754 100644 --- a/app/src/main/res/layout/exo_styled_player_control_view.xml +++ b/app/src/main/res/layout/exo_styled_player_control_view.xml @@ -81,6 +81,15 @@ app:track="@drawable/player_switch_track" app:trackTint="#88ffffff" /> + + auto + + sponsor + intro + selfpromo + interaction + outro + filler + music_offtopic + preview + + + + @string/category_sponsor + @string/category_intro + @string/category_selfpromo + @string/category_interaction + @string/category_outro + @string/category_filler + @string/category_music_offtopic + @string/category_preview + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 05a9113eb..07df94dbd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -468,6 +468,10 @@ auto-generated limited Registration disabled + Create segment + Segment type + Invalid segment start or end + Download Service Shows a notification when downloading media.