Show favicons in sources list
This commit is contained in:
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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?,
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
app/src/main/res/drawable/ic_favicon_fallback.xml
Normal file
16
app/src/main/res/drawable/ic_favicon_fallback.xml
Normal 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>
|
||||||
@@ -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"
|
||||||
|
|||||||
46
app/src/main/res/layout/item_source_config_draggable.xml
Normal file
46
app/src/main/res/layout/item_source_config_draggable.xml
Normal 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>
|
||||||
Reference in New Issue
Block a user