Merge branch 'feature/bs-filter' into devel
commit
6a965ddb28
@ -1,41 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.widgets
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.core.view.isGone
|
|
||||||
import com.google.android.material.R
|
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
|
||||||
import java.lang.reflect.Field
|
|
||||||
|
|
||||||
class AnimatedToolbar @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = R.attr.toolbarStyle,
|
|
||||||
) : MaterialToolbar(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
private var navButtonView: View? = null
|
|
||||||
get() {
|
|
||||||
if (field == null) {
|
|
||||||
runCatching {
|
|
||||||
field = navButtonViewField?.get(this) as? View
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return field
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setNavigationIcon(icon: Drawable?) {
|
|
||||||
super.setNavigationIcon(icon)
|
|
||||||
navButtonView?.isGone = (icon == null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
|
|
||||||
val navButtonViewField: Field? = runCatching {
|
|
||||||
Toolbar::class.java.getDeclaredField("mNavButtonView")
|
|
||||||
.also { it.isAccessible = true }
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class MangaFilter(
|
|
||||||
val sortOrder: SortOrder?,
|
|
||||||
val tags: Set<MangaTag>,
|
|
||||||
) : Parcelable
|
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.filter
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import org.koin.androidx.viewmodel.ViewModelOwner.Companion.from
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
||||||
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
|
||||||
|
class FilterBottomSheet : BaseBottomSheet<SheetFilterBinding>() {
|
||||||
|
|
||||||
|
private val viewModel by sharedViewModel<FilterViewModel>(
|
||||||
|
owner = { from(requireParentFragment(), requireParentFragment()) }
|
||||||
|
) {
|
||||||
|
parametersOf(
|
||||||
|
requireArguments().getParcelable<MangaSource>(ARG_SOURCE),
|
||||||
|
requireArguments().getParcelable<FilterState>(ARG_STATE),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
||||||
|
return SheetFilterBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
binding.toolbar.setNavigationOnClickListener { dismiss() }
|
||||||
|
if (!resources.getBoolean(R.bool.is_tablet)) {
|
||||||
|
binding.toolbar.navigationIcon = null
|
||||||
|
}
|
||||||
|
val adapter = FilterAdapter(viewModel)
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
viewModel.filter.observe(viewLifecycleOwner, adapter::setItems)
|
||||||
|
viewModel.result.observe(viewLifecycleOwner) {
|
||||||
|
parentFragmentManager.setFragmentResult(REQUEST_KEY, bundleOf(ARG_STATE to it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).also {
|
||||||
|
val behavior = (it as? BottomSheetDialog)?.behavior ?: return@also
|
||||||
|
behavior.addBottomSheetCallback(
|
||||||
|
object : BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
|
||||||
|
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
|
||||||
|
|
||||||
|
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||||
|
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
binding.toolbar.setNavigationIcon(R.drawable.ic_cross)
|
||||||
|
} else {
|
||||||
|
binding.toolbar.navigationIcon = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val REQUEST_KEY = "filter"
|
||||||
|
|
||||||
|
const val ARG_STATE = "state"
|
||||||
|
private const val TAG = "FilterBottomSheet"
|
||||||
|
private const val ARG_SOURCE = "source"
|
||||||
|
|
||||||
|
fun show(
|
||||||
|
fm: FragmentManager,
|
||||||
|
source: MangaSource,
|
||||||
|
state: FilterState,
|
||||||
|
) = FilterBottomSheet().withArgs(2) {
|
||||||
|
putParcelable(ARG_SOURCE, source)
|
||||||
|
putParcelable(ARG_STATE, state)
|
||||||
|
}.show(fm, TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.filter
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.core.model.SortOrder
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class FilterState(
|
||||||
|
val sortOrder: SortOrder?,
|
||||||
|
val tags: Set<MangaTag>,
|
||||||
|
) : Parcelable
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.filter
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.core.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class FilterViewModel(
|
||||||
|
private val repository: RemoteMangaRepository,
|
||||||
|
dataRepository: MangaDataRepository,
|
||||||
|
state: FilterState,
|
||||||
|
) : BaseViewModel(), OnFilterChangedListener {
|
||||||
|
|
||||||
|
val filter = MutableLiveData<List<FilterItem>>()
|
||||||
|
val result = MutableLiveData<FilterState>()
|
||||||
|
private var job: Job? = null
|
||||||
|
private var selectedSortOrder: SortOrder? = state.sortOrder
|
||||||
|
private val selectedTags = HashSet(state.tags)
|
||||||
|
private val localTagsDeferred = viewModelScope.async(Dispatchers.Default) {
|
||||||
|
dataRepository.findTags(repository.source)
|
||||||
|
}
|
||||||
|
private var availableTagsDeferred = loadTagsAsync()
|
||||||
|
|
||||||
|
init {
|
||||||
|
showFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSortItemClick(item: FilterItem.Sort) {
|
||||||
|
selectedSortOrder = item.order
|
||||||
|
updateFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTagItemClick(item: FilterItem.Tag) {
|
||||||
|
val isModified = if (item.isChecked) {
|
||||||
|
selectedTags.remove(item.tag)
|
||||||
|
} else {
|
||||||
|
selectedTags.add(item.tag)
|
||||||
|
}
|
||||||
|
if (isModified) {
|
||||||
|
updateFilters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFilters() {
|
||||||
|
val previousJob = job
|
||||||
|
job = launchJob(Dispatchers.Default) {
|
||||||
|
previousJob?.cancelAndJoin()
|
||||||
|
val tags = tryLoadTags()
|
||||||
|
val localTags = localTagsDeferred.await()
|
||||||
|
val sortOrders = repository.sortOrders
|
||||||
|
val list = ArrayList<FilterItem>(sortOrders.size + (tags?.size ?: 1) + 2)
|
||||||
|
list.add(FilterItem.Header(R.string.sort_order))
|
||||||
|
sortOrders.sortedBy { it.ordinal }.mapTo(list) {
|
||||||
|
FilterItem.Sort(it, isSelected = it == selectedSortOrder)
|
||||||
|
}
|
||||||
|
if (tags == null || tags.isNotEmpty() || selectedTags.isNotEmpty()) {
|
||||||
|
list.add(FilterItem.Header(R.string.genres))
|
||||||
|
val mappedTags = TreeSet<FilterItem.Tag>(compareBy({ !it.isChecked }, { it.tag.title }))
|
||||||
|
localTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) }
|
||||||
|
tags?.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) }
|
||||||
|
selectedTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = true) }
|
||||||
|
list.addAll(mappedTags)
|
||||||
|
if (tags == null) {
|
||||||
|
list.add(FilterItem.Error(R.string.filter_load_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ensureActive()
|
||||||
|
filter.postValue(list)
|
||||||
|
}
|
||||||
|
result.value = FilterState(selectedSortOrder, selectedTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showFilter() {
|
||||||
|
job = launchJob(Dispatchers.Default) {
|
||||||
|
val sortOrders = repository.sortOrders
|
||||||
|
val list = ArrayList<FilterItem>(sortOrders.size + selectedTags.size + 3)
|
||||||
|
list.add(FilterItem.Header(R.string.sort_order))
|
||||||
|
sortOrders.sortedBy { it.ordinal }.mapTo(list) {
|
||||||
|
FilterItem.Sort(it, isSelected = it == selectedSortOrder)
|
||||||
|
}
|
||||||
|
if (selectedTags.isNotEmpty()) {
|
||||||
|
list.add(FilterItem.Header(R.string.genres))
|
||||||
|
selectedTags.sortedBy { it.title }.mapTo(list) {
|
||||||
|
FilterItem.Tag(it, isChecked = it in selectedTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.add(FilterItem.Loading)
|
||||||
|
filter.postValue(list)
|
||||||
|
updateFilters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun tryLoadTags(): Set<MangaTag>? {
|
||||||
|
val shouldRetryOnError = availableTagsDeferred.isCompleted
|
||||||
|
val result = availableTagsDeferred.await()
|
||||||
|
if (result == null && shouldRetryOnError) {
|
||||||
|
availableTagsDeferred = loadTagsAsync()
|
||||||
|
return availableTagsDeferred.await()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadTagsAsync() = viewModelScope.async(Dispatchers.Default) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
repository.getTags()
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
package org.koitharu.kotatsu.list.ui.model
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.core.model.SortOrder
|
||||||
|
|
||||||
data class ListHeader(
|
data class ListHeader(
|
||||||
val text: CharSequence?,
|
val text: CharSequence?,
|
||||||
@StringRes val textRes: Int,
|
@StringRes val textRes: Int,
|
||||||
|
val sortOrder: SortOrder?,
|
||||||
) : ListModel
|
) : ListModel
|
||||||
@ -1,50 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
||||||
android:id="@+id/swipeRefreshLayout"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recyclerView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="@dimen/grid_spacing_outer"
|
|
||||||
app:fastScrollEnabled="true"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
tools:listitem="@layout/item_manga_list" />
|
|
||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/divider_filter"
|
|
||||||
android:layout_width="1dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="?attr/colorOutline"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recyclerView_filter"
|
|
||||||
android:layout_width="240dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
android:background="?android:windowBackground"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:scrollbars="vertical"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
tools:listitem="@layout/item_category_checkable"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@ -1,39 +1,21 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.drawerlayout.widget.DrawerLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/swipeRefreshLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
||||||
android:id="@+id/swipeRefreshLayout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recyclerView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="@dimen/grid_spacing_outer"
|
|
||||||
app:fastScrollEnabled="true"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
tools:listitem="@layout/item_manga_list" />
|
|
||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerView_filter"
|
android:id="@+id/recyclerView"
|
||||||
android:layout_width="240dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="end"
|
|
||||||
android:background="?android:windowBackground"
|
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:scrollbars="vertical"
|
android:padding="@dimen/grid_spacing_outer"
|
||||||
|
app:fastScrollEnabled="true"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:listitem="@layout/item_category_checkable" />
|
tools:listitem="@layout/item_manga_list" />
|
||||||
|
|
||||||
</androidx.drawerlayout.widget.DrawerLayout>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_toStartOf="@id/textView_filter"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
|
||||||
|
tools:text="@tools:sample/lorem[21]" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_filter"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:background="@drawable/list_selector"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
|
||||||
|
app:drawableEndCompat="@drawable/ic_drop_down"
|
||||||
|
app:drawableTint="?android:attr/textColorSecondary"
|
||||||
|
tools:ignore="RtlSymmetry"
|
||||||
|
tools:text="@string/popular" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:navigationIcon="@drawable/ic_cross"
|
||||||
|
app:title="@string/filter" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:fastScrollEnabled="true"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/item_category_checkable" />
|
||||||
|
</LinearLayout>
|
||||||
Loading…
Reference in New Issue