feat: support for up/downvoting SponsorBlock segments

This commit is contained in:
Bnyro 2023-10-12 19:53:49 +02:00
parent f84ec13f58
commit e636c966c6
6 changed files with 177 additions and 3 deletions

View File

@ -9,7 +9,7 @@ import retrofit2.http.Query
import retrofit2.http.Url import retrofit2.http.Url
private const val GITHUB_API_URL = "https://api.github.com/repos/libre-tube/LibreTube/releases/latest" private const val GITHUB_API_URL = "https://api.github.com/repos/libre-tube/LibreTube/releases/latest"
private const val SB_SUBMIT_API_URL = "https://sponsor.ajay.app/api/skipSegments" private const val SB_API_URL = "https://sponsor.ajay.app"
interface ExternalApi { interface ExternalApi {
// only for fetching servers list // only for fetching servers list
@ -20,7 +20,7 @@ interface ExternalApi {
@GET(GITHUB_API_URL) @GET(GITHUB_API_URL)
suspend fun getUpdateInfo(): UpdateInfo suspend fun getUpdateInfo(): UpdateInfo
@POST(SB_SUBMIT_API_URL) @POST("$SB_API_URL/api/skipSegments")
suspend fun submitSegment( suspend fun submitSegment(
@Query("videoID") videoId: String, @Query("videoID") videoId: String,
@Query("startTime") startTime: Float, @Query("startTime") startTime: Float,
@ -31,4 +31,14 @@ interface ExternalApi {
@Query("duration") duration: Float? = null, @Query("duration") duration: Float? = null,
@Query("description") description: String = "" @Query("description") description: String = ""
): List<SubmitSegmentResponse> ): List<SubmitSegmentResponse>
/**
* @param score: 0 for downvote, 1 for upvote, 20 for undoing previous vote (if existent)
*/
@POST("$SB_API_URL/api/voteOnSponsorTime")
suspend fun voteOnSponsorTime(
@Query("UUID") uuid: String,
@Query("userID") userID: String,
@Query("type") score: Int
)
} }

View File

@ -1,10 +1,11 @@
package com.github.libretube.api.obj package com.github.libretube.api.obj
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Segment( data class Segment(
val UUID: String? = null, @SerialName("UUID") val uuid: String? = null,
val actionType: String? = null, val actionType: String? = null,
val category: String? = null, val category: String? = null,
val description: String? = null, val description: String? = null,

View File

@ -5,6 +5,7 @@ import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.libretube.BuildConfig import com.github.libretube.BuildConfig
@ -52,6 +53,11 @@ class SubmitSegmentDialog : DialogFragment() {
.setView(binding.root) .setView(binding.root)
.setPositiveButton(R.string.okay, null) .setPositiveButton(R.string.okay, null)
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.vote_for_segment) { _, _ ->
VoteForSegmentDialog().apply {
arguments = bundleOf(IntentData.videoId to videoId)
}.show(parentFragmentManager, null)
}
.show() .show()
.apply { .apply {
getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {

View File

@ -0,0 +1,107 @@
package com.github.libretube.ui.dialogs
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.text.format.DateUtils
import android.util.Log
import android.widget.ArrayAdapter
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.Segment
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.DialogVoteSegmentBinding
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
class VoteForSegmentDialog : DialogFragment() {
private lateinit var videoId: String
private var _binding: DialogVoteSegmentBinding? = null
private var segments: List<Segment> = listOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
videoId = arguments?.getString(IntentData.videoId, "")!!
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DialogVoteSegmentBinding.inflate(layoutInflater)
lifecycleScope.launch(Dispatchers.IO) {
fetchSegments()
}
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.vote_for_segment)
.setView(_binding?.root)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.okay, null)
.show()
.apply {
getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
val binding = _binding ?: return@setOnClickListener
val segmentID = segments.getOrNull(binding.segmentsDropdown.selectedItemPosition)
?.uuid ?: return@setOnClickListener
// see https://wiki.sponsor.ajay.app/w/API_Docs#POST_/api/voteOnSponsorTime
val score = if (binding.upvote.isChecked) 1
else if (binding.downvote.isChecked) 0
else 20
dialog?.hide()
lifecycleScope.launch(Dispatchers.IO) {
try {
RetrofitInstance.externalApi.voteOnSponsorTime(
uuid = segmentID,
userID = PreferenceHelper.getSponsorBlockUserID(),
score = score
)
context.toastFromMainDispatcher(R.string.success)
} catch (e: Exception) {
context.toastFromMainDispatcher(e.localizedMessage.orEmpty())
}
withContext(Dispatchers.Main) { dialog?.dismiss() }
}
}
}
}
private suspend fun fetchSegments() {
val categories = resources.getStringArray(R.array.sponsorBlockSegments).toList()
segments = try {
RetrofitInstance.api.getSegments(videoId, JsonHelper.json.encodeToString(categories)).segments
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return
}
withContext(Dispatchers.Main) {
val binding = _binding ?: return@withContext
val segmentTexts = segments.map {
"${it.category} (${
DateUtils.formatElapsedTime(it.segmentStartAndEnd.first.toLong())
} - ${
DateUtils.formatElapsedTime(it.segmentStartAndEnd.second.toLong())
})"
}
binding.segmentsDropdown.adapter =
ArrayAdapter(requireContext(), R.layout.dropdown_item, segmentTexts)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingHorizontal="12dp">
<com.github.libretube.ui.views.DropdownMenu
android:id="@+id/segments_dropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hint="@string/segment"
app:icon="@drawable/ic_frame" />
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="horizontal">
<RadioButton
android:id="@+id/upvote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:checked="true"
android:text="@string/upvote" />
<RadioButton
android:id="@+id/downvote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:text="@string/downvote" />
<RadioButton
android:id="@+id/undo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/undo" />
</RadioGroup>
</LinearLayout>

View File

@ -452,6 +452,11 @@
<string name="playback_during_call">Continue playback during phone call</string> <string name="playback_during_call">Continue playback during phone call</string>
<string name="playback_during_call_summary">Note that this also affects the app to not handle any kind of audio focus anymore.</string> <string name="playback_during_call_summary">Note that this also affects the app to not handle any kind of audio focus anymore.</string>
<string name="segment_submitted">Segment submitted</string> <string name="segment_submitted">Segment submitted</string>
<string name="vote_for_segment">Vote for segment</string>
<string name="upvote">Up</string>
<string name="downvote">Down</string>
<string name="undo">Undo</string>
<string name="segment">Segment</string>
<!-- Backup & Restore Settings --> <!-- Backup & Restore Settings -->
<string name="import_subscriptions_from">Import subscriptions from</string> <string name="import_subscriptions_from">Import subscriptions from</string>