Implemented Source Selector

This commit is contained in:
tom5079
2020-11-29 13:37:31 +09:00
parent 24aedfc400
commit 3feae80359
17 changed files with 319 additions and 83 deletions

View File

@@ -124,7 +124,7 @@ dependencies {
implementation "xyz.quaver:libpupil:1.9.7"
implementation "xyz.quaver:documentfilex:0.4-alpha02"
implementation "xyz.quaver:floatingsearchview:1.0.7"
implementation "xyz.quaver:floatingsearchview:1.0.9"
testImplementation "junit:junit:4.13.1"
androidTestImplementation "androidx.test.ext:junit:1.1.2"

View File

@@ -42,6 +42,7 @@ import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import xyz.quaver.io.FileX
import xyz.quaver.pupil.sources.initSources
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.*
import xyz.quaver.setClient
@@ -89,6 +90,8 @@ class Pupil : Application() {
firebaseAnalytics = Firebase.analytics
FirebaseCrashlytics.getInstance().setUserId(userID)
initSources(this)
val proxyInfo = getProxyInfo()
clientBuilder = OkHttpClient.Builder()

View File

@@ -0,0 +1,59 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.graphics.drawable.DrawableCompat
import androidx.recyclerview.widget.RecyclerView
import xyz.quaver.pupil.databinding.SourceSelectDialogItemBinding
import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.sourceIcons
class SourceAdapter(private val sources: List<Source<*>>) : RecyclerView.Adapter<SourceAdapter.ViewHolder>() {
var onSourceSelectedListener: ((Source<*>) -> Unit)? = null
inner class ViewHolder(private val binding: SourceSelectDialogItemBinding) : RecyclerView.ViewHolder(binding.root) {
lateinit var source: Source<*>
init {
binding.go.setOnClickListener {
onSourceSelectedListener?.invoke(source)
}
}
fun bind(source: Source<*>) {
this.source = source
binding.icon.setImageDrawable(sourceIcons[source.name])
binding.name.text = source.name
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(SourceSelectDialogItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(sources[position])
}
override fun getItemCount(): Int = sources.size
}

View File

@@ -18,9 +18,13 @@
package xyz.quaver.pupil.sources
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import kotlinx.coroutines.channels.Channel
import xyz.quaver.pupil.R
import kotlin.reflect.KClass
import xyz.quaver.pupil.sources.hitomi.Hitomi
import xyz.quaver.pupil.sources.hitomi.Hiyobi
data class SearchResult(
val id: String,
@@ -49,8 +53,28 @@ data class SearchResult(
}
}
interface Source<Query_SortMode: Enum<*>> {
val querySortModeClass: KClass<Query_SortMode>?
enum class DefaultSortMode {
DEFAULT
}
interface Source<Query_SortMode : Enum<Query_SortMode>> {
val name: String
val iconResID: Int
val availableSortMode: Array<Query_SortMode>
suspend fun query(query: String, range: IntRange, sortMode: Query_SortMode? = null) : Pair<Channel<SearchResult>, Int>
suspend fun query(query: String, range: IntRange, sortMode: Enum<*>) : Pair<Channel<SearchResult>, Int>
}
val sources = mutableMapOf<String, Source<*>>()
val sourceIcons = mutableMapOf<String, Drawable?>()
@Suppress("UNCHECKED_CAST")
fun initSources(context: Context) {
// Add Default Sources
listOf(
Hitomi(),
Hiyobi()
).forEach {
sources[it.name] = it
sourceIcons[it.name] = ContextCompat.getDrawable(context, it.iconResID)
}
}

View File

@@ -21,6 +21,7 @@ package xyz.quaver.pupil.sources.hitomi
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import xyz.quaver.hitomi.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.sources.SearchResult
import xyz.quaver.pupil.sources.SearchResult.ExtraType
import xyz.quaver.pupil.sources.Source
@@ -30,18 +31,20 @@ import kotlin.math.min
class Hitomi : Source<Hitomi.SortMode> {
override val querySortModeClass = SortMode::class
enum class SortMode {
NEWEST,
POPULAR
}
override val name: String = "hitomi.la"
override val iconResID: Int = R.drawable.hitomi
override val availableSortMode: Array<SortMode> = SortMode.values()
var cachedQuery: String? = null
var cachedSortMode: SortMode? = null
val cache = mutableListOf<Int>()
override suspend fun query(query: String, range: IntRange, sortMode: SortMode?): Pair<Channel<SearchResult>, Int> {
override suspend fun query(query: String, range: IntRange, sortMode: Enum<*>): Pair<Channel<SearchResult>, Int> {
if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
cachedQuery = null
cache.clear()

View File

@@ -23,14 +23,19 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import xyz.quaver.hiyobi.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.sources.DefaultSortMode
import xyz.quaver.pupil.sources.SearchResult
import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.util.wordCapitalize
class Hiyobi : Source<Enum<*>> {
override val querySortModeClass = null
class Hiyobi : Source<DefaultSortMode> {
override suspend fun query(query: String, range: IntRange, sortMode: Enum<*>?): Pair<Channel<SearchResult>, Int> {
override val name: String = "hiyobi.me"
override val iconResID: Int = R.drawable.ic_hiyobi
override val availableSortMode: Array<DefaultSortMode> = DefaultSortMode.values()
override suspend fun query(query: String, range: IntRange, sortMode: Enum<*>): Pair<Channel<SearchResult>, Int> {
val channel = Channel<SearchResult>()
val (results, total) = if (query.isEmpty())
@@ -59,7 +64,7 @@ class Hiyobi : Source<Enum<*>> {
mapOf(
SearchResult.ExtraType.CHARACTER to { galleryBlock.characters.joinToString { it.value.wordCapitalize() } },
SearchResult.ExtraType.SERIES to { galleryBlock.parodys.joinToString { it.value.wordCapitalize() } },
SearchResult.ExtraType.TYPE to { galleryBlock.type.name.wordCapitalize() },
SearchResult.ExtraType.TYPE to { galleryBlock.type.name.replace('_', ' ').wordCapitalize() },
SearchResult.ExtraType.PAGECOUNT to { getGalleryInfo(galleryBlock.id).files.size.toString() }
),
galleryBlock.tags.map { it.value }

View File

@@ -25,23 +25,27 @@ import android.os.Bundle
import android.text.InputType
import android.text.util.Linkify
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.animation.DecelerateInterpolator
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.cardview.widget.CardView
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.children
import androidx.core.widget.ImageViewCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import xyz.quaver.floatingsearchview.FloatingSearchView
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.view.MenuView
import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.hitomi.getSuggestionsForQuery
import xyz.quaver.pupil.*
@@ -50,18 +54,16 @@ import xyz.quaver.pupil.databinding.MainActivityBinding
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.sources.SearchResult
import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.hitomi.Hitomi
import xyz.quaver.pupil.sources.hitomi.Hiyobi
import xyz.quaver.pupil.sources.sourceIcons
import xyz.quaver.pupil.sources.sources
import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
import xyz.quaver.pupil.ui.view.ProgressCardView
import xyz.quaver.pupil.ui.view.SwipePageTurnView
import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.checkUpdate
import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.restore
import java.util.regex.Pattern
import kotlin.math.*
import kotlin.random.Random
@@ -82,9 +84,8 @@ class MainActivity :
}
private var queryStack = mutableListOf<String>()
@Suppress("UNCHECKED_CAST")
private var source: Source<Enum<*>> = Hiyobi() as Source<Enum<*>>
private var sortMode = Hitomi.SortMode.NEWEST
private lateinit var source: Source<*>
private lateinit var sortMode: Enum<*>
private var searchJob: Deferred<Pair<Channel<SearchResult>, Int>>? = null
private var totalItems = 0
@@ -97,6 +98,8 @@ class MainActivity :
binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
setSource(sources.values.first())
if (intent.action == Intent.ACTION_VIEW) {
intent.dataString?.let { url ->
restore(url,
@@ -167,6 +170,34 @@ class MainActivity :
}
}
private fun setSource(source: Source<*>) {
this.source = source
sortMode = source.availableSortMode.first()
query = ""
currentPage = 1
with (binding.contents.searchview.binding.querySection.menuView) {
post {
menuItems.findMenu(R.id.sort).subMenu.apply {
clear()
source.availableSortMode.forEach {
add(R.id.sort_mode_group_id, it.ordinal, Menu.NONE, it.name)
}
setGroupCheckable(R.id.sort_mode_group_id, true, true)
children.first().isChecked = true
}
with (getChildAt(1) as ImageView) {
ImageViewCompat.setImageTintList(this, null)
setImageDrawable(sourceIcons[source.name])
}
}
}
}
private fun initView() {
binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@@ -175,7 +206,7 @@ class MainActivity :
min(
max(
binding.contents.searchview.translationY - dy,
-binding.contents.searchview.findViewById<CardView>(R.id.search_query_section).height.toFloat()
-binding.contents.searchview.binding.querySection.root.height.toFloat()
), 0F)
if (dy > 0)
@@ -224,6 +255,7 @@ class MainActivity :
with(binding.contents.randomFab) {
setImageResource(R.drawable.shuffle_variant)
setOnClickListener {
setImageDrawable(CircularProgressDrawable(context))
if (totalItems > 0)
CoroutineScope(Dispatchers.IO).launch {
val random = Random.Default.nextInt(totalItems)
@@ -236,6 +268,7 @@ class MainActivity :
).first.receive()
launch(Dispatchers.Main) {
setImageResource(R.drawable.shuffle_variant)
GalleryDialog(this@MainActivity, randomResult.id).apply {
onChipClickedHandler.add {
query = it.toQuery()
@@ -394,7 +427,7 @@ class MainActivity :
with(binding.contents.searchview) {
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
override fun onMenuOpened() {
(binding.contents.recyclerview.adapter as SearchResultsAdapter).closeAllItems()
(this@MainActivity.binding.contents.recyclerview.adapter as SearchResultsAdapter).closeAllItems()
}
override fun onMenuClosed() {
@@ -402,14 +435,6 @@ class MainActivity :
}
}
post {
findViewById<MenuView>(R.id.menu_view).menuItems.firstOrNull {
(it as MenuItem).itemId == R.id.main_menu_thin
}?.let {
(it as MenuItem).isChecked = Preferences["thin"]
}
}
onHistoryDeleteClickedListener = {
searchHistory.remove(it)
swapSuggestions(defaultSuggestions)
@@ -473,37 +498,32 @@ class MainActivity :
}
}
attachNavigationDrawerToMenuButton(binding.drawer)
attachNavigationDrawerToMenuButton(this@MainActivity.binding.drawer)
}
}
fun onActionMenuItemSelected(item: MenuItem?) {
when(item?.itemId) {
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
R.id.main_menu_thin -> {
// TODO
private fun onActionMenuItemSelected(item: MenuItem?) {
if (item?.groupId == R.id.sort_mode_group_id) {
currentPage = 1
sortMode = source.availableSortMode.let { availableSortMode ->
availableSortMode.getOrElse(item.itemId) { availableSortMode.first() }
}
R.id.main_menu_sort_newest -> {
sortMode = Hitomi.SortMode.NEWEST
item.isChecked = true
runOnUiThread {
currentPage = 1
query()
}
}
R.id.main_menu_sort_popular -> {
sortMode = Hitomi.SortMode.POPULAR
item.isChecked = true
runOnUiThread {
currentPage = 1
query()
}
}
query()
}
else
when(item?.itemId) {
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
R.id.source -> SourceSelectDialog().apply {
onSourceSelectedListener = {
setSource(it)
query()
dismiss()
}
}.show(supportFragmentManager, null)
}
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
@@ -539,6 +559,7 @@ class MainActivity :
private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch {
searchResults.clear()
(binding.contents.recyclerview.adapter as RecyclerSwipeAdapter).mItemManger.closeAllItems()
binding.contents.recyclerview.adapter?.notifyDataSetChanged()
binding.contents.noresult.visibility = View.INVISIBLE

View File

@@ -64,12 +64,7 @@ class GalleryDialog(context: Context, private val galleryID: String) : AlertDial
binding = GalleryDialogBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.attributes.apply {
this ?: return@apply
width = LayoutParams.MATCH_PARENT
height = LayoutParams.MATCH_PARENT
}
window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
with(binding.fab) {
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right))

View File

@@ -0,0 +1,50 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.dialog
import android.app.Dialog
import android.os.Bundle
import android.view.ViewGroup.LayoutParams
import android.view.Window
import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import xyz.quaver.pupil.adapters.SourceAdapter
import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.sources
class SourceSelectDialog : DialogFragment() {
var onSourceSelectedListener: ((Source<*>) -> Unit)? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return Dialog(requireContext()).apply {
window?.requestFeature(Window.FEATURE_NO_TITLE)
window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
setContentView(RecyclerView(context).apply {
layoutManager = LinearLayoutManager(context)
adapter = SourceAdapter(sources.values.toList()).apply {
onSourceSelectedListener = this@SourceSelectDialog.onSourceSelectedListener
}
})
}
}
}

View File

@@ -407,6 +407,16 @@ public class SwipePageTurnView extends ViewGroup implements NestedScrollingChild
stopNestedScroll();
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return dispatchNestedFling(velocityX, velocityY, consumed);
}
// NestedScrollingChild
@Override

View File

@@ -22,6 +22,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import android.view.MenuItem
import androidx.core.content.ContextCompat
import kotlinx.serialization.json.*
import okhttp3.OkHttpClient
@@ -142,4 +143,8 @@ operator fun JsonElement.get(tag: String) =
this.jsonObject[tag]
val JsonElement.content
get() = this.jsonPrimitive.contentOrNull
get() = this.jsonPrimitive.contentOrNull
fun List<MenuItem>.findMenu(itemID: Int): MenuItem {
return first { it.itemId == itemID }
}

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program 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.
~
~ This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="8dp">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/name"
style="@style/TextAppearance.AppCompat.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintLeft_toRightOf="@id/icon"
app:layout_constraintRight_toLeftOf="@id/settings"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageButton
android:id="@+id/settings"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
app:srcCompat="@drawable/ic_settings"
app:tint="?attr/colorControlNormal"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/name"
app:layout_constraintRight_toLeftOf="@id/go"/>
<Button
android:id="@+id/go"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GO!"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -20,19 +20,17 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/source"
android:icon="@drawable/cancel"
android:title="Source"
app:showAsAction="ifRoom" />
<item android:id="@+id/sort"
android:title="@string/main_menu_sort"
android:icon="@drawable/sort_variant"
app:showAsAction="ifRoom">
<menu>
<group android:checkableBehavior="single">
<item android:id="@+id/main_menu_sort_newest"
android:title="@string/main_menu_sort_newest"
android:checked="true"/>
<item android:id="@+id/main_menu_sort_popular"
android:title="@string/main_menu_sort_popular"/>
</group>
</menu>
<menu/>
</item>
<item
@@ -41,9 +39,4 @@
android:title="@string/main_settings"
app:showAsAction="always"/>
<item android:id="@+id/main_menu_thin"
android:title="@string/main_menu_thin"
app:showAsAction="never"
android:checkable="true"/>
</menu>

View File

@@ -114,7 +114,6 @@
<string name="proxy_dialog_error">エラー</string>
<string name="proxy_dialog_addr_hint">サーバーアドレス</string>
<string name="proxy_dialog_server">サーバー</string>
<string name="main_menu_thin">簡単モード</string>
<string name="main_fab_cancel">すべてのダウンロードキャンセル</string>
<string name="channel_update">アップデート</string>
<string name="channel_update_description">アップデートの進行状態を表示</string>

View File

@@ -114,7 +114,6 @@
<string name="proxy_dialog_error">잘못된 값</string>
<string name="proxy_dialog_addr_hint">서버 주소</string>
<string name="proxy_dialog_server">서버</string>
<string name="main_menu_thin">간단히 보기</string>
<string name="main_fab_cancel">다운로드 모두 취소</string>
<string name="channel_update">업데이트</string>
<string name="channel_update_description">업데이트 진행상황 표시</string>

View File

@@ -10,4 +10,6 @@
<item name="notification_download_cancel_action" type="id" />
<item name="notification_import_cancel_action" type="id" />
<item name="sort_mode_group_id" type="id"/>
</resources>

View File

@@ -58,8 +58,6 @@
<string name="main_drawer_group_contact_email">Email me!</string>
<string name="main_drawer_grouop_contact_discord">Discord</string>
<string name="main_menu_thin">Thin Mode</string>
<string name="main_menu_sort">Sort</string>
<string name="main_menu_sort_newest">Newest</string>
<string name="main_menu_sort_popular">Popular</string>