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 42d837fa2..0ca6ee5ff 100644 --- a/app/src/main/java/com/github/libretube/api/ExternalApi.kt +++ b/app/src/main/java/com/github/libretube/api/ExternalApi.kt @@ -9,7 +9,7 @@ import retrofit2.http.Query import retrofit2.http.Url 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 { // only for fetching servers list @@ -20,7 +20,7 @@ interface ExternalApi { @GET(GITHUB_API_URL) suspend fun getUpdateInfo(): UpdateInfo - @POST(SB_SUBMIT_API_URL) + @POST("$SB_API_URL/api/skipSegments") suspend fun submitSegment( @Query("videoID") videoId: String, @Query("startTime") startTime: Float, @@ -31,4 +31,14 @@ interface ExternalApi { @Query("duration") duration: Float? = null, @Query("description") description: String = "" ): List + + /** + * @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 + ) } diff --git a/app/src/main/java/com/github/libretube/api/obj/Segment.kt b/app/src/main/java/com/github/libretube/api/obj/Segment.kt index 95410f2ae..7e122cade 100644 --- a/app/src/main/java/com/github/libretube/api/obj/Segment.kt +++ b/app/src/main/java/com/github/libretube/api/obj/Segment.kt @@ -1,10 +1,11 @@ package com.github.libretube.api.obj +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class Segment( - val UUID: String? = null, + @SerialName("UUID") val uuid: String? = null, val actionType: String? = null, val category: String? = null, val description: String? = null, 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 index f1be3ba5b..2e998b3b4 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/SubmitSegmentDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/SubmitSegmentDialog.kt @@ -5,6 +5,7 @@ import android.content.DialogInterface import android.os.Bundle import android.util.Log import android.widget.ArrayAdapter +import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import com.github.libretube.BuildConfig @@ -52,6 +53,11 @@ class SubmitSegmentDialog : DialogFragment() { .setView(binding.root) .setPositiveButton(R.string.okay, null) .setNegativeButton(R.string.cancel, null) + .setNeutralButton(R.string.vote_for_segment) { _, _ -> + VoteForSegmentDialog().apply { + arguments = bundleOf(IntentData.videoId to videoId) + }.show(parentFragmentManager, null) + } .show() .apply { getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/VoteForSegmentDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/VoteForSegmentDialog.kt new file mode 100644 index 000000000..f040eb7db --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/dialogs/VoteForSegmentDialog.kt @@ -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 = 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 + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_vote_segment.xml b/app/src/main/res/layout/dialog_vote_segment.xml new file mode 100644 index 000000000..3c2e57b7e --- /dev/null +++ b/app/src/main/res/layout/dialog_vote_segment.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 51ce03222..8e8a90bc2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -452,6 +452,11 @@ Continue playback during phone call Note that this also affects the app to not handle any kind of audio focus anymore. Segment submitted + Vote for segment + Up + Down + Undo + Segment Import subscriptions from