Show favicons in sources list

This commit is contained in:
Koitharu
2022-01-25 19:56:37 +02:00
parent 5f5a98e351
commit 2ac6b84f87
13 changed files with 167 additions and 16 deletions

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.core.parser
import android.net.Uri
import coil.map.Mapper
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.core.model.MangaSource
class FaviconMapper() : Mapper<Uri, HttpUrl> {
override fun map(data: Uri): HttpUrl {
val mangaSource = MangaSource.valueOf(data.schemeSpecificPart)
val repo = MangaRepository(mangaSource) as RemoteMangaRepository
return repo.getFaviconUrl().toHttpUrl()
}
override fun handles(data: Uri) = data.scheme == "favicon"
}

View File

@@ -26,7 +26,7 @@ abstract class RemoteMangaRepository(
override suspend fun getTags(): Set<MangaTag> = emptySet() override suspend fun getTags(): Set<MangaTag> = emptySet()
fun getFaviconUrl() = "https://${getDomain()}/favicon.ico" open fun getFaviconUrl() = "https://${getDomain()}/favicon.ico"
open fun onCreatePreferences(map: MutableMap<String, Any>) { open fun onCreatePreferences(map: MutableMap<String, Any>) {
map[SourceSettings.KEY_DOMAIN] = defaultDomain map[SourceSettings.KEY_DOMAIN] = defaultDomain

View File

@@ -21,6 +21,10 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
SortOrder.NEWEST SortOrder.NEWEST
) )
override fun getFaviconUrl(): String {
return "https://cdn.${getDomain()}/favicons/favicon.png"
}
override suspend fun getList2( override suspend fun getList2(
offset: Int, offset: Int,
query: String?, query: String?,

View File

@@ -5,6 +5,7 @@ import coil.ImageLoader
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.parser.FaviconMapper
import org.koitharu.kotatsu.local.data.CbzFetcher import org.koitharu.kotatsu.local.data.CbzFetcher
val uiModule val uiModule
@@ -15,6 +16,7 @@ val uiModule
.componentRegistry( .componentRegistry(
ComponentRegistry.Builder() ComponentRegistry.Builder()
.add(CbzFetcher()) .add(CbzFetcher())
.add(FaviconMapper())
.build() .build()
).build() ).build()
} }

View File

@@ -9,23 +9,24 @@ import coil.fetch.FetchResult
import coil.fetch.Fetcher import coil.fetch.Fetcher
import coil.fetch.SourceResult import coil.fetch.SourceResult
import coil.size.Size import coil.size.Size
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okio.buffer import okio.buffer
import okio.source import okio.source
import java.util.zip.ZipFile import java.util.zip.ZipFile
class CbzFetcher : Fetcher<Uri> { class CbzFetcher : Fetcher<Uri> {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun fetch( override suspend fun fetch(
pool: BitmapPool, pool: BitmapPool,
data: Uri, data: Uri,
size: Size, size: Size,
options: Options, options: Options,
): FetchResult { ): FetchResult = runInterruptible(Dispatchers.IO) {
val zip = ZipFile(data.schemeSpecificPart) val zip = ZipFile(data.schemeSpecificPart)
val entry = zip.getEntry(data.fragment) val entry = zip.getEntry(data.fragment)
val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name) val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name)
return SourceResult( SourceResult(
source = ExtraCloseableBufferedSource( source = ExtraCloseableBufferedSource(
zip.getInputStream(entry).source().buffer(), zip.getInputStream(entry).source().buffer(),
zip, zip,

View File

@@ -8,6 +8,7 @@ import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
@@ -42,7 +43,7 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val sourcesAdapter = SourceConfigAdapter(this) val sourcesAdapter = SourceConfigAdapter(this, get(), viewLifecycleOwner)
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
addItemDecoration(SourceConfigItemDecoration(view.context)) addItemDecoration(SourceConfigItemDecoration(view.context))

View File

@@ -81,6 +81,7 @@ class SourcesSettingsViewModel(
SourceConfigItem.SourceItem( SourceConfigItem.SourceItem(
source = it, source = it,
isEnabled = true, isEnabled = true,
isDraggable = true,
) )
} }
} }
@@ -102,6 +103,7 @@ class SourcesSettingsViewModel(
SourceConfigItem.SourceItem( SourceConfigItem.SourceItem(
source = it, source = it,
isEnabled = false, isEnabled = false,
isDraggable = false,
) )
} }
} }

View File

@@ -1,13 +1,18 @@
package org.koitharu.kotatsu.settings.sources.adapter package org.koitharu.kotatsu.settings.sources.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class SourceConfigAdapter( class SourceConfigAdapter(
listener: SourceConfigListener, listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) : AsyncListDifferDelegationAdapter<SourceConfigItem>( ) : AsyncListDifferDelegationAdapter<SourceConfigItem>(
SourceConfigDiffCallback(), SourceConfigDiffCallback(),
sourceConfigHeaderDelegate(), sourceConfigHeaderDelegate(),
sourceConfigGroupDelegate(listener), sourceConfigGroupDelegate(listener),
sourceConfigItemDelegate(listener), sourceConfigItemDelegate(listener, coil, lifecycleOwner),
sourceConfigDraggableItemDelegate(listener),
) )

View File

@@ -6,12 +6,18 @@ import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePaddingRelative import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
import coil.request.ImageRequest
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemExpandableBinding import org.koitharu.kotatsu.databinding.ItemExpandableBinding
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigDraggableBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import org.koitharu.kotatsu.utils.ext.enqueueWith
fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>( fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>(
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) } { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
@@ -38,11 +44,54 @@ fun sourceConfigGroupDelegate(
} }
} }
@SuppressLint("ClickableViewAccessibility")
fun sourceConfigItemDelegate( fun sourceConfigItemDelegate(
listener: SourceConfigListener, listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>( ) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
{ layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) } { layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is SourceConfigItem.SourceItem && !item.isDraggable }
) {
val eventListener = object : View.OnClickListener, CompoundButton.OnCheckedChangeListener {
override fun onClick(v: View?) = listener.onItemSettingsClick(item)
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
listener.onItemEnabledChanged(item, isChecked)
}
}
var imageRequest: Disposable? = null
binding.imageViewConfig.setOnClickListener(eventListener)
binding.switchToggle.setOnCheckedChangeListener(eventListener)
bind {
binding.textViewTitle.text = item.source.title
binding.switchToggle.isChecked = item.isEnabled
binding.imageViewConfig.isVisible = item.isEnabled
binding.root.updatePaddingRelative(
end = if (item.isEnabled) 0 else binding.imageViewConfig.paddingEnd,
)
imageRequest = ImageRequest.Builder(context)
.data(item.faviconUrl)
.error(R.drawable.ic_favicon_fallback)
.target(binding.imageViewIcon)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
}
onViewRecycled {
imageRequest?.dispose()
imageRequest = null
}
}
@SuppressLint("ClickableViewAccessibility")
fun sourceConfigDraggableItemDelegate(
listener: SourceConfigListener,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigDraggableBinding>(
{ layoutInflater, parent -> ItemSourceConfigDraggableBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is SourceConfigItem.SourceItem && item.isDraggable }
) { ) {
val eventListener = object : View.OnClickListener, View.OnTouchListener, val eventListener = object : View.OnClickListener, View.OnTouchListener,
@@ -70,10 +119,8 @@ fun sourceConfigItemDelegate(
bind { bind {
binding.textViewTitle.text = item.source.title binding.textViewTitle.text = item.source.title
binding.switchToggle.isChecked = item.isEnabled binding.switchToggle.isChecked = item.isEnabled
binding.imageViewHandle.isVisible = item.isEnabled
binding.imageViewConfig.isVisible = item.isEnabled binding.imageViewConfig.isVisible = item.isEnabled
binding.root.updatePaddingRelative( binding.root.updatePaddingRelative(
start = if (item.isEnabled) 0 else binding.imageViewHandle.paddingStart * 2,
end = if (item.isEnabled) 0 else binding.imageViewConfig.paddingEnd, end = if (item.isEnabled) 0 else binding.imageViewConfig.paddingEnd,
) )
} }

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.settings.sources.model package org.koitharu.kotatsu.settings.sources.model
import android.net.Uri
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
@@ -49,8 +50,12 @@ sealed interface SourceConfigItem {
class SourceItem( class SourceItem(
val source: MangaSource, val source: MangaSource,
val isEnabled: Boolean, val isEnabled: Boolean,
val isDraggable: Boolean,
) : SourceConfigItem { ) : SourceConfigItem {
val faviconUrl: Uri
get() = Uri.fromParts("favicon", source.name, null)
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
@@ -59,6 +64,7 @@ sealed interface SourceConfigItem {
if (source != other.source) return false if (source != other.source) return false
if (isEnabled != other.isEnabled) return false if (isEnabled != other.isEnabled) return false
if (isDraggable != other.isDraggable) return false
return true return true
} }
@@ -66,6 +72,7 @@ sealed interface SourceConfigItem {
override fun hashCode(): Int { override fun hashCode(): Int {
var result = source.hashCode() var result = source.hashCode()
result = 31 * result + isEnabled.hashCode() result = 31 * result + isEnabled.hashCode()
result = 31 * result + isDraggable.hashCode()
return result return result
} }
} }

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="?colorControlLight" />
</shape>
</item>
<item
android:bottom="4dp"
android:drawable="@drawable/ic_web"
android:left="4dp"
android:right="4dp"
android:top="4dp" />
</layer-list>

View File

@@ -9,12 +9,14 @@
android:orientation="horizontal"> android:orientation="horizontal">
<ImageView <ImageView
android:id="@+id/imageView_handle" android:id="@+id/imageView_icon"
android:layout_width="wrap_content" android:layout_width="?android:listPreferredItemHeightSmall"
android:layout_height="match_parent" android:layout_height="?android:listPreferredItemHeightSmall"
android:paddingHorizontal="?listPreferredItemPaddingStart" android:layout_marginHorizontal="?listPreferredItemPaddingStart"
android:scaleType="center" android:labelFor="@id/textView_title"
android:src="@drawable/ic_reorder_handle" /> android:padding="6dp"
android:scaleType="fitCenter"
tools:src="@tools:sample/avatars" />
<TextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:windowBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/imageView_handle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingHorizontal="?listPreferredItemPaddingStart"
android:scaleType="center"
android:src="@drawable/ic_reorder_handle" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
tools:text="@tools:sample/lorem[1]" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/imageView_config"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/settings"
android:paddingHorizontal="?listPreferredItemPaddingEnd"
android:scaleType="center"
android:src="@drawable/ic_settings" />
</LinearLayout>