feat: support for contributing to DeArrow

This commit is contained in:
Bnyro 2024-04-11 16:47:15 +02:00
parent feea8ac795
commit cd1eb0f7ff
13 changed files with 279 additions and 16 deletions

View File

@ -1,8 +1,10 @@
package com.github.libretube.api package com.github.libretube.api
import com.github.libretube.api.obj.DeArrowBody
import com.github.libretube.api.obj.PipedInstance import com.github.libretube.api.obj.PipedInstance
import com.github.libretube.api.obj.SubmitSegmentResponse import com.github.libretube.api.obj.SubmitSegmentResponse
import com.github.libretube.obj.update.UpdateInfo import com.github.libretube.obj.update.UpdateInfo
import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Query import retrofit2.http.Query
@ -23,15 +25,18 @@ interface ExternalApi {
@POST("$SB_API_URL/api/skipSegments") @POST("$SB_API_URL/api/skipSegments")
suspend fun submitSegment( suspend fun submitSegment(
@Query("videoID") videoId: String, @Query("videoID") videoId: String,
@Query("userID") userID: String,
@Query("userAgent") userAgent: String,
@Query("startTime") startTime: Float, @Query("startTime") startTime: Float,
@Query("endTime") endTime: Float, @Query("endTime") endTime: Float,
@Query("category") category: String, @Query("category") category: String,
@Query("userAgent") userAgent: String,
@Query("userID") userID: String,
@Query("duration") duration: Float? = null, @Query("duration") duration: Float? = null,
@Query("description") description: String = "" @Query("description") description: String = ""
): List<SubmitSegmentResponse> ): List<SubmitSegmentResponse>
@POST("$SB_API_URL/api/branding")
suspend fun submitDeArrow(@Body body: DeArrowBody)
/** /**
* @param score: 0 for downvote, 1 for upvote, 20 for undoing previous vote (if existent) * @param score: 0 for downvote, 1 for upvote, 20 for undoing previous vote (if existent)
*/ */

View File

@ -0,0 +1,23 @@
package com.github.libretube.api.obj
import kotlinx.serialization.Serializable
@Serializable
data class DeArrowBody(
val videoID: String,
val userID: String,
val userAgent: String,
val title: DeArrowSubmitTitle?,
val thumbnail: DeArrowSubmitThumbnail?,
val downvote: Boolean = false
)
@Serializable
data class DeArrowSubmitTitle(
val title: String
)
@Serializable
data class DeArrowSubmitThumbnail(
val timestamp: Float
)

View File

@ -146,6 +146,7 @@ object PreferenceKeys {
const val MAX_CONCURRENT_DOWNLOADS = "max_parallel_downloads" const val MAX_CONCURRENT_DOWNLOADS = "max_parallel_downloads"
const val DISABLE_VIDEO_IMAGE_PROXY = "disable_video_image_proxy" const val DISABLE_VIDEO_IMAGE_PROXY = "disable_video_image_proxy"
const val CONTRIBUTE_TO_SB = "sb_contribute_key" const val CONTRIBUTE_TO_SB = "sb_contribute_key"
const val CONTRIBUTE_TO_DEARROW = "dearrow_contribute_key"
/** /**
* History * History

View File

@ -0,0 +1,103 @@
package com.github.libretube.ui.dialogs
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.DeArrowBody
import com.github.libretube.api.obj.DeArrowSubmitThumbnail
import com.github.libretube.api.obj.DeArrowSubmitTitle
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.DialogSubmitDearrowBinding
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SubmitDeArrowDialog: DialogFragment() {
private var videoId: String = ""
private var currentPosition: Long = 0
private var _binding: DialogSubmitDearrowBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
videoId = it.getString(IntentData.videoId)!!
currentPosition = it.getLong(IntentData.currentPosition)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DialogSubmitDearrowBinding.inflate(layoutInflater)
binding.dearrowTitle.typingEnabled = true
binding.thumbnailTime.setText(currentPosition.toString())
binding.titleCheckbox.setOnCheckedChangeListener { _, isChecked ->
binding.dearrowTitle.isEnabled = isChecked
}
binding.thumbnailTimeCheckbox.setOnCheckedChangeListener { _, isChecked ->
binding.thumbnailTimeInputLayout.isEnabled = isChecked
}
lifecycleScope.launch { fetchDeArrowData() }
return MaterialAlertDialogBuilder(requireContext())
.setView(binding.root)
.setPositiveButton(R.string.okay, null)
.setNegativeButton(R.string.cancel, null)
.show()
.apply {
getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
lifecycleScope.launch { submitDeArrow() }
}
}
}
private suspend fun fetchDeArrowData() {
val data = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getDeArrowContent(videoId)
}.getOrElse(videoId) { return }
} catch (e: Exception) {
return
}
binding.dearrowTitle.items = data.titles.map { it.title }
}
private suspend fun submitDeArrow() {
val context = requireContext().applicationContext
requireDialog().hide()
val userID = PreferenceHelper.getSponsorBlockUserID()
val userAgent = TextUtils.getUserAgent(context)
val title = binding.dearrowTitle.text
.takeIf { it.isNotEmpty() && binding.titleCheckbox.isChecked }
?.let { DeArrowSubmitTitle(it) }
val thumbnail = binding.thumbnailTime.text.toString().toFloatOrNull()
?.takeIf { binding.thumbnailTimeCheckbox.isChecked }
?.let { DeArrowSubmitThumbnail(it) }
val requestBody = DeArrowBody(videoId, userID, userAgent, title, thumbnail)
try {
// https://wiki.sponsor.ajay.app/w/API_Docs/DeArrow
withContext(Dispatchers.IO) {
RetrofitInstance.externalApi.submitDeArrow(requestBody)
}
context.toastFromMainDispatcher(R.string.success)
} catch (e: Exception) {
context.toastFromMainDispatcher(e.localizedMessage.orEmpty())
}
dialog?.dismiss()
}
}

View File

@ -17,6 +17,7 @@ import com.github.libretube.databinding.DialogSubmitSegmentBinding
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.Exception import java.lang.Exception
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -86,14 +87,14 @@ class SubmitSegmentDialog : DialogFragment() {
val categories = resources.getStringArray(R.array.sponsorBlockSegments) val categories = resources.getStringArray(R.array.sponsorBlockSegments)
val category = categories[binding.segmentCategory.selectedItemPosition] val category = categories[binding.segmentCategory.selectedItemPosition]
val userAgent = "${context.packageName}/${BuildConfig.VERSION_NAME}" val userAgent = TextUtils.getUserAgent(context)
val uuid = PreferenceHelper.getSponsorBlockUserID() val uuid = PreferenceHelper.getSponsorBlockUserID()
val duration = duration?.let { it.toFloat() / 1000 } val duration = duration?.let { it.toFloat() / 1000 }
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
RetrofitInstance.externalApi RetrofitInstance.externalApi
.submitSegment(videoId, startTime, endTime, category, userAgent, uuid, duration) .submitSegment(videoId, uuid, userAgent, startTime, endTime, category, duration)
} }
context.toastFromMainDispatcher(R.string.segment_submitted) context.toastFromMainDispatcher(R.string.segment_submitted)
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -1,6 +1,7 @@
package com.github.libretube.ui.views package com.github.libretube.ui.views
import android.content.Context import android.content.Context
import android.text.InputType
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
@ -37,6 +38,22 @@ class DropdownMenu(
get() = adapter.getPosition(binding.autoCompleteTextView.text.toString()) get() = adapter.getPosition(binding.autoCompleteTextView.text.toString())
set(index) = binding.autoCompleteTextView.setText(adapter.getItem(index), false) set(index) = binding.autoCompleteTextView.setText(adapter.getItem(index), false)
val text get() = binding.autoCompleteTextView.text.toString()
override fun setEnabled(enabled: Boolean) {
binding.textInputLayout.isEnabled = enabled
}
override fun isEnabled(): Boolean {
return binding.textInputLayout.isEnabled
}
var typingEnabled: Boolean
set(enabled) {
binding.autoCompleteTextView.inputType = if (enabled) InputType.TYPE_CLASS_TEXT else InputType.TYPE_NULL
}
get() = binding.autoCompleteTextView.inputType != InputType.TYPE_NULL
init { init {
context.obtainStyledAttributes(attributeSet, R.styleable.DropdownMenu, 0, 0).use { context.obtainStyledAttributes(attributeSet, R.styleable.DropdownMenu, 0, 0).use {
binding.textInputLayout.hint = it.getString(R.styleable.DropdownMenu_hint) binding.textInputLayout.hint = it.getString(R.styleable.DropdownMenu_hint)

View File

@ -1,6 +1,7 @@
package com.github.libretube.ui.views package com.github.libretube.ui.views
import android.content.Context import android.content.Context
import android.os.Bundle
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Window import android.view.Window
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
@ -19,6 +20,7 @@ import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.helpers.WindowHelper import com.github.libretube.helpers.WindowHelper
import com.github.libretube.obj.BottomSheetItem import com.github.libretube.obj.BottomSheetItem
import com.github.libretube.ui.base.BaseActivity import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.dialogs.SubmitDeArrowDialog
import com.github.libretube.ui.dialogs.SubmitSegmentDialog import com.github.libretube.ui.dialogs.SubmitSegmentDialog
import com.github.libretube.ui.interfaces.OnlinePlayerOptions import com.github.libretube.ui.interfaces.OnlinePlayerOptions
import com.github.libretube.ui.models.PlayerViewModel import com.github.libretube.ui.models.PlayerViewModel
@ -162,19 +164,29 @@ class OnlinePlayerView(
binding.sbSubmit.isVisible = PreferenceHelper.getBoolean(PreferenceKeys.CONTRIBUTE_TO_SB, false) binding.sbSubmit.isVisible = PreferenceHelper.getBoolean(PreferenceKeys.CONTRIBUTE_TO_SB, false)
binding.sbSubmit.setOnClickListener { binding.sbSubmit.setOnClickListener {
val currentPosition = player?.currentPosition?.takeIf { it != C.TIME_UNSET } ?: 0 val submitSegmentDialog = SubmitSegmentDialog()
val duration = player?.duration?.takeIf { it != C.TIME_UNSET } submitSegmentDialog.arguments = buildSbBundleArgs() ?: return@setOnClickListener
val videoId = PlayingQueue.getCurrent()?.url?.toID() ?: return@setOnClickListener submitSegmentDialog.show((context as BaseActivity).supportFragmentManager, null)
val bundle = bundleOf(
IntentData.currentPosition to currentPosition,
IntentData.duration to duration,
IntentData.videoId to videoId
)
val newSubmitSegmentDialog = SubmitSegmentDialog()
newSubmitSegmentDialog.arguments = bundle
newSubmitSegmentDialog.show((context as BaseActivity).supportFragmentManager, null)
} }
binding.dearrowSubmit.isVisible = PreferenceHelper.getBoolean(PreferenceKeys.CONTRIBUTE_TO_DEARROW, false)
binding.dearrowSubmit.setOnClickListener {
val submitDialog = SubmitDeArrowDialog()
submitDialog.arguments = buildSbBundleArgs() ?: return@setOnClickListener
submitDialog.show((context as BaseActivity).supportFragmentManager, null)
}
}
private fun buildSbBundleArgs(): Bundle? {
val currentPosition = player?.currentPosition?.takeIf { it != C.TIME_UNSET } ?: 0
val duration = player?.duration?.takeIf { it != C.TIME_UNSET }
val videoId = PlayingQueue.getCurrent()?.url?.toID() ?: return null
return bundleOf(
IntentData.currentPosition to currentPosition,
IntentData.duration to duration,
IntentData.videoId to videoId
)
} }
override fun getWindow(): Window = currentWindow ?: activity.window override fun getWindow(): Window = currentWindow ?: activity.window

View File

@ -5,6 +5,7 @@ import android.icu.text.RelativeDateTimeFormatter
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.text.format.DateUtils import android.text.format.DateUtils
import com.github.libretube.BuildConfig
import com.github.libretube.R import com.github.libretube.R
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
@ -98,4 +99,8 @@ object TextUtils {
if (text.length <= maxLength) return text if (text.length <= maxLength) return text
return text.take(maxLength) + "" return text.take(maxLength) + ""
} }
fun getUserAgent(context: Context): String {
return "${context.packageName}/${BuildConfig.VERSION_NAME}"
}
} }

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:fillColor="#00000000"
android:pathData="m34.221,18.268c0,4.489 -2.217,8.289 -5.096,11.231 -2.879,2.941 -6.599,4.643 -10.994,4.643 -4.394,0 -8.799,-1.002 -11.678,-3.944 -2.879,-2.941 -4.674,-7.441 -4.674,-11.93 0,-4.489 2.32,-8.453 5.199,-11.395 2.879,-2.941 6.761,-5.099 11.154,-5.099 4.393,0 8.466,1.963 11.345,4.904 2.88,2.941 4.745,7.101 4.745,11.589z"
android:strokeWidth="2.96196"
android:strokeColor="#000000" />
<path
android:fillColor="#000000"
android:pathData="m21.223,17.841c0,1.643 -1.378,3.027 -3.022,3.027 -1.643,0 -2.918,-1.383 -2.918,-3.027 0,-1.643 1.275,-2.982 2.918,-2.982 1.643,0 3.022,1.338 3.022,2.982z"
android:strokeWidth="0.616691" />
</vector>

View File

@ -0,0 +1,59 @@
<?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="15dp"
android:paddingTop="15dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/contribute_to_dearrow"
android:textSize="24sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/title_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:checked="true"
android:text="@string/title" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/thumbnail_time_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/thumbnail_time" />
</LinearLayout>
<com.github.libretube.ui.views.DropdownMenu
android:id="@+id/dearrow_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hint="@string/title"
app:icon="@drawable/ic_label" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/thumbnail_time_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/thumbnail_time">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/thumbnail_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@ -81,6 +81,15 @@
app:track="@drawable/player_switch_track" app:track="@drawable/player_switch_track"
app:trackTint="#88ffffff" /> app:trackTint="#88ffffff" />
<ImageButton
android:id="@+id/dearrow_submit"
style="@style/PlayerControlTop"
android:layout_marginEnd="2dp"
android:src="@drawable/ic_dearrow"
android:tooltipText="@string/contribute_to_dearrow"
android:visibility="gone"
app:tint="@android:color/white" />
<ImageButton <ImageButton
android:id="@+id/sb_submit" android:id="@+id/sb_submit"
style="@style/PlayerControlTop" style="@style/PlayerControlTop"

View File

@ -434,6 +434,9 @@
<string name="fallback_piped_proxy_summary">Load videos via the proxy if connecting to YouTube directly doesn\'t work for the current video (increases the initial loading times). If disabled, YouTube music content likely won\'t play due to YT restrictions</string> <string name="fallback_piped_proxy_summary">Load videos via the proxy if connecting to YouTube directly doesn\'t work for the current video (increases the initial loading times). If disabled, YouTube music content likely won\'t play due to YT restrictions</string>
<string name="dearrow">Enable DeArrow</string> <string name="dearrow">Enable DeArrow</string>
<string name="dearrow_summary">Show more accurate and less sensationalist titles and thumbnails. Increases loading times</string> <string name="dearrow_summary">Show more accurate and less sensationalist titles and thumbnails. Increases loading times</string>
<string name="contribute_to_dearrow">Contribute to DeArrow</string>
<string name="title">Title</string>
<string name="thumbnail_time">Thumbnail time</string>
<string name="visibility">Visibility</string> <string name="visibility">Visibility</string>
<string name="visibility_public">Public</string> <string name="visibility_public">Public</string>
<string name="visibility_unlisted">Unlisted</string> <string name="visibility_unlisted">Unlisted</string>

View File

@ -35,6 +35,13 @@
app:summary="@string/dearrow_summary" app:summary="@string/dearrow_summary"
app:title="@string/dearrow" /> app:title="@string/dearrow" />
<SwitchPreferenceCompat
android:summaryOff="@string/disabled"
android:summaryOn="@string/enabled"
app:defaultValue="false"
app:key="dearrow_contribute_key"
app:title="@string/contribute_to_dearrow" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:defaultValue="true" app:defaultValue="true"
app:key="sb_highlights" app:key="sb_highlights"