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-1631666404391.xml" value="0.36203703703703705" />
<entry key="../../../../layout/compose-model-1631801120026.xml" value="1.922077922077922" /> <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-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="../../../../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/drawable/avd_star.xml" value="0.2722222222222222" />
<entry key="app/src/main/res/layout/gallery_dialog.xml" value="0.30052083333333335" /> <entry key="app/src/main/res/layout/gallery_dialog.xml" value="0.30052083333333335" />

View File

@@ -22,7 +22,8 @@ android {
} }
buildTypes { buildTypes {
getByName("debug") { getByName("debug") {
isDebuggable = true isDebuggable = false
isMinifyEnabled = true
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
versionNameSuffix = "-DEBUG" versionNameSuffix = "-DEBUG"
@@ -34,6 +35,7 @@ android {
getByName("release") { getByName("release") {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
applicationIdSuffix = ".beta"
isCrunchPngs = false isCrunchPngs = false
@@ -47,7 +49,7 @@ android {
compose = true compose = true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.0.0" kotlinCompilerExtensionVersion = "1.0.5"
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
@@ -69,17 +71,16 @@ dependencies {
implementation("androidx.compose.ui:ui-tooling:1.0.5") implementation("androidx.compose.ui:ui-tooling:1.0.5")
implementation("androidx.compose.foundation:foundation:1.0.5") implementation("androidx.compose.foundation:foundation:1.0.5")
implementation("androidx.compose.material:material: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.material:material-icons-extended:1.0.5")
implementation("androidx.compose.runtime:runtime-livedata: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.activity:activity-compose:1.4.0")
implementation("androidx.navigation:navigation-compose:2.4.0-beta02") implementation("androidx.navigation:navigation-compose:2.4.0-beta02")
implementation("com.google.accompanist:accompanist-flowlayout:0.16.1") implementation("com.google.accompanist:accompanist-flowlayout:0.20.2")
implementation("com.google.accompanist:accompanist-appcompat-theme:0.16.0") implementation("com.google.accompanist:accompanist-appcompat-theme:0.20.2")
implementation("com.google.accompanist:accompanist-insets:0.18.0") implementation("com.google.accompanist:accompanist-insets:0.20.2")
implementation("com.google.accompanist:accompanist-insets-ui:0.18.0") implementation("com.google.accompanist:accompanist-insets-ui:0.20.2")
implementation("io.coil-kt:coil-compose:1.3.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("xyz.quaver:subsampledimage:0.0.1-alpha09-SNAPSHOT")
implementation("org.kodein.log:kodein-log:0.11.1") 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("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-inline:4.1.0") testImplementation("org.mockito:mockito-inline:4.1.0")
@@ -145,5 +146,9 @@ dependencies {
androidTestImplementation("androidx.test:runner:1.4.0") androidTestImplementation("androidx.test:runner:1.4.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.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": { "artifactType": {
"type": "APK", "type": "APK",
"kind": "Directory" "kind": "Directory"
}, },
"applicationId": "xyz.quaver.pupil", "applicationId": "xyz.quaver.pupil.beta",
"variantName": "processReleaseResources", "variantName": "release",
"elements": [ "elements": [
{ {
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"versionCode": 64, "attributes": [],
"versionName": "5.1.7-alpha1", "versionCode": 600,
"versionName": "6.0.0-alpha2",
"outputFile": "app-release.apk" "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 com.google.firebase.ktx.Firebase
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.okhttp.* import io.ktor.client.engine.okhttp.*
import io.ktor.client.features.*
import io.ktor.client.features.json.* import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.* import io.ktor.client.features.json.serializer.*
import okhttp3.Protocol
import org.kodein.di.* import org.kodein.di.*
import org.kodein.di.android.x.androidXModule import org.kodein.di.android.x.androidXModule
import org.kodein.log.LoggerFactory import org.kodein.log.LoggerFactory
@@ -49,6 +51,7 @@ import xyz.quaver.pupil.sources.sourceModule
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import java.io.File import java.io.File
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
class Pupil : Application(), DIAware { class Pupil : Application(), DIAware {
@@ -62,9 +65,21 @@ class Pupil : Application(), DIAware {
bind { singleton { bind { singleton {
HttpClient(OkHttp) { HttpClient(OkHttp) {
engine {
config {
protocols(listOf(Protocol.HTTP_1_1))
}
}
install(JsonFeature) { install(JsonFeature) {
serializer = KotlinxSerializer() 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.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance 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.databinding.SearchSuggestionItemBinding
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.hitomi.* import xyz.quaver.hitomi.*
@@ -120,6 +122,8 @@ class Hitomi(app: Application) : Source(), DIAware {
override val di: DI by closestDI(app) override val di: DI by closestDI(app)
private val logger = newLogger(LoggerFactory.default)
private val database: AppDatabase by instance() private val database: AppDatabase by instance()
private val bookmarkDao = database.bookmarkDao() private val bookmarkDao = database.bookmarkDao()
@@ -435,13 +439,13 @@ class Hitomi(app: Application) : Source(), DIAware {
var pageCount by remember { mutableStateOf("-") } var pageCount by remember { mutableStateOf("-") }
LaunchedEffect(itemInfo) { LaunchedEffect(itemInfo) {
launch { launch(Dispatchers.Default) {
itemInfo.getPageCount()?.run { itemInfo.getPageCount()?.run {
pageCount = "${this}P" pageCount = "${this}P"
} }
} }
launch { launch(Dispatchers.Default) {
itemInfo.getGroups()?.run { itemInfo.getGroups()?.run {
group = this 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 package xyz.quaver.pupil.ui
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.InputType import androidx.activity.ComponentActivity
import android.view.KeyEvent import androidx.activity.compose.setContent
import android.view.Menu
import android.view.MenuItem
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.CircularProgressIndicator 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.*
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.compose.ui.unit.dp
import androidx.core.view.GravityCompat import androidx.compose.ui.util.fastAny
import androidx.core.view.children
import androidx.core.widget.ImageViewCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.navigation.NavigationView
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI 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.*
import xyz.quaver.pupil.R 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.sources.Source
import xyz.quaver.pupil.types.* import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment import xyz.quaver.pupil.ui.composable.FloatingActionButtonState
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog 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.view.ProgressCardView
import xyz.quaver.pupil.ui.viewmodel.MainViewModel import xyz.quaver.pupil.ui.viewmodel.MainViewModel
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import kotlin.math.* import kotlin.math.*
class MainActivity : class MainActivity : ComponentActivity(), DIAware {
BaseActivity(),
NavigationView.OnNavigationItemSelectedListener,
DIAware
{
override val di by closestDI() override val di by closestDI()
private lateinit var binding: MainActivityBinding
private val model: MainViewModel by viewModels() private val model: MainViewModel by viewModels()
private var refreshOnResume = true private val logger = newLogger(LoggerFactory.default)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = MainActivityBinding.inflate(layoutInflater)
binding.contents.composeView.setContent { setContent {
val searchResults: List<ItemInfo> by model.searchResults.observeAsState(emptyList())
val source: Source? by model.source.observeAsState(null) val source: Source? by model.source.observeAsState(null)
val loading: Boolean by model.loading.observeAsState(false) 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 var lastOffset = 0
val querySectionHeight = binding.contents.searchview.binding.querySection.root.height.toFloat()
snapshotFlow { listState.firstVisibleItemScrollOffset } snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
.distinctUntilChanged() .distinctUntilChanged()
.collect { newOffset -> .collect { newOffset ->
val dy = newOffset - lastOffset val dy = newOffset - lastOffset
lastOffset = newOffset lastOffset = newOffset
binding.contents.searchview.apply { if (abs(dy) < searchBarHeight)
translationY = (translationY - dy).coerceIn(-querySectionHeight .. 0f) searchBarOffset = (searchBarOffset-dy).coerceIn(-searchBarHeight, 0)
}
} }
} }
Box(Modifier.fillMaxSize()) { PupilTheme {
LazyColumn(Modifier.fillMaxSize(), state = listState, contentPadding = PaddingValues(0.dp, 64.dp, 0.dp, 0.dp)) { Scaffold(
item(searchResults) { floatingActionButton = {
searchResults.forEach { itemInfo -> MultipleFloatingActionButton(
ProgressCardView( listOf(
progress = 0.5f, SubFabItem(
onClick = { Icons.Default.Block,
startActivity( stringResource(R.string.main_fab_cancel)
Intent( ),
this@MainActivity, SubFabItem(
ReaderActivity::class.java painterResource(R.drawable.ic_jump),
).apply { stringResource(R.string.main_jump_title)
putExtra("source", model.source.value!!.name) ),
putExtra("id", itemInfo.itemID) 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) if (loading)
CircularProgressIndicator(Modifier.align(Alignment.Center)) CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
setContentView(binding.root) FloatingSearchBar(
modifier = Modifier.offset(0.dp, LocalDensity.current.run { searchBarOffset.toDp() }),
if (Preferences["download_folder", ""].isEmpty()) query = query,
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") onQueryChange = { query = it },
actions = {
checkUpdate(this) Icon(
Icons.Default.Sort,
initView() contentDescription = null,
modifier = Modifier.size(24.dp)
model.query.observe(this) { )
binding.contents.searchview.binding.querySection.searchBarText.run { Icon(
if (text?.toString() != it) setText(it, TextView.BufferType.EDITABLE) Icons.Default.Settings,
} contentDescription = null,
} modifier = Modifier.size(24.dp)
)
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
} }
)
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.FloatingActionButtonState
import xyz.quaver.pupil.ui.composable.MultipleFloatingActionButton import xyz.quaver.pupil.ui.composable.MultipleFloatingActionButton
import xyz.quaver.pupil.ui.composable.SubFabItem 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.ui.viewmodel.ReaderViewModel
import xyz.quaver.pupil.util.FileXImageSource import xyz.quaver.pupil.util.FileXImageSource
import kotlin.math.abs import kotlin.math.abs
@@ -86,7 +87,7 @@ class ReaderActivity : ComponentActivity(), DIAware {
val imageHeights = remember { mutableStateListOf<Float?>() } val imageHeights = remember { mutableStateListOf<Float?>() }
val states = remember { mutableStateListOf<SubSampledImageState>() } val states = remember { mutableStateListOf<SubSampledImageState>() }
LaunchedEffect(model.totalProgress) { LaunchedEffect(model.progressList.sum()) {
if (imageSources.isEmpty() && model.imageList.isNotEmpty()) if (imageSources.isEmpty() && model.imageList.isNotEmpty())
imageSources.addAll(List(model.imageList.size) { null }) imageSources.addAll(List(model.imageList.size) { null })
@@ -101,7 +102,8 @@ class ReaderActivity : ComponentActivity(), DIAware {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
imageSources[i] = kotlin.runCatching { imageSources[i] = kotlin.runCatching {
FileXImageSource(FileX(this@ReaderActivity, image)) FileXImageSource(FileX(this@ReaderActivity, image))
}.onFailure { }.onFailure {
logger.warning(it)
model.error(i) model.error(i)
}.getOrNull() }.getOrNull()
} }
@@ -116,7 +118,7 @@ class ReaderActivity : ComponentActivity(), DIAware {
show(WindowInsetsCompat.Type.systemBars()) show(WindowInsetsCompat.Type.systemBars())
} }
AppCompatTheme { PupilTheme {
Scaffold( Scaffold(
topBar = { topBar = {
if (!isFullscreen) 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.rotate
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color 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.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -41,7 +43,7 @@ enum class FloatingActionButtonState(private val isExpanded: Boolean) {
} }
data class SubFabItem( data class SubFabItem(
val icon: ImageVector, val icon: Any, // ImageVector | Painter | ImageBitmap
val label: String? = null, val label: String? = null,
val onClick: ((SubFabItem) -> Unit)? = null val onClick: ((SubFabItem) -> Unit)? = null
) )
@@ -84,7 +86,15 @@ fun MiniFloatingActionButton(
elevation = elevation, elevation = elevation,
interactionSource = interactionSource 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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ProgressCardView(progress: Float? = null, onLongClick: (() -> Unit)? = null, onClick: () -> Unit, content: @Composable () -> Unit) { fun ProgressCardView(progress: Float? = null, onLongClick: (() -> Unit)? = null, onClick: () -> Unit, content: @Composable () -> Unit) {
MaterialTheme(if (isSystemInDarkTheme()) darkColors() else lightColors()) { Card(
Card( modifier = Modifier
modifier = Modifier .padding(8.dp)
.padding(8.dp) .combinedClickable(onClick = onClick, onLongClick = onLongClick),
.combinedClickable(onClick = onClick, onLongClick = onLongClick), shape = RoundedCornerShape(4.dp),
shape = RoundedCornerShape(4.dp), elevation = 4.dp
elevation = 4.dp ) {
) { Column {
Column { progress?.run { LinearProgressIndicator(progress = progress, modifier = Modifier.fillMaxWidth()) }
progress?.run { LinearProgressIndicator(progress = progress, modifier = Modifier.fillMaxWidth()) } content()
content.invoke()
}
} }
} }
} }

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.ui.viewmodel
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.* import androidx.lifecycle.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.kodein.di.DIAware import org.kodein.di.DIAware
@@ -38,8 +39,7 @@ import kotlin.random.Random
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware { class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
override val di by closestDI() override val di by closestDI()
private val _searchResults = MutableLiveData<List<ItemInfo>>() val searchResults = mutableStateListOf<ItemInfo>()
val searchResults = _searchResults as LiveData<List<ItemInfo>>
private val _loading = MutableLiveData(false) private val _loading = MutableLiveData(false)
val loading = _loading as LiveData<Boolean> val loading = _loading as LiveData<Boolean>
@@ -127,29 +127,28 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
queryJob?.cancel() queryJob?.cancel()
_loading.value = true _loading.value = true
val results = mutableListOf<ItemInfo>()
_searchResults.value = results
queryJob = viewModelScope.launch { queryJob = viewModelScope.launch {
val channel = withContext(Dispatchers.IO) { launch(Dispatchers.Default) {
val (channel, count) = source.search( val channel = withContext(Dispatchers.IO) {
query.value ?: "", val (channel, count) = source.search(
(currentPage - 1) * perPage until currentPage * perPage, query.value ?: "",
sortModeIndex (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 }) progressList.addAll(List(imageCount) { 0f })
imageList.addAll(List(imageCount) { null }) imageList.addAll(List(imageCount) { null })
totalProgressMutex.withLock {
totalProgress = 0
}
images.forEachIndexed { index, image -> images.forEachIndexed { index, image ->
when (val scheme = image.takeWhile { it != ':' }) { when (val scheme = image.takeWhile { it != ':' }) {
@@ -169,7 +172,7 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
totalProgress++ totalProgress++
} }
} else { } 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.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger
import xyz.quaver.hitomi.sha256 import xyz.quaver.hitomi.sha256
import java.io.File import java.io.File
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -54,6 +56,8 @@ class NetworkCache(context: Context) : DIAware {
private val networkScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()) private val networkScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher())
private val logger = newLogger(LoggerFactory.default)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
suspend fun load(requestBuilder: HttpRequestBuilder.() -> Unit): File = coroutineScope { suspend fun load(requestBuilder: HttpRequestBuilder.() -> Unit): File = coroutineScope {
val request = HttpRequestBuilder().apply(requestBuilder) val request = HttpRequestBuilder().apply(requestBuilder)
@@ -95,6 +99,7 @@ class NetworkCache(context: Context) : DIAware {
progressChannel.close() progressChannel.close()
} }
}.onFailure { }.onFailure {
logger.warning(it)
file.delete() file.delete()
FirebaseCrashlytics.getInstance().recordException(it) FirebaseCrashlytics.getInstance().recordException(it)
progressChannel.close(it) progressChannel.close(it)

View File

@@ -1,5 +1,5 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <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> <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.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.junit.Test import org.junit.Test
import xyz.quaver.hitomi.getGalleryInfo
import xyz.quaver.hitomi.imageUrlFromImage
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
@@ -40,8 +42,11 @@ class ExampleUnitTest {
@Test @Test
fun 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() mavenCentral()
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:7.0.3") classpath("com.android.tools.build:gradle:7.0.4")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
classpath("org.jetbrains.kotlin:kotlin-android-extensions:1.5.21") classpath("org.jetbrains.kotlin:kotlin-android-extensions:1.5.31")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.21") classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.31")
classpath("com.google.gms:google-services:4.3.10") classpath("com.google.gms:google-services:4.3.10")
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files