Initial migrate SearchLayout to Compose
TODO: advanced options
This commit is contained in:
1 parent
2829d081cc
commit
7df7a783f6
15 files changed
+298
-1366
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
package com.hippo.ehviewer
|
||||
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
@@ -59,6 +60,7 @@ object Settings : DataStorePreferences(null) {
|
||||
var securityDelay by intPref("require_unlock_delay", 0)
|
||||
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
|
||||
var listThumbSize by intPref("list_tile_size", 40)
|
||||
var searchCategory by intPref("search_pref", EhUtils.ALL_CATEGORY)
|
||||
var requestNewsTimerHour by intPref("request_news_timer_hour", -1)
|
||||
var requestNewsTimerMinute by intPref("request_news_timer_minute", -1)
|
||||
var dataMapNextId by intPref("data_map_next_id", 0)
|
||||
@@ -66,7 +68,6 @@ object Settings : DataStorePreferences(null) {
|
||||
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
|
||||
var displayName by stringOrNullPref("display_name", null)
|
||||
var avatar by stringOrNullPref("avatar", null)
|
||||
var dohUrl by stringPref("doh_url", "")
|
||||
var language by stringPref("app_language", "system").observed { updateWhenLocaleChanges() }
|
||||
var lastDawnDay by longPref("last_dawn_day", 0)
|
||||
|
||||
|
||||
@@ -15,132 +15,7 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.CheckBox
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.yorozuya.toIntOrDefault
|
||||
|
||||
class AdvanceSearchTable @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : LinearLayout(context, attrs) {
|
||||
private var mSh: CheckBox
|
||||
private var mSto: CheckBox
|
||||
private var mMinRating: TextInputLayout
|
||||
private var mSp: CheckBox
|
||||
private var mSpf: EditText
|
||||
private var mSpt: EditText
|
||||
private var mSfl: CheckBox
|
||||
private var mSfu: CheckBox
|
||||
private var mSft: CheckBox
|
||||
private val mArray: Array<String> = context.resources.getStringArray(R.array.search_min_rating)
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
val inflater = LayoutInflater.from(context)
|
||||
inflater.inflate(R.layout.widget_advance_search_table, this)
|
||||
val row0 = getChildAt(0) as ViewGroup
|
||||
mSh = row0.getChildAt(0) as CheckBox
|
||||
mSto = row0.getChildAt(1) as CheckBox
|
||||
val row1 = getChildAt(1) as ViewGroup
|
||||
mMinRating = row1.getChildAt(0) as TextInputLayout
|
||||
val row2 = getChildAt(2) as ViewGroup
|
||||
mSp = row2.getChildAt(0) as CheckBox
|
||||
mSpf = row2.getChildAt(1) as EditText
|
||||
mSpt = row2.getChildAt(3) as EditText
|
||||
val row4 = getChildAt(4) as ViewGroup
|
||||
mSfl = row4.getChildAt(0) as CheckBox
|
||||
mSfu = row4.getChildAt(1) as CheckBox
|
||||
mSft = row4.getChildAt(2) as CheckBox
|
||||
|
||||
mSpt.setOnEditorActionListener { v: TextView, _: Int, _: KeyEvent? ->
|
||||
val nextView = v.focusSearch(
|
||||
FOCUS_DOWN,
|
||||
)
|
||||
nextView?.requestFocus(FOCUS_DOWN)
|
||||
true
|
||||
}
|
||||
|
||||
mSpt.isEnabled = mSp.isChecked
|
||||
mSpf.isEnabled = mSp.isChecked
|
||||
mSp.setOnCheckedChangeListener { _: CompoundButton, isChecked: Boolean ->
|
||||
mSpt.isEnabled = isChecked
|
||||
mSpf.isEnabled = isChecked
|
||||
}
|
||||
}
|
||||
|
||||
var advanceSearch: Int
|
||||
get() {
|
||||
var advanceSearch = 0
|
||||
if (mSh.isChecked) advanceSearch = advanceSearch or SH
|
||||
if (mSto.isChecked) advanceSearch = advanceSearch or STO
|
||||
if (mSfl.isChecked) advanceSearch = advanceSearch or SFL
|
||||
if (mSfu.isChecked) advanceSearch = advanceSearch or SFU
|
||||
if (mSft.isChecked) advanceSearch = advanceSearch or SFT
|
||||
return advanceSearch
|
||||
}
|
||||
set(advanceSearch) {
|
||||
mSh.isChecked = advanceSearch and SH != 0
|
||||
mSto.isChecked = advanceSearch and STO != 0
|
||||
mSfl.isChecked = advanceSearch and SFL != 0
|
||||
mSfu.isChecked = advanceSearch and SFU != 0
|
||||
mSft.isChecked = advanceSearch and SFT != 0
|
||||
}
|
||||
var minRating: Int
|
||||
get() {
|
||||
val position = mArray.indexOf(mMinRating.editText!!.text.toString())
|
||||
return if (position > 0) {
|
||||
position + 1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
set(minRating) {
|
||||
(mMinRating.editText!! as AutoCompleteTextView).setText(
|
||||
mArray[if (minRating in 2..5) minRating - 1 else 0],
|
||||
false,
|
||||
)
|
||||
}
|
||||
var pageFrom: Int
|
||||
get() = if (mSp.isChecked) {
|
||||
mSpf.text.toString().toIntOrDefault(-1)
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
set(pageFrom) {
|
||||
if (pageFrom > 0) {
|
||||
mSpf.setText(pageFrom.toString())
|
||||
mSp.isChecked = true
|
||||
} else {
|
||||
mSp.isChecked = false
|
||||
mSpf.text = null
|
||||
}
|
||||
}
|
||||
var pageTo: Int
|
||||
get() = if (mSp.isChecked) {
|
||||
mSpt.text.toString().toIntOrDefault(-1)
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
set(pageTo) {
|
||||
if (pageTo > 0) {
|
||||
mSpt.setText(pageTo.toString())
|
||||
mSp.isChecked = true
|
||||
} else {
|
||||
mSp.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
class AdvanceSearchTable {
|
||||
companion object {
|
||||
const val SH = 0x1
|
||||
const val STO = 0x2
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.hippo.ehviewer.AppConfig
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.util.pickVisualMedia
|
||||
import com.hippo.unifile.UniFile
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ImageSearchLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0,
|
||||
) : AbstractComposeView(context, attrs, defStyle) {
|
||||
private var uss by mutableStateOf(false)
|
||||
private var osc by mutableStateOf(false)
|
||||
private var path by mutableStateOf("")
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Mdc3Theme {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
fun selectImage() = coroutineScope.launch {
|
||||
context.pickVisualMedia(ImageOnly)?.let {
|
||||
path = it.toString()
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
AnimatedVisibility(visible = path.isNotBlank(), modifier = Modifier.align(Alignment.CenterHorizontally)) {
|
||||
Card {
|
||||
val maxSize = dimensionResource(id = R.dimen.image_search_max_size)
|
||||
AsyncImage(
|
||||
model = path,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.sizeIn(maxWidth = maxSize, maxHeight = maxSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = ::selectImage,
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.select_image))
|
||||
}
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Row {
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
Checkbox(checked = uss, onCheckedChange = { uss = it })
|
||||
Text(text = stringResource(id = R.string.search_uss), modifier = Modifier.align(Alignment.CenterVertically))
|
||||
}
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
Checkbox(checked = osc, onCheckedChange = { osc = it })
|
||||
Text(text = stringResource(id = R.string.search_osc), modifier = Modifier.align(Alignment.CenterVertically))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatListUrlBuilder(lub: ListUrlBuilder) {
|
||||
if (path.isBlank()) throw EhException(context.getString(R.string.select_image_first))
|
||||
val uri = Uri.parse(path)
|
||||
val src = UniFile.fromUri(context, uri)?.imageSource ?: return
|
||||
val temp = AppConfig.createTempFile() ?: return
|
||||
val bitmap = ImageDecoder.decodeBitmap(src, Image.imageSearchDecoderSampleListener)
|
||||
temp.outputStream().use { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it) }
|
||||
lub.imagePath = temp.path
|
||||
lub.isUseSimilarityScan = uss
|
||||
lub.isOnlySearchCovers = osc
|
||||
}
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.RadioButton
|
||||
import androidx.annotation.IdRes
|
||||
|
||||
class RadioGridGroup : SimpleGridLayout {
|
||||
/**
|
||||
*
|
||||
* Returns the identifier of the selected radio button in this group.
|
||||
* Upon empty selection, the returned value is -1.
|
||||
*
|
||||
* @return the unique id of the selected radio button in this group
|
||||
* @see .check
|
||||
* @see .clearCheck
|
||||
*/
|
||||
// holds the checked id; the selection is empty by default
|
||||
@get:IdRes
|
||||
var checkedRadioButtonId = -1
|
||||
private set
|
||||
|
||||
// tracks children radio buttons checked state
|
||||
private var mChildOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||
|
||||
// when true, mOnCheckedChangeListener discards events
|
||||
private var mProtectFromCheckedChange = false
|
||||
private var mOnCheckedChangeListener: OnCheckedChangeListener? = null
|
||||
private var mPassThroughListener: PassThroughHierarchyChangeListener? = null
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
||||
init(context, attrs)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyle,
|
||||
) {
|
||||
init(context, attrs)
|
||||
}
|
||||
|
||||
private fun init(context: Context, attrs: AttributeSet?) {
|
||||
val a = context.obtainStyledAttributes(attrs, RADIO_ATTRS)
|
||||
val value = a.getResourceId(0, NO_ID)
|
||||
if (value != NO_ID) {
|
||||
checkedRadioButtonId = value
|
||||
}
|
||||
a.recycle()
|
||||
mChildOnCheckedChangeListener = CheckedStateTracker()
|
||||
mPassThroughListener = PassThroughHierarchyChangeListener()
|
||||
super.setOnHierarchyChangeListener(mPassThroughListener)
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override fun setOnHierarchyChangeListener(listener: OnHierarchyChangeListener) {
|
||||
// the user listener is delegated to our pass-through listener
|
||||
mPassThroughListener!!.mOnHierarchyChangeListener = listener
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
|
||||
// checks the appropriate radio button as requested in the XML file
|
||||
if (checkedRadioButtonId != -1) {
|
||||
mProtectFromCheckedChange = true
|
||||
setCheckedStateForView(checkedRadioButtonId, true)
|
||||
mProtectFromCheckedChange = false
|
||||
setCheckedId(checkedRadioButtonId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addView(child: View, index: Int, params: LayoutParams) {
|
||||
if (child is RadioButton) {
|
||||
if (child.isChecked) {
|
||||
mProtectFromCheckedChange = true
|
||||
if (checkedRadioButtonId != -1) {
|
||||
setCheckedStateForView(checkedRadioButtonId, false)
|
||||
}
|
||||
mProtectFromCheckedChange = false
|
||||
setCheckedId(child.getId())
|
||||
}
|
||||
}
|
||||
super.addView(child, index, params)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Sets the selection to the radio button whose identifier is passed in
|
||||
* parameter. Using -1 as the selection identifier clears the selection;
|
||||
* such an operation is equivalent to invoking [.clearCheck].
|
||||
*
|
||||
* @param id the unique id of the radio button to select in this group
|
||||
* @see .getCheckedRadioButtonId
|
||||
* @see .clearCheck
|
||||
*/
|
||||
fun check(@IdRes id: Int) {
|
||||
// don't even bother
|
||||
if (id != -1 && id == checkedRadioButtonId) {
|
||||
return
|
||||
}
|
||||
if (checkedRadioButtonId != -1) {
|
||||
setCheckedStateForView(checkedRadioButtonId, false)
|
||||
}
|
||||
if (id != -1) {
|
||||
setCheckedStateForView(id, true)
|
||||
}
|
||||
setCheckedId(id)
|
||||
}
|
||||
|
||||
private fun setCheckedId(@IdRes id: Int) {
|
||||
checkedRadioButtonId = id
|
||||
if (mOnCheckedChangeListener != null) {
|
||||
mOnCheckedChangeListener!!.onCheckedChanged(this, checkedRadioButtonId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCheckedStateForView(viewId: Int, checked: Boolean) {
|
||||
val checkedView = findViewById<View>(viewId)
|
||||
if (checkedView is RadioButton) {
|
||||
checkedView.isChecked = checked
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Clears the selection. When the selection is cleared, no radio button
|
||||
* in this group is selected and [.getCheckedRadioButtonId] returns
|
||||
* null.
|
||||
*
|
||||
* @see .check
|
||||
* @see .getCheckedRadioButtonId
|
||||
*/
|
||||
fun clearCheck() {
|
||||
check(-1)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Register a callback to be invoked when the checked radio button
|
||||
* changes in this group.
|
||||
*
|
||||
* @param listener the callback to call on checked state change
|
||||
*/
|
||||
fun setOnCheckedChangeListener(listener: OnCheckedChangeListener?) {
|
||||
mOnCheckedChangeListener = listener
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Interface definition for a callback to be invoked when the checked
|
||||
* radio button changed in this group.
|
||||
*/
|
||||
interface OnCheckedChangeListener {
|
||||
/**
|
||||
*
|
||||
* Called when the checked radio button has changed. When the
|
||||
* selection is cleared, checkedId is -1.
|
||||
*
|
||||
* @param group the group in which the checked radio button has changed
|
||||
* @param checkedId the unique identifier of the newly checked radio button
|
||||
*/
|
||||
fun onCheckedChanged(group: RadioGridGroup?, @IdRes checkedId: Int)
|
||||
}
|
||||
|
||||
private inner class CheckedStateTracker : CompoundButton.OnCheckedChangeListener {
|
||||
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||
// prevents from infinite recursion
|
||||
if (mProtectFromCheckedChange) {
|
||||
return
|
||||
}
|
||||
mProtectFromCheckedChange = true
|
||||
if (checkedRadioButtonId != -1) {
|
||||
setCheckedStateForView(checkedRadioButtonId, false)
|
||||
}
|
||||
mProtectFromCheckedChange = false
|
||||
val id = buttonView.id
|
||||
setCheckedId(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* A pass-through listener acts upon the events and dispatches them
|
||||
* to another listener. This allows the table layout to set its own internal
|
||||
* hierarchy change listener without preventing the user to setup his.
|
||||
*/
|
||||
private inner class PassThroughHierarchyChangeListener : OnHierarchyChangeListener {
|
||||
var mOnHierarchyChangeListener: OnHierarchyChangeListener? = null
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override fun onChildViewAdded(parent: View, child: View) {
|
||||
if (parent === this@RadioGridGroup && child is RadioButton) {
|
||||
var id = child.getId()
|
||||
// generates an id if it's missing
|
||||
if (id == NO_ID) {
|
||||
id = generateViewId()
|
||||
child.setId(id)
|
||||
}
|
||||
child.setOnCheckedChangeListener(mChildOnCheckedChangeListener)
|
||||
}
|
||||
mOnHierarchyChangeListener?.onChildViewAdded(parent, child)
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override fun onChildViewRemoved(parent: View, child: View) {
|
||||
if (parent === this@RadioGridGroup && child is RadioButton) {
|
||||
child.setOnCheckedChangeListener(null)
|
||||
}
|
||||
mOnHierarchyChangeListener?.onChildViewRemoved(parent, child)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val RADIO_ATTRS = intArrayOf(android.R.attr.checkedButton)
|
||||
}
|
||||
}
|
||||
@@ -1,375 +1,167 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CompoundButton
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.forEach
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.hippo.ehviewer.AppConfig
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.databinding.SearchAdvanceBinding
|
||||
import com.hippo.ehviewer.databinding.SearchCategoryBinding
|
||||
import com.hippo.ehviewer.databinding.SearchNormalBinding
|
||||
import com.hippo.ehviewer.yorozuya.removeFromParent
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.ui.main.ImageSearch
|
||||
import com.hippo.ehviewer.ui.main.NormalSearch
|
||||
import com.hippo.ehviewer.util.pickVisualMedia
|
||||
import com.hippo.unifile.UniFile
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
class SearchLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : EasyRecyclerView(context, attrs),
|
||||
CompoundButton.OnCheckedChangeListener,
|
||||
View.OnClickListener,
|
||||
OnTabSelectedListener {
|
||||
defStyle: Int = 0,
|
||||
) : AbstractComposeView(context, attrs, defStyle) {
|
||||
private var isNormalMode by mutableStateOf(true) // else ImageSearch mode
|
||||
private var isAdvancedMode by mutableStateOf(false)
|
||||
|
||||
@SearchMode
|
||||
private var mSearchMode = SEARCH_MODE_NORMAL
|
||||
private var mEnableAdvance = false
|
||||
private val binding: SearchNormalBinding
|
||||
private val advanceBinding: SearchAdvanceBinding
|
||||
private val mImageView: ImageSearchLayout
|
||||
private val mActionView: View
|
||||
private val mAction: TabLayout
|
||||
private val mAdapter: SearchAdapter
|
||||
private var mHelper: Helper? = null
|
||||
private val mSharePref: SharedPreferences = Settings.prefs
|
||||
private val mInflater: LayoutInflater
|
||||
private var mCategory by mutableIntStateOf(Settings.searchCategory)
|
||||
private var mSearchMode by mutableIntStateOf(1)
|
||||
|
||||
init {
|
||||
mInflater = LayoutInflater.from(context)
|
||||
val resources = context.resources
|
||||
layoutManager = SearchLayoutManager(context)
|
||||
mAdapter = SearchAdapter()
|
||||
mAdapter.setHasStableIds(true)
|
||||
adapter = mAdapter
|
||||
setHasFixedSize(true)
|
||||
clipToPadding = false
|
||||
(itemAnimator as DefaultItemAnimator?)?.supportsChangeAnimations = false
|
||||
val interval = resources.getDimensionPixelOffset(R.dimen.search_layout_interval)
|
||||
val paddingH = resources.getDimensionPixelOffset(R.dimen.search_layout_margin_h)
|
||||
val paddingV = resources.getDimensionPixelOffset(R.dimen.search_layout_margin_v)
|
||||
val decoration = MarginItemDecoration(
|
||||
interval,
|
||||
paddingH,
|
||||
paddingV,
|
||||
paddingH,
|
||||
paddingV,
|
||||
)
|
||||
addItemDecoration(decoration)
|
||||
decoration.applyPaddings(this)
|
||||
// Create normal view
|
||||
binding = SearchNormalBinding.inflate(mInflater)
|
||||
val mCategoryStored = mSharePref.getInt(SEARCH_CATEGORY_PREF, EhUtils.ALL_CATEGORY)
|
||||
for (mPair in mCategoryTable) {
|
||||
val mChip = inflate(context, R.layout.filter_chip, null) as IdentifiedChip
|
||||
mChip.isCheckable = true
|
||||
mChip.setText(mPair.second)
|
||||
mChip.idt = mPair.first
|
||||
mChip.isChecked = mPair.first and mCategoryStored != 0
|
||||
mChip.setOnLongClickListener {
|
||||
if (mChip.isChecked) {
|
||||
binding.searchCategoryChipgroup.forEach {
|
||||
(it as IdentifiedChip).isChecked = true
|
||||
private var uss by mutableStateOf(false)
|
||||
private var osc by mutableStateOf(false)
|
||||
private var path by mutableStateOf("")
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
fun selectImage() = coroutineScope.launch {
|
||||
context.pickVisualMedia(ActivityResultContracts.PickVisualMedia.ImageOnly)?.let {
|
||||
path = it.toString()
|
||||
}
|
||||
}
|
||||
Mdc3Theme {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_layout_margin_h)),
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.search_layout_margin_v)),
|
||||
) {
|
||||
AnimatedVisibility(visible = isNormalMode) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_category_padding_h), vertical = dimensionResource(id = R.dimen.search_category_padding_v))) {
|
||||
Text(text = stringResource(id = R.string.search_normal), modifier = Modifier.height(dimensionResource(id = R.dimen.search_category_title_height)), style = MaterialTheme.typography.titleMedium)
|
||||
NormalSearch(
|
||||
category = mCategory,
|
||||
onCategoryChanged = {
|
||||
Settings.searchCategory = it
|
||||
mCategory = it
|
||||
},
|
||||
searchMode = mSearchMode,
|
||||
onSearchModeChanged = { mSearchMode = it },
|
||||
isAdvanced = isAdvancedMode,
|
||||
onAdvancedChanged = { isAdvancedMode = it },
|
||||
showInfo = { BaseDialogBuilder(context).setMessage(R.string.search_tip).show() },
|
||||
)
|
||||
}
|
||||
}
|
||||
mChip.isChecked = false
|
||||
} else {
|
||||
binding.searchCategoryChipgroup.clearCheck()
|
||||
mChip.isChecked = true
|
||||
}
|
||||
true
|
||||
AnimatedVisibility(visible = isNormalMode && isAdvancedMode) {
|
||||
ElevatedCard {
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(visible = !isNormalMode) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_category_padding_h), vertical = dimensionResource(id = R.dimen.search_category_padding_v))) {
|
||||
Text(text = stringResource(id = R.string.search_image), modifier = Modifier.height(dimensionResource(id = R.dimen.search_category_title_height)), style = MaterialTheme.typography.titleMedium)
|
||||
ImageSearch(
|
||||
imagePath = path,
|
||||
onSelectImage = ::selectImage,
|
||||
uss = uss,
|
||||
onUssChecked = { uss = it },
|
||||
osc = osc,
|
||||
onOscChecked = { osc = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
TabRow(
|
||||
selectedTabIndex = if (isNormalMode) 0 else 1,
|
||||
divider = {},
|
||||
) {
|
||||
Tab(
|
||||
selected = isNormalMode,
|
||||
onClick = { isNormalMode = true },
|
||||
text = { Text(text = stringResource(id = R.string.keyword_search)) },
|
||||
)
|
||||
Tab(
|
||||
selected = !isNormalMode,
|
||||
onClick = { isNormalMode = false },
|
||||
text = { Text(text = stringResource(id = R.string.search_image)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.searchCategoryChipgroup.addView(mChip)
|
||||
}
|
||||
binding.searchCategoryChipgroup.setOnCheckedStateChangeListener { group, checkedIds ->
|
||||
var mCategory = 0
|
||||
for (index in checkedIds) {
|
||||
mCategory = mCategory or group.findViewById<IdentifiedChip>(index).idt
|
||||
}
|
||||
mSharePref.edit { putInt(SEARCH_CATEGORY_PREF, mCategory) }
|
||||
}
|
||||
binding.normalSearchModeHelp.setOnClickListener(this)
|
||||
binding.searchEnableAdvance.setOnCheckedChangeListener(this)
|
||||
// Create advance view
|
||||
advanceBinding = SearchAdvanceBinding.inflate(mInflater)
|
||||
// Create image view
|
||||
mImageView = mInflater.inflate(R.layout.search_image, null) as ImageSearchLayout
|
||||
// Create action view
|
||||
mActionView = mInflater.inflate(R.layout.search_action, null)
|
||||
mActionView.layoutParams = LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
mAction = mActionView.findViewById(R.id.action)
|
||||
mAction.addOnTabSelectedListener(this)
|
||||
}
|
||||
|
||||
fun setHelper(helper: Helper?) {
|
||||
mHelper = helper
|
||||
}
|
||||
|
||||
fun scrollSearchContainerToTop() {
|
||||
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
|
||||
}
|
||||
|
||||
fun setNormalSearchMode(id: Int) {
|
||||
binding.normalSearchMode.check(id)
|
||||
fun setSearchMyTags(isMyTags: Boolean) {
|
||||
if (isMyTags) mSearchMode = 2
|
||||
}
|
||||
|
||||
fun setCategory(category: Int) {
|
||||
binding.searchCategoryChipgroup.forEach {
|
||||
(it as IdentifiedChip).apply {
|
||||
isChecked = idt and category != 0
|
||||
}
|
||||
}
|
||||
mCategory = category
|
||||
}
|
||||
|
||||
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||
if (buttonView === binding.searchEnableAdvance) {
|
||||
post {
|
||||
mEnableAdvance = isChecked
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL) {
|
||||
if (mEnableAdvance) {
|
||||
mAdapter.notifyItemInserted(1)
|
||||
} else {
|
||||
mAdapter.notifyItemRemoved(1)
|
||||
}
|
||||
if (mHelper != null) {
|
||||
mHelper!!.onChangeSearchMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NonConstantResourceId")
|
||||
fun formatListUrlBuilder(urlBuilder: ListUrlBuilder, query: String?) {
|
||||
urlBuilder.reset()
|
||||
when (mSearchMode) {
|
||||
SEARCH_MODE_NORMAL -> {
|
||||
when (binding.normalSearchMode.checkedRadioButtonId) {
|
||||
R.id.search_subscription_search -> {
|
||||
urlBuilder.mode = ListUrlBuilder.MODE_SUBSCRIPTION
|
||||
}
|
||||
|
||||
R.id.search_specify_uploader -> {
|
||||
urlBuilder.mode = ListUrlBuilder.MODE_UPLOADER
|
||||
}
|
||||
|
||||
R.id.search_specify_tag -> {
|
||||
urlBuilder.mode = ListUrlBuilder.MODE_TAG
|
||||
}
|
||||
|
||||
else -> {
|
||||
urlBuilder.mode = ListUrlBuilder.MODE_NORMAL
|
||||
}
|
||||
when (isNormalMode) {
|
||||
true -> {
|
||||
when (mSearchMode) {
|
||||
1 -> urlBuilder.mode = ListUrlBuilder.MODE_NORMAL
|
||||
2 -> urlBuilder.mode = ListUrlBuilder.MODE_SUBSCRIPTION
|
||||
3 -> urlBuilder.mode = ListUrlBuilder.MODE_UPLOADER
|
||||
4 -> urlBuilder.mode = ListUrlBuilder.MODE_TAG
|
||||
}
|
||||
urlBuilder.keyword = query
|
||||
urlBuilder.category = Settings.prefs.getInt(
|
||||
SEARCH_CATEGORY_PREF,
|
||||
0,
|
||||
)
|
||||
if (mEnableAdvance) {
|
||||
urlBuilder.advanceSearch = advanceBinding.searchAdvanceSearchTable.advanceSearch
|
||||
urlBuilder.minRating = advanceBinding.searchAdvanceSearchTable.minRating
|
||||
val pageFrom = advanceBinding.searchAdvanceSearchTable.pageFrom
|
||||
val pageTo = advanceBinding.searchAdvanceSearchTable.pageTo
|
||||
if (pageTo != -1 && pageTo < 10) {
|
||||
throw EhException(context.getString(R.string.search_sp_err1))
|
||||
} else if (pageFrom != -1 && pageTo != -1 && pageTo - pageFrom < 20) {
|
||||
throw EhException(context.getString(R.string.search_sp_err2))
|
||||
}
|
||||
urlBuilder.pageFrom = pageFrom
|
||||
urlBuilder.pageTo = pageTo
|
||||
}
|
||||
urlBuilder.category = mCategory
|
||||
}
|
||||
|
||||
SEARCH_MODE_IMAGE -> {
|
||||
false -> {
|
||||
urlBuilder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
|
||||
mImageView.formatListUrlBuilder(urlBuilder)
|
||||
if (path.isBlank()) throw EhException(context.getString(R.string.select_image_first))
|
||||
val uri = Uri.parse(path)
|
||||
val src = UniFile.fromUri(context, uri)?.imageSource ?: return
|
||||
val temp = AppConfig.createTempFile() ?: return
|
||||
val bitmap = ImageDecoder.decodeBitmap(src, Image.imageSearchDecoderSampleListener)
|
||||
temp.outputStream().use { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it) }
|
||||
urlBuilder.imagePath = temp.path
|
||||
urlBuilder.isUseSimilarityScan = uss
|
||||
urlBuilder.isOnlySearchCovers = osc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (binding.normalSearchModeHelp === v) {
|
||||
BaseDialogBuilder(context)
|
||||
.setMessage(R.string.search_tip)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
post { setSearchMode(tab.position) }
|
||||
}
|
||||
|
||||
private fun setSearchMode(@SearchMode mode: Int) {
|
||||
val oldItemCount = mAdapter.itemCount
|
||||
mSearchMode = mode
|
||||
val newItemCount = mAdapter.itemCount
|
||||
mAdapter.notifyItemRangeRemoved(0, oldItemCount - 1)
|
||||
mAdapter.notifyItemRangeInserted(0, newItemCount - 1)
|
||||
if (mHelper != null) {
|
||||
mHelper!!.onChangeSearchMode()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||
|
||||
@IntDef(SEARCH_MODE_NORMAL, SEARCH_MODE_IMAGE)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
private annotation class SearchMode
|
||||
interface Helper {
|
||||
fun onChangeSearchMode()
|
||||
}
|
||||
|
||||
internal class SearchLayoutManager(context: Context?) : LinearLayoutManager(context!!) {
|
||||
override fun onLayoutChildren(recycler: Recycler, state: State) {
|
||||
try {
|
||||
super.onLayoutChildren(recycler, state)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SimpleHolder(itemView: View) : ViewHolder(itemView)
|
||||
|
||||
private inner class SearchAdapter : Adapter<SimpleHolder>() {
|
||||
override fun getItemCount(): Int {
|
||||
var count = SEARCH_ITEM_COUNT_ARRAY[mSearchMode]
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL && !mEnableAdvance) {
|
||||
count--
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
var type = SEARCH_ITEM_TYPE[mSearchMode][position]
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL && position == 1 && !mEnableAdvance) {
|
||||
type = ITEM_TYPE_ACTION
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleHolder {
|
||||
val view: View
|
||||
if (viewType == ITEM_TYPE_ACTION) {
|
||||
mActionView.removeFromParent()
|
||||
view = mActionView
|
||||
} else {
|
||||
view = mInflater.inflate(R.layout.search_category, parent, false)
|
||||
SearchCategoryBinding.bind(view).run {
|
||||
when (viewType) {
|
||||
ITEM_TYPE_NORMAL -> {
|
||||
categoryTitle.setText(R.string.search_normal)
|
||||
binding.root.removeFromParent()
|
||||
categoryContent.addView(binding.root)
|
||||
}
|
||||
|
||||
ITEM_TYPE_NORMAL_ADVANCE -> {
|
||||
categoryTitle.setText(R.string.search_advance)
|
||||
advanceBinding.root.removeFromParent()
|
||||
categoryContent.addView(advanceBinding.root)
|
||||
}
|
||||
|
||||
ITEM_TYPE_IMAGE -> {
|
||||
categoryTitle.setText(R.string.search_image)
|
||||
mImageView.removeFromParent()
|
||||
categoryContent.addView(mImageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return SimpleHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SimpleHolder, position: Int) {
|
||||
if (holder.itemViewType == ITEM_TYPE_ACTION) {
|
||||
mAction.selectTab(mAction.getTabAt(mSearchMode))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
var type = SEARCH_ITEM_TYPE[mSearchMode][position]
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL && position == 1 && !mEnableAdvance) {
|
||||
type = ITEM_TYPE_ACTION
|
||||
}
|
||||
return type.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SEARCH_MODE_NORMAL = 0
|
||||
const val SEARCH_MODE_IMAGE = 1
|
||||
const val SEARCH_CATEGORY_PREF = "search_pref"
|
||||
private const val ITEM_TYPE_NORMAL = 0
|
||||
private const val ITEM_TYPE_NORMAL_ADVANCE = 1
|
||||
private const val ITEM_TYPE_IMAGE = 2
|
||||
private const val ITEM_TYPE_ACTION = 3
|
||||
private val SEARCH_ITEM_COUNT_ARRAY = intArrayOf(
|
||||
3,
|
||||
2,
|
||||
)
|
||||
private val mCategoryTable: ArrayList<Pair<Int, Int>> = arrayListOf(
|
||||
Pair(EhUtils.DOUJINSHI, R.string.doujinshi),
|
||||
Pair(EhUtils.MANGA, R.string.manga),
|
||||
Pair(EhUtils.ARTIST_CG, R.string.artist_cg),
|
||||
Pair(EhUtils.GAME_CG, R.string.game_cg),
|
||||
Pair(EhUtils.WESTERN, R.string.western),
|
||||
Pair(EhUtils.NON_H, R.string.non_h),
|
||||
Pair(EhUtils.IMAGE_SET, R.string.image_set),
|
||||
Pair(EhUtils.COSPLAY, R.string.cosplay),
|
||||
Pair(EhUtils.ASIAN_PORN, R.string.asian_porn),
|
||||
Pair(EhUtils.MISC, R.string.misc),
|
||||
)
|
||||
private val SEARCH_ITEM_TYPE = arrayOf(
|
||||
intArrayOf(ITEM_TYPE_NORMAL, ITEM_TYPE_NORMAL_ADVANCE, ITEM_TYPE_ACTION),
|
||||
intArrayOf(
|
||||
ITEM_TYPE_IMAGE,
|
||||
ITEM_TYPE_ACTION,
|
||||
),
|
||||
)
|
||||
fun setHelper(helper: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
class IdentifiedChip @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : Chip(
|
||||
context,
|
||||
attrs,
|
||||
) {
|
||||
var idt = 0
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
/**
|
||||
* not scrollable
|
||||
*
|
||||
* @author Hippo
|
||||
*/
|
||||
open class SimpleGridLayout : ViewGroup {
|
||||
private var mColumnCount = 0
|
||||
private var mItemMargin = 0
|
||||
private var mRowHeights: IntArray? = null
|
||||
private var mItemWidth = 0
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
init(context, null)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
||||
init(context, attrs)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyle,
|
||||
) {
|
||||
init(context, attrs)
|
||||
}
|
||||
|
||||
private fun init(context: Context, attrs: AttributeSet?) {
|
||||
val a = context.obtainStyledAttributes(attrs, R.styleable.SimpleGridLayout)
|
||||
mColumnCount = a.getInteger(R.styleable.SimpleGridLayout_columnCount, DEFAULT_COLUMN_COUNT)
|
||||
mItemMargin = a.getDimensionPixelOffset(R.styleable.SimpleGridLayout_itemMargin, 0)
|
||||
a.recycle()
|
||||
}
|
||||
|
||||
fun setItemMargin(itemMargin: Int) {
|
||||
if (mItemMargin != itemMargin) {
|
||||
mItemMargin = itemMargin
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
fun setColumnCount(columnCount: Int) {
|
||||
check(columnCount > 0) { "Column count can't be $columnCount" }
|
||||
if (mColumnCount != columnCount) {
|
||||
mColumnCount = columnCount
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val maxRowCount = (childCount + mColumnCount - 1) / mColumnCount
|
||||
if (mRowHeights == null || mRowHeights!!.size != maxRowCount) {
|
||||
mRowHeights = IntArray(maxRowCount)
|
||||
}
|
||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
||||
var maxWidth = MeasureSpec.getSize(widthMeasureSpec)
|
||||
var maxHeight = MeasureSpec.getSize(heightMeasureSpec)
|
||||
if (widthMode == MeasureSpec.UNSPECIFIED) {
|
||||
maxWidth = 300
|
||||
}
|
||||
if (heightMode == MeasureSpec.UNSPECIFIED) {
|
||||
maxHeight = Int.MAX_VALUE and (0x3 shl 30).inv()
|
||||
}
|
||||
|
||||
// Get item width MeasureSpec
|
||||
mItemWidth =
|
||||
((maxWidth - paddingLeft - paddingRight - (mColumnCount - 1) * mItemMargin) / mColumnCount)
|
||||
.coerceAtLeast(1)
|
||||
val itemWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mItemWidth, MeasureSpec.EXACTLY)
|
||||
val itemHeightMeasureSpec = MeasureSpec.UNSPECIFIED
|
||||
val measuredWidth = maxWidth
|
||||
var measuredHeight = 0
|
||||
var rowHeight = 0
|
||||
var row = 0
|
||||
val count = childCount
|
||||
var index = 0
|
||||
var indexInRow = 0
|
||||
while (index < count) {
|
||||
val child = getChildAt(index)
|
||||
if (child.visibility == GONE) {
|
||||
indexInRow--
|
||||
index++
|
||||
indexInRow++
|
||||
continue
|
||||
}
|
||||
child.measure(itemWidthMeasureSpec, itemHeightMeasureSpec)
|
||||
if (indexInRow == mColumnCount) {
|
||||
// New row
|
||||
indexInRow = 0
|
||||
rowHeight = 0
|
||||
row++
|
||||
}
|
||||
rowHeight = rowHeight.coerceAtLeast(child.measuredHeight)
|
||||
if (indexInRow == mColumnCount - 1 || index == count - 1) {
|
||||
mRowHeights!![row] = rowHeight
|
||||
measuredHeight += rowHeight + mItemMargin
|
||||
}
|
||||
index++
|
||||
indexInRow++
|
||||
}
|
||||
measuredHeight -= mItemMargin
|
||||
measuredHeight = (measuredHeight + paddingTop + paddingBottom).coerceIn(0, maxHeight)
|
||||
setMeasuredDimension(measuredWidth, measuredHeight)
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||
val itemWidth = mItemWidth
|
||||
val itemMargin = mItemMargin
|
||||
val paddingLeft = paddingLeft
|
||||
var left = paddingLeft
|
||||
var top = paddingTop
|
||||
var row = 0
|
||||
val count = childCount
|
||||
var index = 0
|
||||
var indexInRow = 0
|
||||
while (index < count) {
|
||||
val child = getChildAt(index)
|
||||
if (child.visibility == GONE) {
|
||||
indexInRow--
|
||||
index++
|
||||
indexInRow++
|
||||
continue
|
||||
}
|
||||
if (indexInRow == mColumnCount) {
|
||||
// New row
|
||||
left = paddingLeft
|
||||
top += mRowHeights!![row] + itemMargin
|
||||
indexInRow = 0
|
||||
row++
|
||||
}
|
||||
child.layout(left, top, left + child.measuredWidth, top + child.measuredHeight)
|
||||
left += itemWidth + itemMargin
|
||||
index++
|
||||
indexInRow++
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_COLUMN_COUNT = 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.hippo.ehviewer.ui.main
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
@Composable
|
||||
fun ImageSearch(
|
||||
imagePath: String,
|
||||
onSelectImage: () -> Unit,
|
||||
uss: Boolean,
|
||||
onUssChecked: (Boolean) -> Unit,
|
||||
osc: Boolean,
|
||||
onOscChecked: (Boolean) -> Unit,
|
||||
) = Column(modifier = Modifier.fillMaxWidth()) {
|
||||
AnimatedVisibility(visible = imagePath.isNotBlank(), modifier = Modifier.align(Alignment.CenterHorizontally)) {
|
||||
Card {
|
||||
val maxSize = dimensionResource(id = R.dimen.image_search_max_size)
|
||||
AsyncImage(
|
||||
model = imagePath,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.sizeIn(maxWidth = maxSize, maxHeight = maxSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = onSelectImage,
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.select_image))
|
||||
}
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
Row {
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
Checkbox(checked = uss, onCheckedChange = onUssChecked)
|
||||
Text(text = stringResource(id = R.string.search_uss), modifier = Modifier.align(Alignment.CenterVertically))
|
||||
}
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
Checkbox(checked = osc, onCheckedChange = onOscChecked)
|
||||
Text(text = stringResource(id = R.string.search_osc), modifier = Modifier.align(Alignment.CenterVertically))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.hippo.ehviewer.ui.main
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Help
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
|
||||
private val categoryTable = arrayOf(
|
||||
EhUtils.DOUJINSHI to R.string.doujinshi,
|
||||
EhUtils.MANGA to R.string.manga,
|
||||
EhUtils.ARTIST_CG to R.string.artist_cg,
|
||||
EhUtils.GAME_CG to R.string.game_cg,
|
||||
EhUtils.WESTERN to R.string.western,
|
||||
EhUtils.NON_H to R.string.non_h,
|
||||
EhUtils.IMAGE_SET to R.string.image_set,
|
||||
EhUtils.COSPLAY to R.string.cosplay,
|
||||
EhUtils.ASIAN_PORN to R.string.asian_porn,
|
||||
EhUtils.MISC to R.string.misc,
|
||||
)
|
||||
|
||||
private val modeTable = arrayOf(
|
||||
1 to R.string.search_normal_search,
|
||||
2 to R.string.search_subscription_search,
|
||||
3 to R.string.search_specify_uploader,
|
||||
4 to R.string.search_specify_tag,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun NormalSearch(
|
||||
category: Int,
|
||||
onCategoryChanged: (Int) -> Unit,
|
||||
searchMode: Int,
|
||||
onSearchModeChanged: (Int) -> Unit,
|
||||
isAdvanced: Boolean,
|
||||
onAdvancedChanged: (Boolean) -> Unit,
|
||||
showInfo: () -> Unit,
|
||||
) {
|
||||
FlowRow(modifier = Modifier.wrapContentHeight(align = Alignment.Top)) {
|
||||
categoryTable.forEach {
|
||||
val selected = category and it.first != 0
|
||||
FilterChip(
|
||||
selected = selected,
|
||||
onClick = { onCategoryChanged(if (selected) category xor it.first else category or it.first) },
|
||||
label = { Text(text = stringResource(id = it.second)) },
|
||||
leadingIcon = { if (selected) Icon(imageVector = Icons.Default.Check, contentDescription = null) },
|
||||
modifier = Modifier.padding(horizontal = 4.dp).align(alignment = Alignment.CenterVertically),
|
||||
)
|
||||
}
|
||||
}
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
|
||||
FlowRow {
|
||||
modeTable.forEach {
|
||||
Row(
|
||||
modifier = Modifier.weight(1F).width(112.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
selected = (it.first == searchMode),
|
||||
onClick = { onSearchModeChanged(it.first) },
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = it.second),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.Start) {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
IconButton(onClick = showInfo) {
|
||||
Icon(imageVector = Icons.Default.Help, contentDescription = null)
|
||||
}
|
||||
}
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
Checkbox(checked = isAdvanced, onCheckedChange = onAdvancedChanged)
|
||||
Text(text = stringResource(id = R.string.search_enable_advance), modifier = Modifier.align(Alignment.CenterVertically))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,6 @@ import com.hippo.ehviewer.ui.legacy.FabLayout.OnClickFabListener
|
||||
import com.hippo.ehviewer.ui.legacy.FabLayout.OnExpandListener
|
||||
import com.hippo.ehviewer.ui.legacy.FastScroller.OnDragHandlerListener
|
||||
import com.hippo.ehviewer.ui.legacy.GalleryInfoContentHelper
|
||||
import com.hippo.ehviewer.ui.legacy.SearchLayout
|
||||
import com.hippo.ehviewer.ui.legacy.ViewTransition
|
||||
import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper
|
||||
import com.hippo.ehviewer.ui.setMD3Content
|
||||
@@ -109,7 +108,7 @@ import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||
|
||||
class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.Helper, OnClickFabListener, OnExpandListener {
|
||||
class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListener, OnExpandListener {
|
||||
private val mCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
when (mState) {
|
||||
@@ -309,7 +308,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
binding.fabLayout.setSecondaryFabVisibilityAt(2, !isPopular)
|
||||
|
||||
// Update normal search mode and category
|
||||
binding.searchLayout.setNormalSearchMode(if (mode == MODE_SUBSCRIPTION) R.id.search_subscription_search else R.id.search_normal_search)
|
||||
binding.searchLayout.setSearchMyTags(mode == MODE_SUBSCRIPTION)
|
||||
if (category != EhUtils.NONE) {
|
||||
binding.searchLayout.setCategory(category)
|
||||
}
|
||||
@@ -378,7 +377,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
}
|
||||
setSearchBarHint()
|
||||
setSearchBarSuggestionProvider()
|
||||
binding.searchLayout.setHelper(this)
|
||||
binding.searchLayout.setHelper { showSearchBar() }
|
||||
binding.searchLayout.run {
|
||||
setPadding(
|
||||
paddingLeft,
|
||||
@@ -831,13 +830,11 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
|
||||
STATE_SEARCH -> {
|
||||
mViewTransition!!.showView(1, animation)
|
||||
binding.searchLayout.scrollSearchContainerToTop()
|
||||
selectSearchFab(animation)
|
||||
}
|
||||
|
||||
STATE_SEARCH_SHOW_LIST -> {
|
||||
mViewTransition!!.showView(1, animation)
|
||||
binding.searchLayout.scrollSearchContainerToTop()
|
||||
selectSearchFab(animation)
|
||||
}
|
||||
}
|
||||
@@ -847,15 +844,9 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
selectActionFab(animation)
|
||||
}
|
||||
|
||||
STATE_SEARCH -> {
|
||||
mViewTransition!!.showView(1, animation)
|
||||
binding.searchLayout.scrollSearchContainerToTop()
|
||||
}
|
||||
STATE_SEARCH -> mViewTransition!!.showView(1, animation)
|
||||
|
||||
STATE_SEARCH_SHOW_LIST -> {
|
||||
mViewTransition!!.showView(1, animation)
|
||||
binding.searchLayout.scrollSearchContainerToTop()
|
||||
}
|
||||
STATE_SEARCH_SHOW_LIST -> mViewTransition!!.showView(1, animation)
|
||||
}
|
||||
|
||||
STATE_SEARCH, STATE_SEARCH_SHOW_LIST -> if (state == STATE_NORMAL) {
|
||||
@@ -926,10 +917,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeSearchMode() {
|
||||
showSearchBar()
|
||||
}
|
||||
|
||||
private fun onGetGalleryListSuccess(result: GalleryListParser.Result, taskId: Int) {
|
||||
if (Settings.preloadThumbAggressively) {
|
||||
lifecycleScope.launchIO {
|
||||
|
||||
Reference in new issue
Block a user