/* * 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 . */ package com.hippo.ehviewer.widget import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.net.Uri import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.CheckBox import android.widget.CompoundButton import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import androidx.annotation.IntDef import androidx.core.content.edit import androidx.core.view.forEach import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import com.hippo.app.BaseDialogBuilder import com.hippo.easyrecyclerview.EasyRecyclerView import com.hippo.easyrecyclerview.MarginItemDecoration import com.hippo.easyrecyclerview.SimpleHolder import com.hippo.ehviewer.R import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.data.ListUrlBuilder import com.hippo.ehviewer.client.exception.EhException import com.hippo.widget.RadioGridGroup import com.hippo.yorozuya.NumberUtils import com.hippo.yorozuya.ViewUtils @SuppressLint("InflateParams") class SearchLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, ) : EasyRecyclerView(context, attrs), CompoundButton.OnCheckedChangeListener, View.OnClickListener, ImageSearchLayout.Helper, OnTabSelectedListener { @SearchMode private var mSearchMode = SEARCH_MODE_NORMAL private var mEnableAdvance = false private var mCategoryGroup: ChipGroup private var mNormalView: View private var mNormalSearchMode: RadioGridGroup private var mNormalSearchModeHelp: ImageView private var mEnableAdvanceSwitch: CheckBox private var mAdvanceView: View private var mTableAdvanceSearch: AdvanceSearchTable private var mImageView: ImageSearchLayout private var mActionView: View private var mAction: TabLayout private var mAdapter: SearchAdapter private var mHelper: Helper? = null private val mSharePref: SharedPreferences private val mInflater: LayoutInflater init { mSharePref = PreferenceManager.getDefaultSharedPreferences(context) 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 mNormalView = mInflater.inflate(R.layout.search_normal, null) val mCategoryStored = mSharePref.getInt(SEARCH_CATEGORY_PREF, EhUtils.ALL_CATEGORY) mCategoryGroup = mNormalView.findViewById(R.id.search_category_chipgroup) 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 = NumberUtils.int2boolean(mPair.first and mCategoryStored) mChip.setOnLongClickListener { if (mChip.isChecked) { mCategoryGroup.forEach { (it as IdentifiedChip).isChecked = true } mChip.isChecked = false } else { mCategoryGroup.clearCheck() mChip.isChecked = true } true } mCategoryGroup.addView(mChip) } mCategoryGroup.setOnCheckedStateChangeListener { group, checkedIds -> var mCategory = 0 for (index in checkedIds) { mCategory = mCategory or group.findViewById(index).idt } mSharePref.edit { putInt(SEARCH_CATEGORY_PREF, mCategory) } } mNormalSearchMode = mNormalView.findViewById(R.id.normal_search_mode) mNormalSearchModeHelp = mNormalView.findViewById(R.id.normal_search_mode_help) mEnableAdvanceSwitch = mNormalView.findViewById(R.id.search_enable_advance) mNormalSearchModeHelp.setOnClickListener(this) mEnableAdvanceSwitch.setOnCheckedChangeListener(this) // Create advance view mAdvanceView = mInflater.inflate(R.layout.search_advance, null) mTableAdvanceSearch = mAdvanceView.findViewById(R.id.search_advance_search_table) // Create image view mImageView = mInflater.inflate(R.layout.search_image, null) as ImageSearchLayout mImageView.setHelper(this) // 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 setImageUri(imageUri: Uri?) { mImageView.setImageUri(imageUri) } fun setNormalSearchMode(id: Int) { mNormalSearchMode.check(id) } fun setCategory(category: Int) { mCategoryGroup.forEach { (it as IdentifiedChip).apply { isChecked = idt and category != 0 } } } override fun onSelectImage() { if (mHelper != null) { mHelper!!.onSelectImage() } } override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { if (buttonView === mEnableAdvanceSwitch) { post { mEnableAdvance = isChecked if (mSearchMode == SEARCH_MODE_NORMAL) { if (mEnableAdvance) { mAdapter.notifyItemInserted(1) } else { mAdapter.notifyItemRemoved(1) } if (mHelper != null) { mHelper!!.onChangeSearchMode() } } } } } @SuppressLint("NonConstantResourceId") @Throws(EhException::class) fun formatListUrlBuilder(urlBuilder: ListUrlBuilder, query: String?) { urlBuilder.reset() when (mSearchMode) { SEARCH_MODE_NORMAL -> { when (mNormalSearchMode.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 } } urlBuilder.keyword = query urlBuilder.category = PreferenceManager.getDefaultSharedPreferences(context).getInt( SEARCH_CATEGORY_PREF, 0, ) if (mEnableAdvance) { urlBuilder.advanceSearch = mTableAdvanceSearch.advanceSearch urlBuilder.minRating = mTableAdvanceSearch.minRating val pageFrom = mTableAdvanceSearch.pageFrom val pageTo = mTableAdvanceSearch.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 } } SEARCH_MODE_IMAGE -> { urlBuilder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH mImageView.formatListUrlBuilder(urlBuilder) } } } override fun onClick(v: View) { if (mNormalSearchModeHelp === 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() fun onSelectImage() } 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 SearchAdapter : Adapter() { 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) { ViewUtils.removeFromParent(mActionView) view = mActionView } else { view = mInflater.inflate(R.layout.search_category, parent, false) val title = view.findViewById(R.id.category_title) val content = view.findViewById(R.id.category_content) when (viewType) { ITEM_TYPE_NORMAL -> { title.setText(R.string.search_normal) ViewUtils.removeFromParent(mNormalView) content.addView(mNormalView) } ITEM_TYPE_NORMAL_ADVANCE -> { title.setText(R.string.search_advance) ViewUtils.removeFromParent(mAdvanceView) content.addView(mAdvanceView) } ITEM_TYPE_IMAGE -> { title.setText(R.string.search_image) ViewUtils.removeFromParent(mImageView) content.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> = 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, ), ) } } class IdentifiedChip @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, ) : Chip( context, attrs, ) { var idt = 0 }