webview for extensions

This commit is contained in:
rebelonion 2024-01-18 01:09:11 -06:00
parent 26b6564825
commit ff02280239
29 changed files with 447 additions and 100 deletions

View file

@ -190,6 +190,24 @@
<data android:host="discord.dantotsu.com" />
</intent-filter>
</activity>
<activity
android:name=".others.webview.CookieCatcher"
android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleTask">
<intent-filter android:label="Discord Login for Dantotsu">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="dantotsu" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="discord.dantotsu.com" />
</intent-filter>
</activity>
<activity
android:name=".connections.anilist.UrlMedia"
android:configChanges="orientation|screenSize|layoutDirection"

View file

@ -945,14 +945,19 @@ fun checkCountry(context: Context): Boolean {
}
const val INCOGNITO_CHANNEL_ID = 26
@SuppressLint("LaunchActivityFromNotification")
fun incognitoNotification(context: Context){
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val incognito = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("incognito", false)
fun incognitoNotification(context: Context) {
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val incognito = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("incognito", false)
if (incognito) {
val intent = Intent(context, NotificationClickReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent,
PendingIntent.FLAG_IMMUTABLE)
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(context, Notifications.CHANNEL_INCOGNITO_MODE)
.setSmallIcon(R.drawable.ic_incognito_24)
.setContentTitle("Incognito Mode")

View file

@ -198,24 +198,23 @@ class MainActivity : AppCompatActivity() {
else -> 1
}
} else {
uiSettings.defaultStartUpTab
}
uiSettings.defaultStartUpTab
}
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
}
val offline = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("offlineMode", false)
.getBoolean("offlineMode", false)
if (!isOnline(this)) {
snackString(this@MainActivity.getString(R.string.no_internet_connection))
startActivity(Intent(this, NoInternet::class.java))
} else {
if (offline){
if (offline) {
snackString(this@MainActivity.getString(R.string.no_internet_connection))
startActivity(Intent(this, NoInternet::class.java))
}
else {
} else {
val model: AnilistHomeViewModel by viewModels()
model.genres.observe(this) { it ->
if (it != null) {
@ -226,15 +225,16 @@ class MainActivity : AppCompatActivity() {
binding.mainProgressBar.visibility = View.GONE
val mainViewPager = binding.viewpager
mainViewPager.isUserInputEnabled = false
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
navbar.setOnTabSelectListener(object :
AnimatedBottomBar.OnTabSelectListener {
AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
navbar.animate().translationZ(12f).setDuration(200).start()
selectedOption = newIndex
@ -242,7 +242,12 @@ class MainActivity : AppCompatActivity() {
}
})
navbar.selectTabAt(selectedOption)
mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) }
mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
} else {
binding.mainProgressBar.visibility = View.GONE
}
@ -262,8 +267,8 @@ class MainActivity : AppCompatActivity() {
if (media != null) {
media.cameFromContinue = cont
startActivity(
Intent(this@MainActivity, MediaDetailsActivity::class.java)
.putExtra("media", media as Serializable)
Intent(this@MainActivity, MediaDetailsActivity::class.java)
.putExtra("media", media as Serializable)
)
} else {
snackString(this@MainActivity.getString(R.string.anilist_not_found))
@ -282,8 +287,8 @@ class MainActivity : AppCompatActivity() {
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
addView(TextView(this@MainActivity).apply {
val markWon =
Markwon.builder(this@MainActivity)
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
Markwon.builder(this@MainActivity)
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
markWon.setMarkdown(this, md)
})
@ -296,8 +301,8 @@ class MainActivity : AppCompatActivity() {
saveData("allow_opening_links", true, this@MainActivity)
tryWith(true) {
startActivity(
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
.setData(Uri.parse("package:$packageName"))
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
.setData(Uri.parse("package:$packageName"))
)
}
}

View file

@ -10,7 +10,6 @@ import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
import androidx.activity.viewModels

View file

@ -43,7 +43,8 @@ class AnimeNameAdapter {
text
}
return if (removedNumber.equals(text, true)) {
val failedEpisodeNumberPattern: Regex = Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
val failedEpisodeNumberPattern: Regex =
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
mr.value.replaceFirst(mr.groupValues[1], "")
}.ifEmpty { removedNumber }
@ -58,7 +59,8 @@ class AnimeNameAdapter {
text
}
return if (removedNumber.equals(text, true)) {
val failedEpisodeNumberPattern: Regex = Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
val failedEpisodeNumberPattern: Regex =
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
mr.value.replaceFirst(mr.groupValues[1], "")
}.ifEmpty { removedNumber }

View file

@ -12,6 +12,7 @@ import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
@ -22,12 +23,14 @@ import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@ -86,8 +89,12 @@ class AnimeWatchAdapter(
null
)
}
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("offlineMode", false) == true) View.GONE else View.VISIBLE
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
)
?.getBoolean("offlineMode", false) == true
) View.GONE else View.VISIBLE
binding.animeSourceNameContainer.visibility = offline
binding.animeSourceSettings.visibility = offline
@ -188,7 +195,7 @@ class AnimeWatchAdapter(
val dialogView =
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
val dialogBinding = DialogLayoutBinding.bind(dialogView)
var refresh = false
var run = false
var reversed = media.selected!!.recyclerReversed
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
@ -237,6 +244,21 @@ class AnimeWatchAdapter(
dialogBinding.layoutText.text = "Compact"
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
//start CookieCatcher activity
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
val sourceAHH = watchSources[source] as? DynamicAnimeParser
val sourceHttp =
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource
val url = sourceHttp?.baseUrl
url?.let {
refresh = true
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
startActivity(fragment.requireContext(), intent, null)
}
}
}
//hidden
dialogBinding.animeScanlatorContainer.visibility = View.GONE
@ -247,8 +269,13 @@ class AnimeWatchAdapter(
.setView(dialogView)
.setPositiveButton("OK") { _, _ ->
if (run) fragment.onIconPressed(style, reversed)
if (refresh) fragment.loadEpisodes(source, true)
}
.setNegativeButton("Cancel") { _, _ ->
if (refresh) fragment.loadEpisodes(source, true)
}
.setOnCancelListener {
if (refresh) fragment.loadEpisodes(source, true)
}
.create()
nestedDialog?.show()
@ -410,7 +437,8 @@ class AnimeWatchAdapter(
)
val items = adapter.count
binding?.animeSourceLanguageContainer?.visibility = if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguageContainer?.visibility =
if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguage?.setAdapter(adapter)
}

View file

@ -11,7 +11,6 @@ import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.lifecycle.coroutineScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadIndex
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
@ -63,6 +62,7 @@ class EpisodeAdapter(
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return (when (viewType) {
0 -> EpisodeListViewHolder(
@ -248,7 +248,10 @@ class EpisodeAdapter(
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), episodeNumber)
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
media.mainName(),
episodeNumber
)
val id = fragment.requireContext().getSharedPreferences(
ContextCompat.getString(fragment.requireContext(), R.string.anime_downloads),
Context.MODE_PRIVATE
@ -323,6 +326,7 @@ class EpisodeAdapter(
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) :
RecyclerView.ViewHolder(binding.root) {
private val activeCoroutines = mutableSetOf<String>()
init {
itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)

View file

@ -3,6 +3,7 @@ package ani.dantotsu.media.manga
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -23,12 +24,14 @@ import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.media.anime.handleProgress
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@ -65,8 +68,12 @@ class MangaReadAdapter(
null
)
}
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("offlineMode", false) == true) View.GONE else View.VISIBLE
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
)
?.getBoolean("offlineMode", false) == true
) View.GONE else View.VISIBLE
binding.animeSourceNameContainer.visibility = offline
binding.animeSourceSettings.visibility = offline
@ -149,9 +156,10 @@ class MangaReadAdapter(
binding.animeNestedButton.setOnClickListener {
val dialogView = LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
val dialogView =
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
val dialogBinding = DialogLayoutBinding.bind(dialogView)
var refresh = false
var run = false
var reversed = media.selected!!.recyclerReversed
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
@ -194,6 +202,20 @@ class MangaReadAdapter(
dialogBinding.layoutText.text = "Compact"
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
//start CookieCatcher activity
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
val url = sourceHttp?.baseUrl
url?.let {
refresh = true
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
ContextCompat.startActivity(fragment.requireContext(), intent, null)
}
}
}
//Multi download
dialogBinding.downloadNo.text = "0"
@ -216,7 +238,8 @@ class MangaReadAdapter(
}
//Scanlator
dialogBinding.animeScanlatorContainer.visibility = if (options.count() > 1) View.VISIBLE else View.GONE
dialogBinding.animeScanlatorContainer.visibility =
if (options.count() > 1) View.VISIBLE else View.GONE
dialogBinding.scanlatorNo.text = "${options.count()}"
dialogBinding.animeScanlatorTop.setOnClickListener {
val dialogView2 =
@ -267,8 +290,13 @@ class MangaReadAdapter(
if (dialogBinding.downloadNo.text != "0") {
fragment.multiDownload(dialogBinding.downloadNo.text.toString().toInt())
}
if (refresh) fragment.loadChapters(source, true)
}
.setNegativeButton("Cancel") { _, _ ->
if (refresh) fragment.loadChapters(source, true)
}
.setOnCancelListener {
if (refresh) fragment.loadChapters(source, true)
}
.create()
nestedDialog?.show()
@ -437,7 +465,8 @@ class MangaReadAdapter(
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
)
val items = adapter.count
binding?.animeSourceLanguageContainer?.visibility = if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguageContainer?.visibility =
if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguage?.setAdapter(adapter)

View file

@ -0,0 +1,60 @@
package ani.dantotsu.others.webview
import android.annotation.SuppressLint
import android.app.Application
import android.os.Build
import android.os.Bundle
import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.R
import ani.dantotsu.themes.ThemeManager
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class CookieCatcher : AppCompatActivity() {
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
//get url from intent
val url = intent.getStringExtra("url") ?: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = Application.getProcessName()
if (packageName != process) WebView.setDataDirectorySuffix(process)
}
setContentView(R.layout.activity_discord)
val webView = findViewById<WebView>(R.id.discordWebview)
val cookies: CookieManager = Injekt.get<NetworkHelper>().cookieJar.manager
cookies.setAcceptThirdPartyCookies(webView, true)
webView.apply {
settings.javaScriptEnabled = true
settings.databaseEnabled = true
settings.domStorageEnabled = true
}
WebView.setWebContentsDebuggingEnabled(true)
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
return super.shouldOverrideUrlLoading(view, request)
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
}
}
webView.loadUrl(url)
}
}

View file

@ -11,6 +11,9 @@ import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.FileUrl
import ani.dantotsu.databinding.BottomSheetWebviewBinding
import ani.dantotsu.defaultHeaders
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
abstract class WebViewBottomDialog : BottomSheetDialogFragment() {
@ -30,7 +33,8 @@ abstract class WebViewBottomDialog : BottomSheetDialogFragment() {
dismiss()
}
val cookies: CookieManager = CookieManager.getInstance()
val cookies: CookieManager = Injekt.get<NetworkHelper>().cookieJar.manager
//CookieManager.getInstance()
override fun onCreateView(
inflater: LayoutInflater,

View file

@ -11,15 +11,23 @@ class SourcePreferences(
// Common options
fun sourceDisplayMode() = preferenceStore.getObject("pref_display_mode_catalogue", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize)
fun sourceDisplayMode() = preferenceStore.getObject(
"pref_display_mode_catalogue",
LibraryDisplayMode.default,
LibraryDisplayMode.Serializer::serialize,
LibraryDisplayMode.Serializer::deserialize
)
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
fun enabledLanguages() =
preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL)
fun migrationSortingMode() =
preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL)
fun migrationSortingDirection() = preferenceStore.getEnum("pref_migration_direction", SetMigrateSorting.Direction.ASCENDING)
fun migrationSortingDirection() =
preferenceStore.getEnum("pref_migration_direction", SetMigrateSorting.Direction.ASCENDING)
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet())
@ -37,12 +45,17 @@ class SourcePreferences(
fun animeExtensionUpdatesCount() = preferenceStore.getInt("animeext_updates_count", 0)
fun mangaExtensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
fun searchPinnedAnimeSourcesOnly() = preferenceStore.getBoolean("search_pinned_anime_sources_only", false)
fun searchPinnedMangaSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false)
fun searchPinnedAnimeSourcesOnly() =
preferenceStore.getBoolean("search_pinned_anime_sources_only", false)
fun hideInAnimeLibraryItems() = preferenceStore.getBoolean("browse_hide_in_anime_library_items", false)
fun searchPinnedMangaSourcesOnly() =
preferenceStore.getBoolean("search_pinned_sources_only", false)
fun hideInMangaLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
fun hideInAnimeLibraryItems() =
preferenceStore.getBoolean("browse_hide_in_anime_library_items", false)
fun hideInMangaLibraryItems() =
preferenceStore.getBoolean("browse_hide_in_library_items", false)
// SY -->
@ -62,7 +75,8 @@ class SourcePreferences(
fun dataSaverImageQuality() = preferenceStore.getInt("data_saver_image_quality", 80)
fun dataSaverImageFormatJpeg() = preferenceStore.getBoolean("data_saver_image_format_jpeg", false)
fun dataSaverImageFormatJpeg() =
preferenceStore.getBoolean("data_saver_image_format_jpeg", false)
fun dataSaverServer() = preferenceStore.getString("data_saver_server", "")

View file

@ -3,10 +3,15 @@ package eu.kanade.tachiyomi.animesource.model
sealed class AnimeFilter<T>(val name: String, var state: T) {
open class Header(name: String) : AnimeFilter<Any>(name, 0)
open class Separator(name: String = "") : AnimeFilter<Any>(name, 0)
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : AnimeFilter<Int>(name, state)
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) :
AnimeFilter<Int>(name, state)
abstract class Text(name: String, state: String = "") : AnimeFilter<String>(name, state)
abstract class CheckBox(name: String, state: Boolean = false) : AnimeFilter<Boolean>(name, state)
abstract class TriState(name: String, state: Int = STATE_IGNORE) : AnimeFilter<Int>(name, state) {
abstract class CheckBox(name: String, state: Boolean = false) :
AnimeFilter<Boolean>(name, state)
abstract class TriState(name: String, state: Int = STATE_IGNORE) :
AnimeFilter<Int>(name, state) {
fun isIgnored() = state == STATE_IGNORE
fun isIncluded() = state == STATE_INCLUDE
fun isExcluded() = state == STATE_EXCLUDE

View file

@ -50,7 +50,8 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
override val id by lazy {
val key = "${name.lowercase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }
.reduce(Long::or) and Long.MAX_VALUE
}
/**
@ -112,7 +113,11 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
override fun fetchSearchAnime(
page: Int,
query: String,
filters: AnimeFilterList
): Observable<AnimesPage> {
return Observable.defer {
try {
client.newCall(searchAnimeRequest(page, query, filters)).asObservableSuccess()
@ -134,7 +139,11 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* @param query the search query.
* @param filters the list of filters to apply.
*/
protected abstract fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request
protected abstract fun searchAnimeRequest(
page: Int,
query: String,
filters: AnimeFilterList
): Request
/**
* Parses the response from the site and returns a [AnimesPage] object.
@ -311,7 +320,12 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
val animeDownloadClient = client.newBuilder()
.callTimeout(30, TimeUnit.MINUTES)
.build()
return animeDownloadClient.newCachelessCallWithProgress(videoRequest(video, video.totalBytesDownloaded), video)
return animeDownloadClient.newCachelessCallWithProgress(
videoRequest(
video,
video.totalBytesDownloaded
), video
)
.asObservableSuccess()
}

View file

@ -23,7 +23,10 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
private val packageActionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
when (intent.getIntExtra(
PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE
)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
if (userAction == null) {
@ -34,9 +37,11 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
service.startActivity(userAction)
}
PackageInstaller.STATUS_FAILURE_ABORTED -> {
continueQueue(InstallStep.Idle)
}
PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
else -> continueQueue(InstallStep.Error)
}
@ -52,7 +57,8 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
super.processEntry(entry)
activeSession = null
try {
val installParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val installParams =
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
}
@ -60,7 +66,8 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
val fileSize = service.getUriSize(entry.uri) ?: throw IllegalStateException()
installParams.setSize(fileSize)
val inputStream = service.contentResolver.openInputStream(entry.uri) ?: throw IllegalStateException()
val inputStream =
service.contentResolver.openInputStream(entry.uri) ?: throw IllegalStateException()
val session = packageInstaller.openSession(activeSession!!.second)
val outputStream = session.openWrite(entry.downloadId.toString(), 0, fileSize)
session.use {
@ -82,7 +89,10 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
session.commit(intentSender)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
logcat(
LogPriority.ERROR,
e
) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
logcat(LogPriority.ERROR) { "Exception: $e" }
snackString("Failed to install extension ${entry.downloadId} ${entry.uri}")
activeSession?.let { (_, sessionId) ->
@ -108,7 +118,12 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
}
init {
ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_EXPORTED)
ContextCompat.registerReceiver(
service,
packageActionReceiver,
IntentFilter(INSTALL_ACTION),
ContextCompat.RECEIVER_EXPORTED
)
}
}

View file

@ -7,8 +7,8 @@ import android.content.pm.ServiceInfo
import android.net.Uri
import android.os.Build
import android.os.IBinder
import eu.kanade.domain.base.BasePreferences
import ani.dantotsu.R
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
import eu.kanade.tachiyomi.extension.anime.installer.PackageInstallerInstallerAnime
@ -32,8 +32,12 @@ class AnimeExtensionInstallService : Service() {
setProgress(100, 100, true)
}.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
}else{
startForeground(
Notifications.ID_EXTENSION_INSTALLER,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
}
}
@ -51,7 +55,10 @@ class AnimeExtensionInstallService : Service() {
if (installer == null) {
installer = when (installerUsed) {
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstallerAnime(this)
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstallerAnime(
this
)
else -> {
logcat(LogPriority.ERROR) { "Not implemented for installer $installerUsed" }
stopSelf()

View file

@ -41,15 +41,18 @@ internal object AnimeExtensionLoader {
const val LIB_VERSION_MIN = 12
const val LIB_VERSION_MAX = 15
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
private const val PACKAGE_FLAGS =
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// jmir1's key
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"
private const val officialSignature =
"50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"
/**
* List of the trusted signatures.
*/
var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
var trustedSignatures =
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
/**
* Return a list of all the installed extensions initialized concurrently.
@ -105,7 +108,11 @@ internal object AnimeExtensionLoader {
* @param pkgName The package name of the extension to load.
* @param pkgInfo The package info of the extension.
*/
private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): AnimeLoadResult {
private fun loadExtension(
context: Context,
pkgName: String,
pkgInfo: PackageInfo
): AnimeLoadResult {
val pkgManager = context.packageManager
val appInfo = try {
@ -141,7 +148,14 @@ internal object AnimeExtensionLoader {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return AnimeLoadResult.Error
} else if (signatureHash !in trustedSignatures) {
val extension = AnimeExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
val extension = AnimeExtension.Untrusted(
extName,
pkgName,
versionName,
versionCode,
libVersion,
signatureHash
)
logcat(LogPriority.WARN, message = { "Extension $pkgName isn't trusted" })
return AnimeLoadResult.Untrusted(extension)
}

View file

@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -63,9 +62,11 @@ class MangaExtensionManager(
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = _installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
val pkgName =
_installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
if (pkgName != null) {
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
return iconMap[pkgName]
?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
}
return null
}
@ -257,14 +258,20 @@ class MangaExtensionManager(
MangaExtensionLoader.trustedSignatures += signature
preferences.trustedSignatures() += signature
val nowTrustedExtensions = _untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
val nowTrustedExtensions =
_untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
_untrustedExtensionsFlow.value -= nowTrustedExtensions
val ctx = context
launchNow {
nowTrustedExtensions
.map { extension ->
async { MangaExtensionLoader.loadMangaExtensionFromPkgName(ctx, extension.pkgName) }
async {
MangaExtensionLoader.loadMangaExtensionFromPkgName(
ctx,
extension.pkgName
)
}
}
.map { it.await() }
.forEach { result ->
@ -354,13 +361,15 @@ class MangaExtensionManager(
}
private fun MangaExtension.Installed.updateExists(availableExtension: MangaExtension.Available? = null): Boolean {
val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
val availableExt =
availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
if (isUnofficial || availableExt == null) return false
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
}
private fun updatePendingUpdatesCount() {
preferences.mangaExtensionUpdatesCount().set(_installedExtensionsFlow.value.count { it.hasUpdate })
preferences.mangaExtensionUpdatesCount()
.set(_installedExtensionsFlow.value.count { it.hasUpdate })
}
}

View file

@ -77,7 +77,11 @@ internal class MangaExtensionInstaller(private val context: Context) {
val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name)
.setMimeType(APK_MIME)
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
.setDestinationInExternalFilesDir(
context,
Environment.DIRECTORY_DOWNLOADS,
downloadUri.lastPathSegment
)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val id = downloadManager.enqueue(request)
@ -141,6 +145,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
context.startActivity(intent)
}
else -> {
val intent =
MangaExtensionInstallService.getIntent(context, downloadId, uri, installer)

View file

@ -7,7 +7,7 @@ import okhttp3.HttpUrl
class AndroidCookieJar : CookieJar {
private val manager = CookieManager.getInstance()
val manager = CookieManager.getInstance()
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val urlString = url.toString()

View file

@ -17,6 +17,9 @@ class NetworkPreferences(
}
fun defaultUserAgent(): Preference<String> {
return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0")
return preferenceStore.getString(
"default_user_agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0"
)
}
}

View file

@ -30,7 +30,11 @@ class CloudflareInterceptor(
return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
}
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
override fun intercept(
chain: Interceptor.Chain,
request: Request,
response: Response
): Response {
try {
response.close()
cookieManager.remove(request.url, COOKIE_NAMES, 0)
@ -125,7 +129,10 @@ class CloudflareInterceptor(
if (!cloudflareBypassed) {
// Prompt user to update WebView if it seems too outdated
if (isWebViewOutdated) {
context.toast("Please update the webview app for better compatibility", Toast.LENGTH_LONG)
context.toast(
"Please update the webview app for better compatibility",
Toast.LENGTH_LONG
)
}
throw CloudflareBypassException()

View file

@ -50,7 +50,8 @@ abstract class HttpSource : CatalogueSource {
override val id by lazy {
val key = "${name.lowercase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }
.reduce(Long::or) and Long.MAX_VALUE
}
/**
@ -112,7 +113,11 @@ abstract class HttpSource : CatalogueSource {
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList
): Observable<MangasPage> {
return Observable.defer {
try {
client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
@ -134,7 +139,11 @@ abstract class HttpSource : CatalogueSource {
* @param query the search query.
* @param filters the list of filters to apply.
*/
protected abstract fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
protected abstract fun searchMangaRequest(
page: Int,
query: String,
filters: FilterList
): Request
/**
* Parses the response from the site and returns a [MangasPage] object.

View file

@ -16,13 +16,21 @@ import androidx.core.content.getSystemService
val Context.notificationManager: NotificationManager
get() = getSystemService()!!
fun Context.notify(id: Int, channelId: String, block: (NotificationCompat.Builder.() -> Unit)? = null) {
fun Context.notify(
id: Int,
channelId: String,
block: (NotificationCompat.Builder.() -> Unit)? = null
) {
val notification = notificationBuilder(channelId, block).build()
this.notify(id, notification)
}
fun Context.notify(id: Int, notification: Notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionChecker.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PermissionChecker.PERMISSION_GRANTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionChecker.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PermissionChecker.PERMISSION_GRANTED
) {
return
}
@ -30,7 +38,11 @@ fun Context.notify(id: Int, notification: Notification) {
}
fun Context.notify(notificationWithIdAndTags: List<NotificationWithIdAndTag>) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionChecker.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PermissionChecker.PERMISSION_GRANTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionChecker.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PermissionChecker.PERMISSION_GRANTED
) {
return
}
@ -48,7 +60,10 @@ fun Context.cancelNotification(id: Int) {
* @param block the function that will execute inside the builder.
* @return a notification to be displayed or updated.
*/
fun Context.notificationBuilder(channelId: String, block: (NotificationCompat.Builder.() -> Unit)? = null): NotificationCompat.Builder {
fun Context.notificationBuilder(
channelId: String,
block: (NotificationCompat.Builder.() -> Unit)? = null
): NotificationCompat.Builder {
val builder = NotificationCompat.Builder(this, channelId)
.setColor(getColor(android.R.color.holo_blue_dark))
if (block != null) {

View file

@ -23,4 +23,5 @@ interface Preference<T> {
fun stateIn(scope: CoroutineScope): StateFlow<T>
}
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(block(get()))
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) =
set(block(get()))

View file

@ -52,9 +52,11 @@ fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
fun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job =
launchIO { withContext(NonCancellable, block) }
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) =
withContext(Dispatchers.Main, block)
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) =
withContext(Dispatchers.IO, block)
suspend fun <T> withNonCancellableContext(block: suspend CoroutineScope.() -> T) =
withContext(NonCancellable, block)

View file

@ -30,4 +30,5 @@ class StubAnimeSource(private val sourceData: AnimeSourceData) : AnimeSource {
return if (sourceData.isMissingInfo.not()) "$name (${lang.uppercase()})" else id.toString()
}
}
class AnimeSourceNotInstalledException : Exception()

View file

@ -19,7 +19,11 @@ interface AnimeSourceRepository {
fun getSourcesWithNonLibraryAnime(): Flow<List<AnimeSourceWithCount>>
fun searchAnime(sourceId: Long, query: String, filterList: AnimeFilterList): AnimeSourcePagingSourceType
fun searchAnime(
sourceId: Long,
query: String,
filterList: AnimeFilterList
): AnimeSourcePagingSourceType
fun getPopularAnime(sourceId: Long): AnimeSourcePagingSourceType

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880q-82,0 -155,-31.5t-127.5,-86Q143,708 111.5,635T80,480q0,-83 31.5,-155.5t86,-127Q252,143 325,111.5T480,80q83,0 155.5,31.5t127,86q54.5,54.5 86,127T880,480q0,82 -31.5,155t-86,127.5q-54.5,54.5 -127,86T480,880ZM480,798q26,-36 45,-75t31,-83L404,640q12,44 31,83t45,75ZM376,782q-18,-33 -31.5,-68.5T322,640L204,640q29,50 72.5,87t99.5,55ZM584,782q56,-18 99.5,-55t72.5,-87L638,640q-9,38 -22.5,73.5T584,782ZM170,560h136q-3,-20 -4.5,-39.5T300,480q0,-21 1.5,-40.5T306,400L170,400q-5,20 -7.5,39.5T160,480q0,21 2.5,40.5T170,560ZM386,560h188q3,-20 4.5,-39.5T580,480q0,-21 -1.5,-40.5T574,400L386,400q-3,20 -4.5,39.5T380,480q0,21 1.5,40.5T386,560ZM654,560h136q5,-20 7.5,-39.5T800,480q0,-21 -2.5,-40.5T790,400L654,400q3,20 4.5,39.5T660,480q0,21 -1.5,40.5T654,560ZM638,320h118q-29,-50 -72.5,-87T584,178q18,33 31.5,68.5T638,320ZM404,320h152q-12,-44 -31,-83t-45,-75q-26,36 -45,75t-31,83ZM204,320h118q9,-38 22.5,-73.5T376,178q-56,18 -99.5,55T204,320Z"/>
</vector>

View file

@ -6,10 +6,12 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -38,10 +40,12 @@
tools:ignore="TextContrastCheck"
tools:text="Continuous" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
android:layout_weight="1" />
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -139,20 +143,22 @@
android:id="@+id/animeSourceTop"
android:layout_width="48dp"
android:layout_height="48dp"
android:rotation="90"
android:background="?android:attr/selectableItemBackground"
android:rotation="90"
app:srcCompat="@drawable/ic_round_arrow_back_ios_new_24"
app:tint="?attr/colorOnBackground"
tools:ignore="ContentDescription,ImageContrastCheck" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<LinearLayout
android:id="@+id/animeDownloadContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:id="@+id/animeDownloadContainer"
android:orientation="horizontal">
<LinearLayout
android:layout_width="265dp"
android:layout_height="match_parent"
@ -161,8 +167,8 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/poppins_bold"
android:alpha="0.58"
android:fontFamily="@font/poppins_bold"
android:text="Download" />
<TextView
@ -194,10 +200,11 @@
</LinearLayout>
<LinearLayout
android:id="@+id/animeScanlatorContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:id="@+id/animeScanlatorContainer">
android:layout_gravity="center_vertical">
<LinearLayout
android:layout_width="265dp"
android:layout_height="match_parent"
@ -206,8 +213,8 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/poppins_bold"
android:alpha="0.58"
android:fontFamily="@font/poppins_bold"
android:text="Scanlator" />
<TextView
@ -219,12 +226,14 @@
tools:ignore="TextContrastCheck"
tools:text="number" />
</LinearLayout>
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/nav_bg_inv"
app:cardCornerRadius="16dp"
app:cardElevation="0dp">
<ImageButton
android:id="@+id/animeScanlatorTop"
android:layout_width="48dp"
@ -235,5 +244,54 @@
tools:ignore="ContentDescription,ImageContrastCheck" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<LinearLayout
android:id="@+id/animeWebviewContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="265dp"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="0.58"
android:fontFamily="@font/poppins_bold"
android:text="Set Cookies" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/poppins_bold"
android:textColor="?attr/colorSecondary"
tools:ignore="TextContrastCheck"
android:text="Open Website" />
</LinearLayout>
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/nav_bg_inv"
app:cardCornerRadius="16dp"
app:cardElevation="0dp">
<ImageButton
android:id="@+id/animeWebViewTop"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground"
app:srcCompat="@drawable/ic_internet"
app:tint="?attr/colorOnBackground"
tools:ignore="ContentDescription,ImageContrastCheck" />
</androidx.cardview.widget.CardView>
</LinearLayout>
</LinearLayout>
</FrameLayout>