diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index f0683223..00000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b10aba0e..22f913d7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -25,6 +25,7 @@ + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 457a90bc..c2f87842 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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("clearAppCache") { + commandLine("adb", "shell", "pm", "clear", "xyz.quaver.pupil.debug") } \ No newline at end of file diff --git a/app/release/app-debug.apk b/app/release/app-debug.apk deleted file mode 100644 index a6797ed1..00000000 Binary files a/app/release/app-debug.apk and /dev/null differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 3c3a9e37..e1659dc1 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -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" } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/Pupil.kt b/app/src/main/java/xyz/quaver/pupil/Pupil.kt index fc18bd43..ac1d8265 100644 --- a/app/src/main/java/xyz/quaver/pupil/Pupil.kt +++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt @@ -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() } } } } diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt index e091a3ad..78dcf908 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt @@ -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) + } } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt index 72fc5e9b..0fb49925 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt @@ -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 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 } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt index 571d7bab..316b4b7c 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt @@ -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() } val states = remember { mutableStateListOf() } - 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) diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/FloatingSearchBar.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/FloatingSearchBar.kt new file mode 100644 index 00000000..6b6e9cc5 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/FloatingSearchBar.kt @@ -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 . + */ + +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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/MultipleFloatingActionButton.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/MultipleFloatingActionButton.kt index 638fee8f..761f6edf 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/composable/MultipleFloatingActionButton.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/MultipleFloatingActionButton.kt @@ -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") + } } } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt b/app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt new file mode 100644 index 00000000..63746bc6 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt @@ -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 . + */ + +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) \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/theme/Shape.kt b/app/src/main/java/xyz/quaver/pupil/ui/theme/Shape.kt new file mode 100644 index 00000000..074ade75 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/theme/Shape.kt @@ -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 . + */ + +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) +) \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt b/app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt new file mode 100644 index 00000000..bc238c6d --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt @@ -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 . + */ + +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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/theme/Type.kt b/app/src/main/java/xyz/quaver/pupil/ui/theme/Type.kt new file mode 100644 index 00000000..6229e003 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/theme/Type.kt @@ -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 . + */ + +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 + ) +) \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCardView.kt b/app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCardView.kt index 8d997e50..881eed3f 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCardView.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCardView.kt @@ -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() } } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt index 5d14690b..e460d0ba 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt @@ -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>() - val searchResults = _searchResults as LiveData> + val searchResults = mutableStateListOf() private val _loading = MutableLiveData(false) val loading = _loading as LiveData @@ -127,29 +127,28 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware { queryJob?.cancel() _loading.value = true - val results = mutableListOf() - _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 } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt index 237bf538..676f40b4 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt @@ -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) } } } diff --git a/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt b/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt index dbb659ba..37f7df85 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt @@ -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) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c13ab423..e1094792 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - Pupil + Pupil-BETA https://api.github.com/repos/tom5079/Pupil/releases diff --git a/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt index 95278d0d..652d776a 100644 --- a/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt +++ b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt @@ -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)}") } } diff --git a/build.gradle.kts b/build.gradle.kts index 803eefd3..a8bcab60 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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