This commit is contained in:
tom5079
2021-12-14 22:19:15 +09:00
parent ddbfd0a201
commit 458530e80c
22 changed files with 449 additions and 380 deletions

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_3a_API_30_x86.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-11-25T00:59:48.587692Z" />
</component>
</project>

1
.idea/misc.xml generated
View File

@@ -25,6 +25,7 @@
<entry key="../../../../layout/compose-model-1631666404391.xml" value="0.36203703703703705" />
<entry key="../../../../layout/compose-model-1631801120026.xml" value="1.922077922077922" />
<entry key="../../../../layout/compose-model-1631838314391.xml" value="2.0" />
<entry key="../../../../layout/compose-model-1639478149655.xml" value="0.33" />
<entry key="../../../../layout/custom_preview.xml" value="0.518974358974359" />
<entry key="app/src/main/res/drawable/avd_star.xml" value="0.2722222222222222" />
<entry key="app/src/main/res/layout/gallery_dialog.xml" value="0.30052083333333335" />

View File

@@ -22,7 +22,8 @@ android {
}
buildTypes {
getByName("debug") {
isDebuggable = true
isDebuggable = false
isMinifyEnabled = true
applicationIdSuffix = ".debug"
versionNameSuffix = "-DEBUG"
@@ -34,6 +35,7 @@ android {
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
applicationIdSuffix = ".beta"
isCrunchPngs = false
@@ -47,7 +49,7 @@ android {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.0.0"
kotlinCompilerExtensionVersion = "1.0.5"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -69,17 +71,16 @@ dependencies {
implementation("androidx.compose.ui:ui-tooling:1.0.5")
implementation("androidx.compose.foundation:foundation:1.0.5")
implementation("androidx.compose.material:material:1.0.5")
implementation("androidx.compose.material:material-icons-core:1.0.5")
implementation("androidx.compose.material:material-icons-extended:1.0.5")
implementation("androidx.compose.runtime:runtime-livedata:1.0.5")
implementation("androidx.compose.material:material-icons-extended:1.0.5")
implementation("androidx.compose.ui:ui-util:1.0.5")
implementation("androidx.activity:activity-compose:1.4.0")
implementation("androidx.navigation:navigation-compose:2.4.0-beta02")
implementation("com.google.accompanist:accompanist-flowlayout:0.16.1")
implementation("com.google.accompanist:accompanist-appcompat-theme:0.16.0")
implementation("com.google.accompanist:accompanist-insets:0.18.0")
implementation("com.google.accompanist:accompanist-insets-ui:0.18.0")
implementation("com.google.accompanist:accompanist-flowlayout:0.20.2")
implementation("com.google.accompanist:accompanist-appcompat-theme:0.20.2")
implementation("com.google.accompanist:accompanist-insets:0.20.2")
implementation("com.google.accompanist:accompanist-insets-ui:0.20.2")
implementation("io.coil-kt:coil-compose:1.3.2")
@@ -135,7 +136,7 @@ dependencies {
implementation("xyz.quaver:subsampledimage:0.0.1-alpha09-SNAPSHOT")
implementation("org.kodein.log:kodein-log:0.11.1")
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
//debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-inline:4.1.0")
@@ -145,5 +146,9 @@ dependencies {
androidTestImplementation("androidx.test:runner:1.4.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.1.0-beta03")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.0.5")
}
task<Exec>("clearAppCache") {
commandLine("adb", "shell", "pm", "clear", "xyz.quaver.pupil.debug")
}

Binary file not shown.

View File

@@ -1,18 +1,20 @@
{
"version": 2,
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "xyz.quaver.pupil",
"variantName": "processReleaseResources",
"applicationId": "xyz.quaver.pupil.beta",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"versionCode": 64,
"versionName": "5.1.7-alpha1",
"attributes": [],
"versionCode": 600,
"versionName": "6.0.0-alpha2",
"outputFile": "app-release.apk"
}
]
],
"elementType": "File"
}

View File

@@ -37,8 +37,10 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.ktx.Firebase
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import okhttp3.Protocol
import org.kodein.di.*
import org.kodein.di.android.x.androidXModule
import org.kodein.log.LoggerFactory
@@ -49,6 +51,7 @@ import xyz.quaver.pupil.sources.sourceModule
import xyz.quaver.pupil.util.*
import java.io.File
import java.util.*
import java.util.concurrent.TimeUnit
class Pupil : Application(), DIAware {
@@ -62,9 +65,21 @@ class Pupil : Application(), DIAware {
bind { singleton {
HttpClient(OkHttp) {
engine {
config {
protocols(listOf(Protocol.HTTP_1_1))
}
}
install(JsonFeature) {
serializer = KotlinxSerializer()
}
install(HttpTimeout) {
requestTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
socketTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
connectTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
}
BrowserUserAgent()
}
} }
}

View File

@@ -61,6 +61,8 @@ import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.hitomi.*
@@ -120,6 +122,8 @@ class Hitomi(app: Application) : Source(), DIAware {
override val di: DI by closestDI(app)
private val logger = newLogger(LoggerFactory.default)
private val database: AppDatabase by instance()
private val bookmarkDao = database.bookmarkDao()
@@ -435,13 +439,13 @@ class Hitomi(app: Application) : Source(), DIAware {
var pageCount by remember { mutableStateOf("-") }
LaunchedEffect(itemInfo) {
launch {
launch(Dispatchers.Default) {
itemInfo.getPageCount()?.run {
pageCount = "${this}P"
}
}
launch {
launch(Dispatchers.Default) {
itemInfo.getGroups()?.run {
group = this
}
@@ -518,7 +522,9 @@ class Hitomi(app: Application) : Source(), DIAware {
)
}
TagGroup(tags = itemInfo.tags)
key(itemInfo.tags) {
TagGroup(tags = itemInfo.tags)
}
}
}

View File

@@ -19,361 +19,169 @@
package xyz.quaver.pupil.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.InputType
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.view.GravityCompat
import androidx.core.view.children
import androidx.core.widget.ImageViewCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.navigation.NavigationView
import androidx.compose.ui.util.fastAny
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import xyz.quaver.floatingsearchview.FloatingSearchView
import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger
import xyz.quaver.pupil.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.MainActivityBinding
import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
import xyz.quaver.pupil.ui.composable.FloatingActionButtonState
import xyz.quaver.pupil.ui.composable.FloatingSearchBar
import xyz.quaver.pupil.ui.composable.MultipleFloatingActionButton
import xyz.quaver.pupil.ui.composable.SubFabItem
import xyz.quaver.pupil.ui.theme.PupilTheme
import xyz.quaver.pupil.ui.view.ProgressCardView
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
import xyz.quaver.pupil.util.*
import kotlin.math.*
class MainActivity :
BaseActivity(),
NavigationView.OnNavigationItemSelectedListener,
DIAware
{
class MainActivity : ComponentActivity(), DIAware {
override val di by closestDI()
private lateinit var binding: MainActivityBinding
private val model: MainViewModel by viewModels()
private var refreshOnResume = true
private val logger = newLogger(LoggerFactory.default)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = MainActivityBinding.inflate(layoutInflater)
binding.contents.composeView.setContent {
val searchResults: List<ItemInfo> by model.searchResults.observeAsState(emptyList())
setContent {
val source: Source? by model.source.observeAsState(null)
val loading: Boolean by model.loading.observeAsState(false)
val listState = rememberLazyListState()
var query by remember { mutableStateOf("") }
LaunchedEffect(listState) {
var isFabExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
val lazyListState = rememberLazyListState()
val searchBarHeight = LocalDensity.current.run { 56.dp.roundToPx() }
var searchBarOffset by remember { mutableStateOf(0) }
LaunchedEffect(lazyListState) {
var lastOffset = 0
val querySectionHeight = binding.contents.searchview.binding.querySection.root.height.toFloat()
snapshotFlow { listState.firstVisibleItemScrollOffset }
snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
.distinctUntilChanged()
.collect { newOffset ->
val dy = newOffset - lastOffset
lastOffset = newOffset
binding.contents.searchview.apply {
translationY = (translationY - dy).coerceIn(-querySectionHeight .. 0f)
}
if (abs(dy) < searchBarHeight)
searchBarOffset = (searchBarOffset-dy).coerceIn(-searchBarHeight, 0)
}
}
Box(Modifier.fillMaxSize()) {
LazyColumn(Modifier.fillMaxSize(), state = listState, contentPadding = PaddingValues(0.dp, 64.dp, 0.dp, 0.dp)) {
item(searchResults) {
searchResults.forEach { itemInfo ->
ProgressCardView(
progress = 0.5f,
onClick = {
startActivity(
Intent(
this@MainActivity,
ReaderActivity::class.java
).apply {
putExtra("source", model.source.value!!.name)
putExtra("id", itemInfo.itemID)
})
PupilTheme {
Scaffold(
floatingActionButton = {
MultipleFloatingActionButton(
listOf(
SubFabItem(
Icons.Default.Block,
stringResource(R.string.main_fab_cancel)
),
SubFabItem(
painterResource(R.drawable.ic_jump),
stringResource(R.string.main_jump_title)
),
SubFabItem(
Icons.Default.Shuffle,
stringResource(R.string.main_fab_random)
),
SubFabItem(
painterResource(R.drawable.numeric),
stringResource(R.string.main_open_gallery_by_id)
),
),
targetState = isFabExpanded,
onStateChanged = {
isFabExpanded = it
}
)
}
) {
Box(Modifier.fillMaxSize()) {
LazyColumn(
Modifier.fillMaxSize(),
state = lazyListState,
contentPadding = PaddingValues(0.dp, 56.dp, 0.dp, 0.dp)
) {
items(model.searchResults, key = { it.itemID }) { itemInfo ->
ProgressCardView(
progress = 0.5f,
onClick = {
startActivity(
Intent(
this@MainActivity,
ReaderActivity::class.java
).apply {
putExtra("source", model.source.value!!.name)
putExtra("id", itemInfo.itemID)
})
}
) {
source?.SearchResult(itemInfo = itemInfo)
}
) {
source?.SearchResult(itemInfo = itemInfo)
}
}
}
}
if (loading)
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
if (loading)
CircularProgressIndicator(Modifier.align(Alignment.Center))
setContentView(binding.root)
if (Preferences["download_folder", ""].isEmpty())
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
checkUpdate(this)
initView()
model.query.observe(this) {
binding.contents.searchview.binding.querySection.searchBarText.run {
if (text?.toString() != it) setText(it, TextView.BufferType.EDITABLE)
}
}
model.availableSortMode.observe(this) {
binding.contents.searchview.post {
binding.contents.searchview.binding.querySection.menuView.menuItems.findMenu(R.id.sort)?.subMenu?.apply {
clear()
it.forEachIndexed { index, sortMode ->
add(R.id.sort_mode_group_id, index, Menu.NONE, sortMode).setOnMenuItemClickListener {
model.setPage(1)
model.sortModeIndex.value = it.itemId
children.forEachIndexed { menuIndex, menuItem ->
menuItem.isChecked = menuIndex == index
FloatingSearchBar(
modifier = Modifier.offset(0.dp, LocalDensity.current.run { searchBarOffset.toDp() }),
query = query,
onQueryChange = { query = it },
actions = {
Icon(
Icons.Default.Sort,
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Icon(
Icons.Default.Settings,
contentDescription = null,
modifier = Modifier.size(24.dp)
)
}
model.query()
true
}
}
setGroupCheckable(R.id.sort_mode_group_id, true, true)
children.first().isChecked = true
}
}
}
model.sourceIcon.observe(this) {
binding.contents.searchview.post {
(binding.contents.searchview.binding.querySection.menuView.getChildAt(1) as ImageView).apply {
ImageViewCompat.setImageTintList(this, null)
setImageResource(it)
}
}
}
model.suggestions.observe(this) { runOnUiThread {
binding.contents.searchview.swapSuggestions(
if (it.isEmpty()) listOf(NoResultSuggestion(getString(R.string.main_no_result))) else it
)
} }
}
override fun onResume() {
super.onResume()
if (refreshOnResume) {
model.query()
refreshOnResume = false
}
}
override fun onBackPressed() {
if (binding.drawer.isDrawerOpen(GravityCompat.START))
binding.drawer.closeDrawer(GravityCompat.START)
else if (!model.onBackPressed())
super.onBackPressed()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> {
if (model.currentPage.value!! > 1) {
runOnUiThread {
model.prevPage()
model.query()
)
}
}
true
}
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (model.currentPage.value!! < model.totalPages.value!!) {
runOnUiThread {
model.nextPage()
model.query()
}
}
true
}
else -> super.onKeyDown(keyCode, event)
}
}
private fun initView() {
//NavigationView
binding.navView.setNavigationItemSelectedListener(this)
with (binding.contents.cancelFab) {
setOnClickListener {
}
}
with (binding.contents.jumpFab) {
setOnClickListener {
val perPage = Preferences["per_page", "25"].toInt()
val editText = EditText(context)
AlertDialog.Builder(context).apply {
setView(editText)
setTitle(R.string.main_jump_title)
setMessage(getString(
R.string.main_jump_message,
model.currentPage.value!!,
ceil(model.totalPages.value!! / perPage.toDouble()).roundToInt()
))
setPositiveButton(android.R.string.ok) { _, _ ->
model.setPage(editText.text.toString().toIntOrNull() ?: return@setPositiveButton)
model.query()
}
}.show()
}
}
with (binding.contents.randomFab) {
setOnClickListener {
setImageDrawable(CircularProgressDrawable(context))
/*
model.random { runOnUiThread {
GalleryDialogFragment(model.source.value!!.name, it.itemID).apply {
onChipClickedHandler.add {
model.setQueryAndSearch(it.toQuery())
dismiss()
}
}.show(supportFragmentManager, "GalleryDialogFragment")
} } */
}
}
with (binding.contents.idFab) {
setOnClickListener {
val editText = EditText(context).apply {
inputType = InputType.TYPE_CLASS_NUMBER
}
AlertDialog.Builder(context).apply {
setView(editText)
setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ ->
val galleryID = editText.text.toString()
/*
GalleryDialogFragment(model.source.value!!.name, galleryID).apply {
onChipClickedHandler.add {
model.setQueryAndSearch(it.toQuery())
dismiss()
}
}.show(supportFragmentManager, "GalleryDialogFragment")*/
}
}.show()
}
}
setupSearchBar()
// TODO: Save recent source
}
private fun setupSearchBar() {
with (binding.contents.searchview) {
onMenuItemClickListener = {
onActionMenuItemSelected(it)
}
onQueryChangeListener = { _, query ->
model.query.value = query
model.suggestion()
swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString())))
}
onSuggestionBinding = model.source.value!!::onSuggestionBind
onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener {
override fun onFocus() {
}
override fun onFocusCleared() {
model.setPage(1)
model.query()
}
}
attachNavigationDrawerToMenuButton(this@MainActivity.binding.drawer)
}
}
private fun onActionMenuItemSelected(item: MenuItem?) {
when(item?.itemId) {
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
R.id.source -> SourceSelectDialog().apply {
onSourceSelectedListener = {
model.setSourceAndReset(it)
dismiss()
}
onSourceSettingsSelectedListener = {
startActivity(Intent(this@MainActivity, SettingsActivity::class.java).putExtra(SettingsActivity.SETTINGS_EXTRA, it))
refreshOnResume = true
dismiss()
}
}.show(supportFragmentManager, null)
}
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
runOnUiThread {
binding.drawer.closeDrawers()
when(item.itemId) {
R.id.main_drawer_home -> model.setModeAndReset(MainViewModel.MainMode.SEARCH)
R.id.main_drawer_history -> model.setModeAndReset(MainViewModel.MainMode.HISTORY)
R.id.main_drawer_downloads -> model.setModeAndReset(MainViewModel.MainMode.DOWNLOADS)
R.id.main_drawer_help -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
R.id.main_drawer_github -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github))))
R.id.main_drawer_homepage -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page))))
R.id.main_drawer_email -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
R.id.main_drawer_kakaotalk -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
}
}
return true
}
}

View File

@@ -59,6 +59,7 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.composable.FloatingActionButtonState
import xyz.quaver.pupil.ui.composable.MultipleFloatingActionButton
import xyz.quaver.pupil.ui.composable.SubFabItem
import xyz.quaver.pupil.ui.theme.PupilTheme
import xyz.quaver.pupil.ui.viewmodel.ReaderViewModel
import xyz.quaver.pupil.util.FileXImageSource
import kotlin.math.abs
@@ -86,7 +87,7 @@ class ReaderActivity : ComponentActivity(), DIAware {
val imageHeights = remember { mutableStateListOf<Float?>() }
val states = remember { mutableStateListOf<SubSampledImageState>() }
LaunchedEffect(model.totalProgress) {
LaunchedEffect(model.progressList.sum()) {
if (imageSources.isEmpty() && model.imageList.isNotEmpty())
imageSources.addAll(List(model.imageList.size) { null })
@@ -101,7 +102,8 @@ class ReaderActivity : ComponentActivity(), DIAware {
CoroutineScope(Dispatchers.Default).launch {
imageSources[i] = kotlin.runCatching {
FileXImageSource(FileX(this@ReaderActivity, image))
}.onFailure {
}.onFailure {
logger.warning(it)
model.error(i)
}.getOrNull()
}
@@ -116,7 +118,7 @@ class ReaderActivity : ComponentActivity(), DIAware {
show(WindowInsetsCompat.Type.systemBars())
}
AppCompatTheme {
PupilTheme {
Scaffold(
topBar = {
if (!isFullscreen)

View File

@@ -0,0 +1,94 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.composable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import xyz.quaver.pupil.R
@Preview
@Composable
fun FloatingSearchBar(
modifier: Modifier = Modifier,
query: String = "",
onQueryChange: (String) -> Unit = { },
navigationIcon: @Composable () -> Unit = {
Icon(
Icons.Default.Menu,
modifier = Modifier.size(24.dp),
contentDescription = null,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
)
},
actions: @Composable RowScope.() -> Unit = { }
) {
Card(
modifier = modifier
.fillMaxWidth()
.height(64.dp)
.padding(8.dp, 8.dp)
.background(Color.Transparent),
elevation = 8.dp
) {
Row(
modifier = Modifier.fillMaxSize().padding(16.dp, 0.dp),
verticalAlignment = Alignment.CenterVertically
) {
navigationIcon()
BasicTextField(
modifier = Modifier.weight(1f).padding(16.dp, 0.dp),
value = query,
onValueChange = onQueryChange,
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colors.primary),
decorationBox = { innerTextField ->
if (query.isEmpty())
Text(
stringResource(R.string.search_hint),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
)
innerTextField()
}
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
content = actions
)
}
}
}

View File

@@ -20,6 +20,8 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -41,7 +43,7 @@ enum class FloatingActionButtonState(private val isExpanded: Boolean) {
}
data class SubFabItem(
val icon: ImageVector,
val icon: Any, // ImageVector | Painter | ImageBitmap
val label: String? = null,
val onClick: ((SubFabItem) -> Unit)? = null
)
@@ -84,7 +86,15 @@ fun MiniFloatingActionButton(
elevation = elevation,
interactionSource = interactionSource
) {
Icon(item.icon, contentDescription = null)
when (item.icon) {
is ImageVector ->
Icon(item.icon, contentDescription = null)
is Painter ->
Icon(item.icon, contentDescription = null)
is ImageBitmap ->
Icon(item.icon, contentDescription = null)
else -> error("Icon is not ImageVector | Painter | ImageBitmap")
}
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.theme
import androidx.compose.ui.graphics.Color
val LightBlue300 = Color(0xFF4FC3F7)
val LightBlue700 = Color(0xFF0288D1)
val Pink600 = Color(0xFFD81B60)
val Blue700 = Color(0xFF1976D2)
val GreenA700 = Color(0xFF00C853)
val Orange500 = Color(0xFFFF9800)

View File

@@ -0,0 +1,29 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

View File

@@ -0,0 +1,43 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
private val DarkColorPalette = darkColors()
private val LightColorPalette = lightColors()
@Composable
fun PupilTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) DarkColorPalette else LightColorPalette
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}

View File

@@ -0,0 +1,33 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
)

View File

@@ -26,18 +26,16 @@ import xyz.quaver.pupil.sources.Source
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ProgressCardView(progress: Float? = null, onLongClick: (() -> Unit)? = null, onClick: () -> Unit, content: @Composable () -> Unit) {
MaterialTheme(if (isSystemInDarkTheme()) darkColors() else lightColors()) {
Card(
modifier = Modifier
.padding(8.dp)
.combinedClickable(onClick = onClick, onLongClick = onLongClick),
shape = RoundedCornerShape(4.dp),
elevation = 4.dp
) {
Column {
progress?.run { LinearProgressIndicator(progress = progress, modifier = Modifier.fillMaxWidth()) }
content.invoke()
}
Card(
modifier = Modifier
.padding(8.dp)
.combinedClickable(onClick = onClick, onLongClick = onLongClick),
shape = RoundedCornerShape(4.dp),
elevation = 4.dp
) {
Column {
progress?.run { LinearProgressIndicator(progress = progress, modifier = Modifier.fillMaxWidth()) }
content()
}
}
}

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.ui.viewmodel
import android.annotation.SuppressLint
import android.app.Application
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.*
import kotlinx.coroutines.*
import org.kodein.di.DIAware
@@ -38,8 +39,7 @@ import kotlin.random.Random
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
override val di by closestDI()
private val _searchResults = MutableLiveData<List<ItemInfo>>()
val searchResults = _searchResults as LiveData<List<ItemInfo>>
val searchResults = mutableStateListOf<ItemInfo>()
private val _loading = MutableLiveData(false)
val loading = _loading as LiveData<Boolean>
@@ -127,29 +127,28 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
queryJob?.cancel()
_loading.value = true
val results = mutableListOf<ItemInfo>()
_searchResults.value = results
queryJob = viewModelScope.launch {
val channel = withContext(Dispatchers.IO) {
val (channel, count) = source.search(
query.value ?: "",
(currentPage - 1) * perPage until currentPage * perPage,
sortModeIndex
)
launch(Dispatchers.Default) {
val channel = withContext(Dispatchers.IO) {
val (channel, count) = source.search(
query.value ?: "",
(currentPage - 1) * perPage until currentPage * perPage,
sortModeIndex
)
totalItems.postValue(count)
totalItems.postValue(count)
channel
channel
}
for (result in channel) {
yield()
searchResults.add(result)
}
_loading.postValue(false)
}
for (result in channel) {
yield()
results.add(result)
_searchResults.value = results.toList()
}
_loading.value = false
}
}

View File

@@ -144,6 +144,9 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
progressList.addAll(List(imageCount) { 0f })
imageList.addAll(List(imageCount) { null })
totalProgressMutex.withLock {
totalProgress = 0
}
images.forEachIndexed { index, image ->
when (val scheme = image.takeWhile { it != ':' }) {
@@ -169,7 +172,7 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
totalProgress++
}
} else {
TODO("Handle error")
error(index)
}
}
}

View File

@@ -34,6 +34,8 @@ import kotlinx.coroutines.channels.Channel
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger
import xyz.quaver.hitomi.sha256
import java.io.File
import java.util.concurrent.ConcurrentHashMap
@@ -54,6 +56,8 @@ class NetworkCache(context: Context) : DIAware {
private val networkScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher())
private val logger = newLogger(LoggerFactory.default)
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun load(requestBuilder: HttpRequestBuilder.() -> Unit): File = coroutineScope {
val request = HttpRequestBuilder().apply(requestBuilder)
@@ -95,6 +99,7 @@ class NetworkCache(context: Context) : DIAware {
progressChannel.close()
}
}.onFailure {
logger.warning(it)
file.delete()
FirebaseCrashlytics.getInstance().recordException(it)
progressChannel.close(it)

View File

@@ -1,5 +1,5 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" translatable="false" tools:override="true">Pupil</string>
<string name="app_name" translatable="false" tools:override="true">Pupil-BETA</string>
<string name="release_url" translatable="false">https://api.github.com/repos/tom5079/Pupil/releases</string>

View File

@@ -31,6 +31,8 @@ import kotlinx.coroutines.runBlocking
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import org.junit.Test
import xyz.quaver.hitomi.getGalleryInfo
import xyz.quaver.hitomi.imageUrlFromImage
import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass
import kotlin.reflect.KType
@@ -40,8 +42,11 @@ class ExampleUnitTest {
@Test
fun test() {
runBlocking {
val galleryID = 479010
val files = getGalleryInfo(galleryID).files
files.forEachIndexed { i, it ->
println("$i: ${imageUrlFromImage(galleryID, it, true)}")
}
}

View File

@@ -6,10 +6,10 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.0.3")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10")
classpath("org.jetbrains.kotlin:kotlin-android-extensions:1.5.21")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.21")
classpath("com.android.tools.build:gradle:7.0.4")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
classpath("org.jetbrains.kotlin:kotlin-android-extensions:1.5.31")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.31")
classpath("com.google.gms:google-services:4.3.10")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files