Tracker debug info
This commit is contained in:
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 633
|
versionCode = 634
|
||||||
versionName = '6.8.3'
|
versionName = '7.0-a1'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||||
ksp {
|
ksp {
|
||||||
@@ -104,7 +104,7 @@ dependencies {
|
|||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'com.google.android.material:material:1.12.0-beta01'
|
implementation 'com.google.android.material:material:1.12.0-rc01'
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.7.0'
|
||||||
implementation 'androidx.webkit:webkit:1.10.0'
|
implementation 'androidx.webkit:webkit:1.10.0'
|
||||||
|
|
||||||
|
|||||||
12
app/src/debug/AndroidManifest.xml
Normal file
12
app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".tracker.ui.debug.TrackerDebugActivity"
|
||||||
|
android:label="@string/check_for_new_chapters" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.debug
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.text.bold
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
|
import androidx.core.text.color
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.drawableStart
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemTrackDebugBinding
|
||||||
|
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
fun trackDebugAD(
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
coil: ImageLoader,
|
||||||
|
clickListener: OnListItemClickListener<TrackDebugItem>,
|
||||||
|
) = adapterDelegateViewBinding<TrackDebugItem, TrackDebugItem, ItemTrackDebugBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemTrackDebugBinding.inflate(layoutInflater, parent, false) },
|
||||||
|
) {
|
||||||
|
val indicatorNew = ContextCompat.getDrawable(context, R.drawable.ic_new)
|
||||||
|
|
||||||
|
itemView.setOnClickListener { v ->
|
||||||
|
clickListener.onItemClick(item, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
|
||||||
|
placeholder(R.drawable.ic_placeholder)
|
||||||
|
fallback(R.drawable.ic_placeholder)
|
||||||
|
error(R.drawable.ic_error_placeholder)
|
||||||
|
allowRgb565(true)
|
||||||
|
source(item.manga.source)
|
||||||
|
enqueueWith(coil)
|
||||||
|
}
|
||||||
|
binding.textViewTitle.text = item.manga.title
|
||||||
|
binding.textViewSummary.text = buildSpannedString {
|
||||||
|
item.lastCheckTime?.let {
|
||||||
|
append(
|
||||||
|
DateUtils.getRelativeDateTimeString(
|
||||||
|
context,
|
||||||
|
it.toEpochMilli(),
|
||||||
|
DateUtils.MINUTE_IN_MILLIS,
|
||||||
|
DateUtils.WEEK_IN_MILLIS,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (item.lastResult == TrackEntity.RESULT_FAILED) {
|
||||||
|
append(" - ")
|
||||||
|
bold {
|
||||||
|
color(context.getThemeColor(materialR.attr.colorError, Color.RED)) {
|
||||||
|
append(getString(R.string.error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.textViewTitle.drawableStart = if (item.newChapters > 0) {
|
||||||
|
indicatorNew
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.debug
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
data class TrackDebugItem(
|
||||||
|
val manga: Manga,
|
||||||
|
val lastChapterId: Long,
|
||||||
|
val newChapters: Int,
|
||||||
|
val lastCheckTime: Instant?,
|
||||||
|
val lastChapterDate: Instant?,
|
||||||
|
val lastResult: Int,
|
||||||
|
) : ListModel {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
return other is TrackDebugItem && other.manga.id == manga.id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.debug
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import coil.ImageLoader
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
|
import org.koitharu.kotatsu.databinding.ActivityTrackerDebugBinding
|
||||||
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class TrackerDebugActivity : BaseActivity<ActivityTrackerDebugBinding>(), OnListItemClickListener<TrackDebugItem> {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
|
private val viewModel by viewModels<TrackerDebugViewModel>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(ActivityTrackerDebugBinding.inflate(layoutInflater))
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
val tracksAdapter = BaseListAdapter<TrackDebugItem>()
|
||||||
|
.addDelegate(ListItemType.FEED, trackDebugAD(this, coil, this))
|
||||||
|
with(viewBinding.recyclerView) {
|
||||||
|
adapter = tracksAdapter
|
||||||
|
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||||
|
}
|
||||||
|
viewModel.content.observe(this, tracksAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
val rv = viewBinding.recyclerView
|
||||||
|
rv.updatePadding(
|
||||||
|
left = insets.left + rv.paddingTop,
|
||||||
|
right = insets.right + rv.paddingTop,
|
||||||
|
bottom = insets.bottom,
|
||||||
|
)
|
||||||
|
viewBinding.toolbar.updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(item: TrackDebugItem, view: View) {
|
||||||
|
startActivity(DetailsActivity.newIntent(this, item.manga))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.debug
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
|
||||||
|
import org.koitharu.kotatsu.tracker.data.TrackWithManga
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class TrackerDebugViewModel @Inject constructor(
|
||||||
|
private val db: MangaDatabase
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
val content = db.getTracksDao().observeAll()
|
||||||
|
.map { it.toUiList() }
|
||||||
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
|
private fun List<TrackWithManga>.toUiList(): List<TrackDebugItem> = map {
|
||||||
|
TrackDebugItem(
|
||||||
|
manga = it.manga.toManga(emptySet()),
|
||||||
|
lastChapterId = it.track.lastChapterId,
|
||||||
|
newChapters = it.track.newChapters,
|
||||||
|
lastCheckTime = it.track.lastCheckTime.toInstantOrNull(),
|
||||||
|
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
|
||||||
|
lastResult = it.track.lastResult,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/src/debug/res/layout/activity_tracker_debug.xml
Normal file
44
app/src/debug/res/layout/activity_tracker_debug.xml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
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">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
android:id="@+id/collapsingToolbarLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
|
||||||
|
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
||||||
|
app:toolbarId="@id/toolbar">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:layout_collapseMode="pin" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||||
|
|
||||||
|
</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"
|
||||||
|
android:padding="@dimen/list_spacing_normal"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||||
|
tools:listitem="@layout/item_track_debug" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
56
app/src/debug/res/layout/item_track_debug.xml
Normal file
56
app/src/debug/res/layout/item_track_debug.xml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
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="72dp"
|
||||||
|
android:background="@drawable/list_selector"
|
||||||
|
android:clipChildren="false">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/imageView_cover"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/textView_summary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
|
||||||
|
tools:text="@tools:sample/lorem" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_summary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/imageView_cover"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/textView_title"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -8,4 +8,9 @@
|
|||||||
android:title="@string/leak_canary_display_activity_label"
|
android:title="@string/leak_canary_display_activity_label"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
</menu>
|
<item
|
||||||
|
android:id="@id/action_tracker"
|
||||||
|
android:title="@string/check_for_new_chapters"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ class SettingsActivity :
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.action_tracker -> {
|
||||||
|
val intent = Intent()
|
||||||
|
intent.component = ComponentName(this, "org.koitharu.kotatsu.tracker.ui.debug.TrackerDebugActivity")
|
||||||
|
startActivity(intent)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ abstract class TracksDao {
|
|||||||
@Query("SELECT * FROM tracks ORDER BY last_check_time ASC LIMIT :limit OFFSET :offset")
|
@Query("SELECT * FROM tracks ORDER BY last_check_time ASC LIMIT :limit OFFSET :offset")
|
||||||
abstract suspend fun findAll(offset: Int, limit: Int): List<TrackWithManga>
|
abstract suspend fun findAll(offset: Int, limit: Int): List<TrackWithManga>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM tracks ORDER BY last_check_time DESC")
|
||||||
|
abstract fun observeAll(): Flow<List<TrackWithManga>>
|
||||||
|
|
||||||
@Query("SELECT manga_id FROM tracks")
|
@Query("SELECT manga_id FROM tracks")
|
||||||
abstract suspend fun findAllIds(): LongArray
|
abstract suspend fun findAllIds(): LongArray
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
@@ -337,8 +338,9 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startNow() {
|
fun startNow() {
|
||||||
val constraints =
|
val constraints = Constraints.Builder()
|
||||||
Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
val request = OneTimeWorkRequestBuilder<TrackWorker>()
|
val request = OneTimeWorkRequestBuilder<TrackWorker>()
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.addTag(TAG_ONESHOT)
|
.addTag(TAG_ONESHOT)
|
||||||
@@ -370,6 +372,6 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
const val MAX_ATTEMPTS = 3
|
const val MAX_ATTEMPTS = 3
|
||||||
const val DATA_KEY_SUCCESS = "success"
|
const val DATA_KEY_SUCCESS = "success"
|
||||||
const val DATA_KEY_FAILED = "failed"
|
const val DATA_KEY_FAILED = "failed"
|
||||||
const val BATCH_SIZE = 20
|
val BATCH_SIZE = if (BuildConfig.DEBUG) 20 else 46
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<item name="toolbar" type="id" />
|
<item name="toolbar" type="id" />
|
||||||
<item name="container" type="id" />
|
<item name="container" type="id" />
|
||||||
<item name="action_leaks" type="id" />
|
<item name="action_leaks" type="id" />
|
||||||
|
<item name="action_tracker" type="id" />
|
||||||
<item name="fast_scroller" type="id" />
|
<item name="fast_scroller" type="id" />
|
||||||
<item name="group_branches" type="id" />
|
<item name="group_branches" type="id" />
|
||||||
<item name="layout_tip" type="id" />
|
<item name="layout_tip" type="id" />
|
||||||
|
|||||||
Reference in New Issue
Block a user