mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge remote-tracking branch 'origin/master' into batteryoptimization
This commit is contained in:
commit
1fa4807716
11 changed files with 120 additions and 48 deletions
|
@ -230,7 +230,7 @@ dependencies {
|
||||||
// Downloading & Networking
|
// Downloading & Networking
|
||||||
implementation("androidx.work:work-runtime:2.9.0")
|
implementation("androidx.work:work-runtime:2.9.0")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||||
implementation("com.github.Blatzar:NiceHttp:0.4.5") // HTTP Lib
|
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("androidSourcesJar", Jar::class) {
|
tasks.register("androidSourcesJar", Jar::class) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.base64DecodeArray
|
import com.lagradost.cloudstream3.base64DecodeArray
|
||||||
|
import com.lagradost.cloudstream3.base64Encode
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
@ -16,13 +17,52 @@ import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
// Code found in https://github.com/theonlymo/keys
|
|
||||||
// special credits to @theonlymo for providing key
|
|
||||||
class Megacloud : Rabbitstream() {
|
class Megacloud : Rabbitstream() {
|
||||||
override val name = "Megacloud"
|
override val name = "Megacloud"
|
||||||
override val mainUrl = "https://megacloud.tv"
|
override val mainUrl = "https://megacloud.tv"
|
||||||
override val embed = "embed-2/ajax/e-1"
|
override val embed = "embed-2/ajax/e-1"
|
||||||
override val key = "https://raw.githubusercontent.com/theonlymo/keys/e1/key"
|
private val scriptUrl = "$mainUrl/js/player/a/prod/e1-player.min.js"
|
||||||
|
|
||||||
|
override suspend fun extractRealKey(sources: String): Pair<String, String> {
|
||||||
|
val rawKeys = getKeys()
|
||||||
|
val sourcesArray = sources.toCharArray()
|
||||||
|
|
||||||
|
var extractedKey = ""
|
||||||
|
var currentIndex = 0
|
||||||
|
for (index in rawKeys) {
|
||||||
|
val start = index[0] + currentIndex
|
||||||
|
val end = start + index[1]
|
||||||
|
for (i in start until end) {
|
||||||
|
extractedKey += sourcesArray[i].toString()
|
||||||
|
sourcesArray[i] = ' '
|
||||||
|
}
|
||||||
|
currentIndex += index[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractedKey to sourcesArray.joinToString("").replace(" ", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getKeys(): List<List<Int>> {
|
||||||
|
val script = app.get(scriptUrl).text
|
||||||
|
fun matchingKey(value: String): String {
|
||||||
|
return Regex(",$value=((?:0x)?([0-9a-fA-F]+))").find(script)?.groupValues?.get(1)
|
||||||
|
?.removePrefix("0x") ?: throw ErrorLoadingException("Failed to match the key")
|
||||||
|
}
|
||||||
|
|
||||||
|
val regex = Regex("case\\s*0x[0-9a-f]+:(?![^;]*=partKey)\\s*\\w+\\s*=\\s*(\\w+)\\s*,\\s*\\w+\\s*=\\s*(\\w+);")
|
||||||
|
val indexPairs = regex.findAll(script).toList().map { match ->
|
||||||
|
val matchKey1 = matchingKey(match.groupValues[1])
|
||||||
|
val matchKey2 = matchingKey(match.groupValues[2])
|
||||||
|
try {
|
||||||
|
listOf(matchKey1.toInt(16), matchKey2.toInt(16))
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}.filter { it.isNotEmpty() }
|
||||||
|
|
||||||
|
return indexPairs
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Dokicloud : Rabbitstream() {
|
class Dokicloud : Rabbitstream() {
|
||||||
|
@ -30,12 +70,14 @@ class Dokicloud : Rabbitstream() {
|
||||||
override val mainUrl = "https://dokicloud.one"
|
override val mainUrl = "https://dokicloud.one"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Code found in https://github.com/eatmynerds/key
|
||||||
|
// special credits to @eatmynerds for providing key
|
||||||
open class Rabbitstream : ExtractorApi() {
|
open class Rabbitstream : ExtractorApi() {
|
||||||
override val name = "Rabbitstream"
|
override val name = "Rabbitstream"
|
||||||
override val mainUrl = "https://rabbitstream.net"
|
override val mainUrl = "https://rabbitstream.net"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
open val embed = "ajax/embed-4"
|
open val embed = "ajax/embed-4"
|
||||||
open val key = "https://raw.githubusercontent.com/theonlymo/keys/e4/key"
|
open val key = "https://raw.githubusercontent.com/eatmynerds/key/e4/key.txt"
|
||||||
|
|
||||||
override suspend fun getUrl(
|
override suspend fun getUrl(
|
||||||
url: String,
|
url: String,
|
||||||
|
@ -47,7 +89,7 @@ open class Rabbitstream : ExtractorApi() {
|
||||||
|
|
||||||
val response = app.get(
|
val response = app.get(
|
||||||
"$mainUrl/$embed/getSources?id=$id",
|
"$mainUrl/$embed/getSources?id=$id",
|
||||||
referer = url,
|
referer = mainUrl,
|
||||||
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,7 +98,7 @@ open class Rabbitstream : ExtractorApi() {
|
||||||
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
|
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
|
||||||
response.parsedSafe()
|
response.parsedSafe()
|
||||||
} else {
|
} else {
|
||||||
val (key, encData) = extractRealKey(sources, getRawKey())
|
val (key, encData) = extractRealKey(sources)
|
||||||
val decrypted = decryptMapped<List<Sources>>(encData, key)
|
val decrypted = decryptMapped<List<Sources>>(encData, key)
|
||||||
SourcesResponses(
|
SourcesResponses(
|
||||||
sources = decrypted,
|
sources = decrypted,
|
||||||
|
@ -72,11 +114,11 @@ open class Rabbitstream : ExtractorApi() {
|
||||||
).forEach(callback)
|
).forEach(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptedSources?.tracks?.filter { it?.kind == "captions" }?.map { track ->
|
decryptedSources?.tracks?.map { track ->
|
||||||
subtitleCallback.invoke(
|
subtitleCallback.invoke(
|
||||||
SubtitleFile(
|
SubtitleFile(
|
||||||
track?.label ?: "",
|
track?.label ?: return@map,
|
||||||
track?.file ?: return@map
|
track.file ?: return@map
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -84,25 +126,10 @@ open class Rabbitstream : ExtractorApi() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getRawKey(): String = app.get(key).text
|
open suspend fun extractRealKey(sources: String): Pair<String, String> {
|
||||||
|
val rawKeys = parseJson<List<Int>>(app.get(key).text)
|
||||||
private fun extractRealKey(sources: String, stops: String): Pair<String, String> {
|
val extractedKey = base64Encode(rawKeys.map { it.toByte() }.toByteArray())
|
||||||
val decryptKey = parseJson<List<List<Int>>>(stops)
|
return extractedKey to sources
|
||||||
val sourcesArray = sources.toCharArray()
|
|
||||||
|
|
||||||
var extractedKey = ""
|
|
||||||
var currentIndex = 0
|
|
||||||
for (index in decryptKey) {
|
|
||||||
val start = index[0] + currentIndex
|
|
||||||
val end = start + index[1]
|
|
||||||
for (i in start until end) {
|
|
||||||
extractedKey += sourcesArray[i].toString()
|
|
||||||
sourcesArray[i] = ' '
|
|
||||||
}
|
|
||||||
currentIndex += index[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return extractedKey to sourcesArray.joinToString("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <reified T> decryptMapped(input: String, key: String): T? {
|
private inline fun <reified T> decryptMapped(input: String, key: String): T? {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import java.net.URI
|
||||||
* @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare.
|
* @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare.
|
||||||
* @param script pass custom js to execute
|
* @param script pass custom js to execute
|
||||||
* @param scriptCallback will be called with the result from custom js
|
* @param scriptCallback will be called with the result from custom js
|
||||||
|
* @param timeout close webview after timeout
|
||||||
* */
|
* */
|
||||||
class WebViewResolver(
|
class WebViewResolver(
|
||||||
val interceptUrl: Regex,
|
val interceptUrl: Regex,
|
||||||
|
@ -38,18 +39,29 @@ class WebViewResolver(
|
||||||
val userAgent: String? = USER_AGENT,
|
val userAgent: String? = USER_AGENT,
|
||||||
val useOkhttp: Boolean = true,
|
val useOkhttp: Boolean = true,
|
||||||
val script: String? = null,
|
val script: String? = null,
|
||||||
val scriptCallback: ((String) -> Unit)? = null
|
val scriptCallback: ((String) -> Unit)? = null,
|
||||||
|
val timeout: Long = DEFAULT_TIMEOUT
|
||||||
) :
|
) :
|
||||||
Interceptor {
|
Interceptor {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
interceptUrl: Regex,
|
||||||
|
additionalUrls: List<Regex> = emptyList(),
|
||||||
|
userAgent: String? = USER_AGENT,
|
||||||
|
useOkhttp: Boolean = true,
|
||||||
|
script: String? = null,
|
||||||
|
scriptCallback: ((String) -> Unit)? = null,
|
||||||
|
) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, script, scriptCallback, DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
interceptUrl: Regex,
|
interceptUrl: Regex,
|
||||||
additionalUrls: List<Regex> = emptyList(),
|
additionalUrls: List<Regex> = emptyList(),
|
||||||
userAgent: String? = USER_AGENT,
|
userAgent: String? = USER_AGENT,
|
||||||
useOkhttp: Boolean = true
|
useOkhttp: Boolean = true
|
||||||
) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null)
|
) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null, DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val DEFAULT_TIMEOUT = 60_000L
|
||||||
var webViewUserAgent: String? = null
|
var webViewUserAgent: String? = null
|
||||||
|
|
||||||
@JvmName("getWebViewUserAgent1")
|
@JvmName("getWebViewUserAgent1")
|
||||||
|
@ -262,7 +274,7 @@ class WebViewResolver(
|
||||||
|
|
||||||
var loop = 0
|
var loop = 0
|
||||||
// Timeouts after this amount, 60s
|
// Timeouts after this amount, 60s
|
||||||
val totalTime = 60000L
|
val totalTime = timeout
|
||||||
|
|
||||||
val delayTime = 100L
|
val delayTime = 100L
|
||||||
|
|
||||||
|
|
|
@ -440,9 +440,9 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
interceptor = interceptor
|
interceptor = interceptor
|
||||||
).isSuccessful
|
).isSuccessful
|
||||||
} else {
|
} else {
|
||||||
val statusResponse = status?.let { setStatus ->
|
val statusResponse = this.status?.let { setStatus ->
|
||||||
val newStatus =
|
val newStatus =
|
||||||
SimklListStatusType.values()
|
SimklListStatusType.entries
|
||||||
.firstOrNull { it.value == setStatus }?.originalName
|
.firstOrNull { it.value == setStatus }?.originalName
|
||||||
?: SimklListStatusType.Watching.originalName!!
|
?: SimklListStatusType.Watching.originalName!!
|
||||||
|
|
||||||
|
@ -479,9 +479,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
).isSuccessful
|
).isSuccessful
|
||||||
} ?: true
|
} ?: true
|
||||||
|
|
||||||
|
// You cannot rate if you are planning to watch it.
|
||||||
|
val shouldRate =
|
||||||
|
score != null && status != SimklListStatusType.Planning.value
|
||||||
|
val realScore = if (shouldRate) score else null
|
||||||
|
|
||||||
val historyResponse =
|
val historyResponse =
|
||||||
// Only post if there are episodes or score to upload
|
// Only post if there are episodes or score to upload
|
||||||
if (addEpisodes != null || score != null) {
|
if (addEpisodes != null || shouldRate) {
|
||||||
app.post(
|
app.post(
|
||||||
"${this.url}/sync/history",
|
"${this.url}/sync/history",
|
||||||
json = StatusRequest(
|
json = StatusRequest(
|
||||||
|
@ -492,8 +497,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
ids,
|
ids,
|
||||||
addEpisodes?.first,
|
addEpisodes?.first,
|
||||||
addEpisodes?.second,
|
addEpisodes?.second,
|
||||||
score,
|
realScore,
|
||||||
score?.let { time },
|
realScore?.let { time },
|
||||||
)
|
)
|
||||||
), movies = emptyList()
|
), movies = emptyList()
|
||||||
),
|
),
|
||||||
|
@ -827,7 +832,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
if (foundItem != null) {
|
if (foundItem != null) {
|
||||||
return SimklSyncStatus(
|
return SimklSyncStatus(
|
||||||
status = foundItem.status?.let { SyncWatchType.fromInternalId(SimklListStatusType.fromString(it)?.value) }
|
status = foundItem.status?.let {
|
||||||
|
SyncWatchType.fromInternalId(
|
||||||
|
SimklListStatusType.fromString(
|
||||||
|
it
|
||||||
|
)?.value
|
||||||
|
)
|
||||||
|
}
|
||||||
?: return null,
|
?: return null,
|
||||||
score = foundItem.user_rating,
|
score = foundItem.user_rating,
|
||||||
watchedEpisodes = foundItem.watched_episodes_count,
|
watchedEpisodes = foundItem.watched_episodes_count,
|
||||||
|
@ -859,8 +870,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val builder = SimklScoreBuilder.Builder()
|
val builder = SimklScoreBuilder.Builder()
|
||||||
.apiUrl(this.mainUrl)
|
.apiUrl(this.mainUrl)
|
||||||
.score(status.score, simklStatus?.oldScore)
|
.score(status.score, simklStatus?.oldScore)
|
||||||
.status(status.status.internalId, (status as? SimklSyncStatus)?.oldStatus?.let { oldStatus ->
|
.status(
|
||||||
SimklListStatusType.values().firstOrNull {
|
status.status.internalId,
|
||||||
|
(status as? SimklSyncStatus)?.oldStatus?.let { oldStatus ->
|
||||||
|
SimklListStatusType.entries.firstOrNull {
|
||||||
it.originalName == oldStatus
|
it.originalName == oldStatus
|
||||||
}?.value
|
}?.value
|
||||||
})
|
})
|
||||||
|
@ -996,7 +1009,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val list = getSyncListSmart() ?: return null
|
val list = getSyncListSmart() ?: return null
|
||||||
|
|
||||||
val baseMap =
|
val baseMap =
|
||||||
SimklListStatusType.values()
|
SimklListStatusType.entries
|
||||||
.filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value }
|
.filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value }
|
||||||
.associate {
|
.associate {
|
||||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||||
|
|
|
@ -317,7 +317,7 @@ class HomeParentItemAdapterPreview(
|
||||||
homePreviewText.text = item.name
|
homePreviewText.text = item.name
|
||||||
populateChips(
|
populateChips(
|
||||||
homePreviewTags,
|
homePreviewTags,
|
||||||
item.tags ?: emptyList(),
|
item.tags?.take(6) ?: emptyList(),
|
||||||
R.style.ChipFilledSemiTransparent
|
R.style.ChipFilledSemiTransparent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -684,6 +684,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
||||||
resultMetaYear.setText(d.yearText)
|
resultMetaYear.setText(d.yearText)
|
||||||
resultMetaDuration.setText(d.durationText)
|
resultMetaDuration.setText(d.durationText)
|
||||||
resultMetaRating.setText(d.ratingText)
|
resultMetaRating.setText(d.ratingText)
|
||||||
|
resultMetaStatus.setText(d.onGoingText)
|
||||||
resultMetaContentRating.setText(d.contentRatingText)
|
resultMetaContentRating.setText(d.contentRatingText)
|
||||||
resultCastText.setText(d.actorsText)
|
resultCastText.setText(d.actorsText)
|
||||||
resultNextAiring.setText(d.nextAiringEpisode)
|
resultNextAiring.setText(d.nextAiringEpisode)
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
package com.lagradost.cloudstream3.ui.settings.extensions
|
package com.lagradost.cloudstream3.ui.settings.extensions
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||||
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
|
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
|
||||||
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
|
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
|
||||||
|
@ -112,6 +119,17 @@ class RepoAdapter(
|
||||||
repositoryItemRoot.setOnClickListener {
|
repositoryItemRoot.setOnClickListener {
|
||||||
clickCallback(repositoryData)
|
clickCallback(repositoryData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repositoryItemRoot.setOnLongClickListener {
|
||||||
|
val clipboardManager =
|
||||||
|
activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?
|
||||||
|
clipboardManager?.setPrimaryClip(ClipData.newPlainText("RepoUrl", repositoryData.url))
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
|
showToast(R.string.copyRepoUrl, Toast.LENGTH_SHORT)
|
||||||
|
}
|
||||||
|
return@setOnLongClickListener true
|
||||||
|
}
|
||||||
|
|
||||||
mainText.text = repositoryData.name
|
mainText.text = repositoryData.name
|
||||||
subText.text = repositoryData.url
|
subText.text = repositoryData.url
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import com.lagradost.cloudstream3.utils.TestingUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import okhttp3.internal.toImmutableList
|
|
||||||
|
|
||||||
class TestViewModel : ViewModel() {
|
class TestViewModel : ViewModel() {
|
||||||
data class TestProgress(
|
data class TestProgress(
|
||||||
|
|
|
@ -137,7 +137,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="5"
|
android:maxLines="3"
|
||||||
android:paddingBottom="5dp"
|
android:paddingBottom="5dp"
|
||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
tools:text="very nice tv series" />
|
tools:text="very nice tv series" />
|
||||||
|
|
|
@ -387,6 +387,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
||||||
<com.lagradost.cloudstream3.widget.FlowLayout
|
<com.lagradost.cloudstream3.widget.FlowLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
app:itemSpacing="10dp">
|
app:itemSpacing="10dp">
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
|
|
|
@ -174,6 +174,7 @@
|
||||||
<string name="sort_clear">Clear</string>
|
<string name="sort_clear">Clear</string>
|
||||||
<string name="sort_save">Save</string>
|
<string name="sort_save">Save</string>
|
||||||
<string name="copyTitle">Title copied!</string>
|
<string name="copyTitle">Title copied!</string>
|
||||||
|
<string name="copyRepoUrl">Repo URL copied!</string>
|
||||||
<string name="subscribe_tooltip">New episode notification</string>
|
<string name="subscribe_tooltip">New episode notification</string>
|
||||||
<string name="result_search_tooltip">Search in other extensions</string>
|
<string name="result_search_tooltip">Search in other extensions</string>
|
||||||
<string name="recommendations_tooltip">Show recommendations</string>
|
<string name="recommendations_tooltip">Show recommendations</string>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue