mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-29 00:10:32 +05:30
feat: support for up/downvoting SponsorBlock segments
This commit is contained in:
parent
f84ec13f58
commit
e636c966c6
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
45
app/src/main/res/layout/dialog_vote_segment.xml
Normal file
45
app/src/main/res/layout/dialog_vote_segment.xml
Normal 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>
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user