Add extension (#1447)
This commit is contained in:
2
src/all/googledriveindex/AndroidManifest.xml
Normal file
2
src/all/googledriveindex/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.animeextension" />
|
13
src/all/googledriveindex/build.gradle
Normal file
13
src/all/googledriveindex/build.gradle
Normal file
@ -0,0 +1,13 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'GoogleDriveIndex'
|
||||
pkgNameSuffix = 'all.googledriveindex'
|
||||
extClass = '.GoogleDriveIndex'
|
||||
extVersionCode = 1
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/all/googledriveindex/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/googledriveindex/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
BIN
src/all/googledriveindex/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/googledriveindex/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
src/all/googledriveindex/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/googledriveindex/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
BIN
src/all/googledriveindex/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/googledriveindex/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
BIN
src/all/googledriveindex/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/googledriveindex/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
src/all/googledriveindex/res/web_hi_res_512.png
Normal file
BIN
src/all/googledriveindex/res/web_hi_res_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
@ -0,0 +1,38 @@
|
||||
package eu.kanade.tachiyomi.animeextension.all.googledriveindex
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ResponseData(
|
||||
val nextPageToken: String? = null,
|
||||
val data: DataObject,
|
||||
) {
|
||||
@Serializable
|
||||
data class DataObject(
|
||||
val files: List<FileObject>,
|
||||
) {
|
||||
@Serializable
|
||||
data class FileObject(
|
||||
val mimeType: String,
|
||||
val id: String,
|
||||
val name: String,
|
||||
val modifiedTime: String? = null,
|
||||
val size: String? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class LinkData(
|
||||
val type: String,
|
||||
val url: String,
|
||||
val info: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class IdUrl(
|
||||
val id: String,
|
||||
val url: String,
|
||||
val referer: String,
|
||||
val type: String,
|
||||
)
|
@ -0,0 +1,497 @@
|
||||
package eu.kanade.tachiyomi.animeextension.all.googledriveindex
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Base64
|
||||
import android.widget.Toast
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class GoogleDriveIndex : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "GoogleDriveIndex"
|
||||
|
||||
override val baseUrl by lazy {
|
||||
preferences.getString("domain_list", "")!!.split(",").first()
|
||||
}
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
private var pageToken: String? = ""
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
if (baseUrl.isEmpty()) {
|
||||
throw Exception("Enter drive path(s) in extension settings.")
|
||||
}
|
||||
|
||||
if (page == 1) pageToken = ""
|
||||
val popHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.add("Host", baseUrl.toHttpUrl().host)
|
||||
.add("Origin", "https://${baseUrl.toHttpUrl().host}")
|
||||
.add("Referer", baseUrl)
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
|
||||
val popBody = "password=&page_token=$pageToken&page_index=${page - 1}".toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
||||
|
||||
return POST(baseUrl, body = popBody, headers = popHeaders)
|
||||
}
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
return parsePage(response, baseUrl)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage = throw Exception("Not used")
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
if (baseUrl.isEmpty()) {
|
||||
throw Exception("Enter drive path(s) in extension settings.")
|
||||
}
|
||||
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val serverFilter = filterList.find { it is ServerFilter } as ServerFilter
|
||||
val serverUrl = serverFilter.toUriPart()
|
||||
|
||||
if (page == 1) pageToken = ""
|
||||
val searchHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.add("Host", serverUrl.toHttpUrl().host)
|
||||
.add("Origin", "https://${serverUrl.toHttpUrl().host}")
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
|
||||
return if (query.isBlank()) {
|
||||
val popBody = "password=&page_token=$pageToken&page_index=${page - 1}".toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
||||
POST(
|
||||
serverUrl,
|
||||
body = popBody,
|
||||
headers = searchHeaders.add("Referer", serverUrl).build(),
|
||||
)
|
||||
} else {
|
||||
val cleanQuery = query.replace(" ", "+")
|
||||
val searchUrl = "https://${serverUrl.toHttpUrl().host}/${serverUrl.toHttpUrl().pathSegments[0]}search"
|
||||
|
||||
val popBody = "q=$cleanQuery&page_token=$pageToken&page_index=${page - 1}".toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
||||
|
||||
POST(
|
||||
searchUrl,
|
||||
body = popBody,
|
||||
headers = searchHeaders.add("Referer", "$searchUrl?q=$cleanQuery").build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
return parsePage(response, response.request.url.toString())
|
||||
}
|
||||
|
||||
// ============================== FILTERS ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("Text search will only search inside selected server"),
|
||||
ServerFilter(getDomains()),
|
||||
)
|
||||
|
||||
private class ServerFilter(domains: Array<Pair<String, String>>) : UriPartFilter(
|
||||
"Select server",
|
||||
domains,
|
||||
)
|
||||
|
||||
private fun getDomains(): Array<Pair<String, String>> {
|
||||
return preferences.getString("domain_list", "")!!.split(",").map {
|
||||
Pair(it.substringAfter("https://"), it)
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
||||
return Observable.just(anime)
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime = throw Exception("Not used")
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
val parsed = json.decodeFromString<LinkData>(anime.url)
|
||||
var counter = 1
|
||||
|
||||
val newParsed = if (parsed.type != "search") {
|
||||
parsed
|
||||
} else {
|
||||
val idParsed = json.decodeFromString<IdUrl>(parsed.url)
|
||||
val id2pathHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.add("Host", idParsed.url.toHttpUrl().host)
|
||||
.add("Origin", "https://${idParsed.url.toHttpUrl().host}")
|
||||
.add("Referer", idParsed.referer)
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
|
||||
val postBody = "id=${idParsed.id}".toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
||||
val slug = client.newCall(
|
||||
POST(idParsed.url + "id2path", body = postBody, headers = id2pathHeaders),
|
||||
).execute().body.string()
|
||||
|
||||
LinkData(
|
||||
idParsed.type,
|
||||
idParsed.url + slug,
|
||||
parsed.info,
|
||||
)
|
||||
}
|
||||
|
||||
if (newParsed.type == "single") {
|
||||
val episode = SEpisode.create()
|
||||
val size = if (newParsed.info == null) {
|
||||
""
|
||||
} else {
|
||||
" - ${newParsed.info}"
|
||||
}
|
||||
episode.name = "${newParsed.url.toHttpUrl().pathSegments.last()}$size"
|
||||
episode.url = newParsed.url
|
||||
episode.episode_number = 1F
|
||||
episodeList.add(episode)
|
||||
}
|
||||
|
||||
if (newParsed.type == "multi") {
|
||||
val basePathCounter = newParsed.url.toHttpUrl().pathSize
|
||||
|
||||
fun traverseDirectory(url: String) {
|
||||
var newToken: String? = ""
|
||||
var newPageIndex = 0
|
||||
|
||||
while (newToken != null) {
|
||||
val popHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.add("Host", url.toHttpUrl().host)
|
||||
.add("Origin", "https://${url.toHttpUrl().host}")
|
||||
.add("Referer", url)
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
|
||||
val popBody = "password=&page_token=$newToken&page_index=$newPageIndex".toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
||||
|
||||
val parsedBody = client.newCall(
|
||||
POST(url, body = popBody, headers = popHeaders),
|
||||
).execute().body.string().decrypt()
|
||||
val parsed = json.decodeFromString<ResponseData>(parsedBody)
|
||||
|
||||
parsed.data.files.forEach { item ->
|
||||
if (item.mimeType.endsWith("folder")) {
|
||||
if (
|
||||
preferences.getString("blacklist_folders", "")!!.split("/")
|
||||
.any { it.equals(item.name, ignoreCase = true) }
|
||||
) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val newUrl = joinUrl(url, item.name).addSuffix("/")
|
||||
traverseDirectory(newUrl)
|
||||
}
|
||||
if (item.mimeType.startsWith("video/")) {
|
||||
val episode = SEpisode.create()
|
||||
val epUrl = joinUrl(url, item.name)
|
||||
val paths = epUrl.toHttpUrl().pathSegments
|
||||
|
||||
// Get season stuff
|
||||
val season = if (paths.size == basePathCounter) {
|
||||
""
|
||||
} else {
|
||||
paths[basePathCounter - 1]
|
||||
}
|
||||
val seasonInfoRegex = """(\([\s\w-]+\))(?: ?\[[\s\w-]+\])?${'$'}""".toRegex()
|
||||
val seasonInfo = if (seasonInfoRegex.containsMatchIn(season)) {
|
||||
"${seasonInfoRegex.find(season)!!.groups[1]!!.value} • "
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val seasonText = if (season.isBlank()) {
|
||||
""
|
||||
} else {
|
||||
"[${season.trimInfo()}] "
|
||||
}
|
||||
|
||||
// Get other info
|
||||
val extraInfo = if (paths.size > basePathCounter) {
|
||||
"/" + paths.subList(basePathCounter - 1, paths.size - 1).joinToString("/") { it.trimInfo() }
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val size = item.size?.toLongOrNull()?.let { formatFileSize(it) }
|
||||
|
||||
episode.name = "$seasonText${item.name.trimInfo()}${if (size == null) "" else " - $size"}"
|
||||
episode.url = epUrl
|
||||
episode.scanlator = seasonInfo + extraInfo
|
||||
episode.episode_number = counter.toFloat()
|
||||
counter++
|
||||
|
||||
episodeList.add(episode)
|
||||
}
|
||||
}
|
||||
|
||||
newToken = parsed.nextPageToken
|
||||
newPageIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
traverseDirectory(newParsed.url)
|
||||
}
|
||||
|
||||
return Observable.just(episodeList.reversed())
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> = throw Exception("Not used")
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
|
||||
val url = episode.url
|
||||
|
||||
val doc = client.newCall(
|
||||
GET("$url?a=view"),
|
||||
).execute().asJsoup()
|
||||
|
||||
val script = doc.selectFirst("script:containsData(videodomain)")?.data()
|
||||
?: return Observable.just(listOf(Video(url, "Video", url)))
|
||||
val domainUrl = script.substringAfter("\"videodomain\":\"").substringBefore("\"")
|
||||
val videoUrl = if (domainUrl.isBlank()) {
|
||||
url
|
||||
} else {
|
||||
domainUrl + url.toHttpUrl().encodedPath
|
||||
}
|
||||
|
||||
return Observable.just(listOf(Video(videoUrl, "Video", videoUrl)))
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun joinUrl(path1: String, path2: String): String {
|
||||
return path1.removeSuffix("/") + "/" + path2.removePrefix("/")
|
||||
}
|
||||
|
||||
private fun String.decrypt(): String {
|
||||
return Base64.decode(this.reversed().substring(24, this.length - 20), Base64.DEFAULT).toString(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun String.addSuffix(suffix: String): String {
|
||||
return if (this.endsWith(suffix)) {
|
||||
this
|
||||
} else {
|
||||
this.plus(suffix)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.trimInfo(): String {
|
||||
var newString = this.replaceFirst("""^\[\w+\] ?""".toRegex(), "")
|
||||
val regex = """( ?\[[\s\w-]+\]| ?\([\s\w-]+\))(\.mkv|\.mp4|\.avi)?${'$'}""".toRegex()
|
||||
|
||||
while (regex.containsMatchIn(newString)) {
|
||||
newString = regex.replace(newString) { matchResult ->
|
||||
matchResult.groups[2]?.value ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
return newString.trim()
|
||||
}
|
||||
|
||||
private fun formatFileSize(bytes: Long): String {
|
||||
return when {
|
||||
bytes >= 1073741824 -> "%.2f GB".format(bytes / 1073741824.0)
|
||||
bytes >= 1048576 -> "%.2f MB".format(bytes / 1048576.0)
|
||||
bytes >= 1024 -> "%.2f KB".format(bytes / 1024.0)
|
||||
bytes > 1 -> "$bytes bytes"
|
||||
bytes == 1L -> "$bytes byte"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun LinkData.toJsonString(): String {
|
||||
return json.encodeToString(this)
|
||||
}
|
||||
|
||||
private fun IdUrl.toJsonString(): String {
|
||||
return json.encodeToString(this)
|
||||
}
|
||||
|
||||
private fun parsePage(response: Response, url: String): AnimesPage {
|
||||
val parsed = json.decodeFromString<ResponseData>(response.body.string().decrypt())
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
val isSearch = url.endsWith(":search")
|
||||
|
||||
parsed.data.files.forEach { item ->
|
||||
if (item.mimeType.endsWith("folder")) {
|
||||
val anime = SAnime.create()
|
||||
anime.title = item.name.trimInfo()
|
||||
|
||||
if (isSearch) {
|
||||
anime.setUrlWithoutDomain(
|
||||
LinkData(
|
||||
"search",
|
||||
IdUrl(
|
||||
item.id,
|
||||
url.substringBeforeLast("search"),
|
||||
response.request.header("Referer")!!,
|
||||
"multi",
|
||||
).toJsonString(),
|
||||
).toJsonString(),
|
||||
)
|
||||
} else {
|
||||
anime.setUrlWithoutDomain(
|
||||
LinkData(
|
||||
"multi",
|
||||
joinUrl(url, item.name).addSuffix("/"),
|
||||
).toJsonString(),
|
||||
)
|
||||
}
|
||||
animeList.add(anime)
|
||||
}
|
||||
if (
|
||||
item.mimeType.startsWith("video/") &&
|
||||
!(preferences.getBoolean("ignore_non_folder", true) && isSearch)
|
||||
) {
|
||||
val anime = SAnime.create()
|
||||
anime.title = item.name.trimInfo()
|
||||
|
||||
if (isSearch) {
|
||||
anime.setUrlWithoutDomain(
|
||||
LinkData(
|
||||
"search",
|
||||
IdUrl(
|
||||
item.id,
|
||||
url.substringBeforeLast("search"),
|
||||
response.request.header("Referer")!!,
|
||||
"single",
|
||||
).toJsonString(),
|
||||
item.size?.toLongOrNull()?.let { formatFileSize(it) },
|
||||
).toJsonString(),
|
||||
)
|
||||
} else {
|
||||
anime.setUrlWithoutDomain(
|
||||
LinkData(
|
||||
"single",
|
||||
joinUrl(url, item.name),
|
||||
item.size?.toLongOrNull()?.let { formatFileSize(it) },
|
||||
).toJsonString(),
|
||||
)
|
||||
}
|
||||
animeList.add(anime)
|
||||
}
|
||||
}
|
||||
|
||||
pageToken = parsed.nextPageToken
|
||||
|
||||
return AnimesPage(animeList, parsed.nextPageToken != null)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val domainListPref = EditTextPreference(screen.context).apply {
|
||||
key = "domain_list"
|
||||
title = "Enter drive paths to be shown in extension"
|
||||
summary = """Enter drive paths to be shown in extension
|
||||
|Enter as comma separated list
|
||||
""".trimMargin()
|
||||
this.setDefaultValue("")
|
||||
dialogTitle = "Path list"
|
||||
dialogMessage = "Separate paths with a comma"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val res = preferences.edit().putString("domain_list", newValue as String).commit()
|
||||
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
|
||||
res
|
||||
} catch (e: java.lang.Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
val blacklistFolders = EditTextPreference(screen.context).apply {
|
||||
key = "blacklist_folders"
|
||||
title = "Blacklist folder names"
|
||||
summary = """Enter names of folders to skip over
|
||||
|Enter as slash / separated list
|
||||
""".trimMargin()
|
||||
this.setDefaultValue("NC/Extras")
|
||||
dialogTitle = "Blacklisted folders"
|
||||
dialogMessage = "Separate folders with a slash (case insensitive)"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val res = preferences.edit().putString("blacklist_folders", newValue as String).commit()
|
||||
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
|
||||
res
|
||||
} catch (e: java.lang.Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ignoreNonFolder = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = "ignore_non_folder"
|
||||
title = "Only include folders on search"
|
||||
setDefaultValue(true)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putBoolean(key, newValue as Boolean).commit()
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(domainListPref)
|
||||
screen.addPreference(blacklistFolders)
|
||||
screen.addPreference(ignoreNonFolder)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user