Dependency upgrade

[Manatoki] Implemented Artist suggestions
This commit is contained in:
tom5079
2021-12-26 11:46:51 +09:00
parent 84c536a597
commit 3abd015505
14 changed files with 1227 additions and 856 deletions

1
.idea/gradle.xml generated
View File

@@ -12,6 +12,7 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/buildSrc" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />

View File

@@ -49,7 +49,7 @@ android {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.0.5"
kotlinCompilerExtensionVersion = Versions.JETPACK_COMPOSE
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -75,23 +75,24 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2-native-mt")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
implementation("androidx.compose.ui:ui:1.0.5")
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-extended:1.0.5")
implementation("androidx.compose.runtime:runtime-livedata:1.0.5")
implementation("androidx.compose.ui:ui-util:1.0.5")
implementation("androidx.compose.animation:animation:1.1.0-rc01")
implementation("androidx.activity:activity-compose:1.4.0")
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
implementation("com.google.accompanist:accompanist-flowlayout:0.20.3")
implementation("com.google.accompanist:accompanist-appcompat-theme:0.20.3")
implementation("com.google.accompanist:accompanist-insets:0.20.3")
implementation("com.google.accompanist:accompanist-insets-ui:0.20.3")
implementation("com.google.accompanist:accompanist-drawablepainter:0.20.3")
implementation("com.google.accompanist:accompanist-systemuicontroller:0.20.3")
implementation(JetpackCompose.FOUNDATION)
implementation(JetpackCompose.UI)
implementation(JetpackCompose.UI_UTIL)
implementation(JetpackCompose.UI_TOOLING)
implementation(JetpackCompose.ANIMATION)
implementation(JetpackCompose.MATERIAL)
implementation(JetpackCompose.MATERIAL_ICONS)
implementation(JetpackCompose.RUNTIME_LIVEDATA)
implementation(Accompanist.INSETS)
implementation(Accompanist.INSETS_UI)
implementation(Accompanist.FLOW_LAYOUT)
implementation(Accompanist.SYSTEM_UI_CONTROLLER)
implementation(Accompanist.DRAWABLE_PAINTER)
implementation(Accompanist.APPCOMPAT_THEME)
implementation("io.coil-kt:coil-compose:1.4.0")
@@ -104,8 +105,6 @@ dependencies {
implementation("androidx.fragment:fragment-ktx:1.4.0")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.2")
implementation("androidx.gridlayout:gridlayout:1.0.0")
implementation("androidx.biometric:biometric:1.1.0")
implementation("androidx.work:work-runtime-ktx:2.7.1")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")

View File

@@ -18,6 +18,7 @@
package xyz.quaver.pupil.sources.composable
import android.util.Log
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
@@ -184,18 +185,13 @@ fun ModalTopSheetLayout(
available: Offset,
source: NestedScrollSource
): Offset {
return if (drawerState.offset.value < 0f && source == NestedScrollSource.Drag)
return if (source == NestedScrollSource.Drag)
Offset(0f, drawerState.performDrag(available.y))
else
Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = available.y
return if (toFling > 0 && drawerState.offset.value < 0f) {
available
} else Velocity.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
drawerState.performFling(available.y)

View File

@@ -92,7 +92,6 @@ import xyz.quaver.pupil.ui.theme.Orange500
import kotlin.math.max
import kotlin.math.sign
private val imageUserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36"
@OptIn(
ExperimentalMaterialApi::class,
@@ -105,8 +104,6 @@ class Manatoki(app: Application) : Source(), DIAware {
private val logger = newLogger(LoggerFactory.default)
private val client: HttpClient by instance()
override val name = "manatoki.net"
override val iconResID = R.drawable.manatoki
@@ -119,819 +116,4 @@ class Manatoki(app: Application) : Source(), DIAware {
}
}
@Composable
fun Main(navController: NavController) {
val model: MainViewModel = viewModel()
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
val coroutineScope = rememberCoroutineScope()
val onListing: (MangaListing) -> Unit = {
mangaListing = it
}
val context = LocalContext.current
LaunchedEffect(Unit) {
context.settingsDataStore.updateData {
it.toBuilder()
.setRecentSource(name)
.build()
}
}
val onReader: (ReaderInfo) -> Unit = { readerInfo ->
coroutineScope.launch {
sheetState.snapTo(ModalBottomSheetValue.Hidden)
navController.navigate("manatoki.net/reader/${readerInfo.itemID}")
}
}
var sourceSelectDialog by remember { mutableStateOf(false) }
if (sourceSelectDialog)
SourceSelectDialog(navController, name) { sourceSelectDialog = false }
LaunchedEffect(Unit) {
model.load()
}
BackHandler {
if (sheetState.currentValue == ModalBottomSheetValue.Hidden)
navController.popBackStack()
else
coroutineScope.launch {
sheetState.hide()
}
}
ModalBottomSheetLayout(
sheetState = sheetState,
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
sheetContent = {
MangaListingBottomSheet(mangaListing) {
coroutineScope.launch {
client.getItem(it, onListing, onReader)
}
}
}
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text("마나토끼")
},
actions = {
IconButton(onClick = { sourceSelectDialog = true }) {
Image(
painter = painterResource(id = R.drawable.manatoki),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
}
IconButton(onClick = { navController.navigate("settings") }) {
Icon(Icons.Default.Settings, contentDescription = null)
}
},
contentPadding = rememberInsetsPaddingValues(
insets = LocalWindowInsets.current.statusBars,
applyBottom = false
)
)
},
floatingActionButton = {
FloatingActionButton(
modifier = Modifier.navigationBarsPadding(),
onClick = {
navController.navigate("manatoki.net/search")
}
) {
Icon(
Icons.Default.Search,
contentDescription = null
)
}
}
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
Column(
Modifier
.padding(8.dp, 0.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
"최신화",
style = MaterialTheme.typography.h5
)
IconButton(onClick = { navController.navigate("manatoki.net/recent") }) {
Icon(
Icons.Default.Add,
contentDescription = null
)
}
}
LazyRow(
modifier = Modifier
.fillMaxWidth()
.height(210.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(model.recentUpload) { item ->
Thumbnail(item,
Modifier
.width(180.dp)
.aspectRatio(6 / 7f)) {
coroutineScope.launch {
mangaListing = null
sheetState.show()
}
coroutineScope.launch {
client.getItem(it, onListing, onReader)
}
}
}
}
Divider()
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
BoardButton("마나게시판", Color(0xFF007DB4))
BoardButton("유머/가십", Color(0xFFF09614))
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
BoardButton("역식자게시판", Color(0xFFA0C850))
BoardButton("원본게시판", Color(0xFFFF4500))
}
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("만화 목록", style = MaterialTheme.typography.h5)
IconButton(onClick = { navController.navigate("manatoki.net/search") }) {
Icon(
Icons.Default.Add,
contentDescription = null
)
}
}
LazyRow(
modifier = Modifier
.fillMaxWidth()
.height(210.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(model.mangaList) { item ->
Thumbnail(item,
Modifier
.width(180.dp)
.aspectRatio(6f / 7)) {
coroutineScope.launch {
mangaListing = null
sheetState.show()
}
coroutineScope.launch {
client.getItem(it, onListing, onReader)
}
}
}
}
Text("주간 베스트", style = MaterialTheme.typography.h5)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
model.topWeekly.forEachIndexed { index, item ->
Card(
modifier = Modifier.clickable {
coroutineScope.launch {
mangaListing = null
sheetState.show()
}
coroutineScope.launch {
client.getItem(item.itemID, onListing, onReader)
}
}
) {
Row(
modifier = Modifier.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.background(Color(0xFF64C3F5))
.width(24.dp)
.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text(
(index + 1).toString(),
color = Color.White,
textAlign = TextAlign.Center
)
}
Text(
item.title,
modifier = Modifier
.weight(1f)
.padding(0.dp, 4.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
item.count,
color = Color(0xFFFF4500)
)
}
}
}
}
Box(Modifier.navigationBarsPadding())
}
}
}
}
}
@Composable
fun Reader(navController: NavController) {
val model: ReaderBaseViewModel = viewModel()
val database: AppDatabase by rememberInstance()
val bookmarkDao = database.bookmarkDao()
val coroutineScope = rememberCoroutineScope()
val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID")
var readerInfo: ReaderInfo? by rememberSaveable { mutableStateOf(null) }
LaunchedEffect(Unit) {
if (itemID != null)
client.getItem(itemID, onReader = {
readerInfo = it
model.load(it.urls) {
set("User-Agent", imageUserAgent)
}
})
else model.error = true
}
val bookmark by bookmarkDao.contains(name, itemID ?: "").observeAsState(false)
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
val mangaListingRippleInteractionSource = remember { mutableStateListOf<MutableInteractionSource>() }
val navigationBarsPadding = LocalDensity.current.run {
rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
).calculateBottomPadding().toPx()
}
val listState = rememberLazyListState()
var scrollDirection by remember { mutableStateOf(0f) }
BackHandler {
when {
sheetState.isVisible -> coroutineScope.launch { sheetState.hide() }
model.fullscreen -> model.fullscreen = false
else -> navController.popBackStack()
}
}
var mangaListingListSize: Size? by remember { mutableStateOf(null) }
ModalBottomSheetLayout(
sheetState = sheetState,
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
sheetContent = {
MangaListingBottomSheet(
mangaListing,
currentItemID = itemID,
onListSize = {
mangaListingListSize = it
},
rippleInteractionSource = mangaListingRippleInteractionSource,
listState = listState
) {
coroutineScope.launch {
client.getItem(
it,
onReader = {
navController.navigate("manatoki.net/reader/${it.itemID}") {
popUpTo("manatoki.net/")
}
}
)
}
}
}
) {
Scaffold(
topBar = {
if (!model.fullscreen)
TopAppBar(
title = {
Text(
readerInfo?.title ?: stringResource(R.string.reader_loading),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
Icons.Default.NavigateBefore,
contentDescription = null
)
}
},
actions = {
IconButton({ }) {
Image(
painter = painterResource(R.drawable.manatoki),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
}
IconButton(onClick = {
itemID?.let {
coroutineScope.launch {
if (bookmark) bookmarkDao.delete(name, it)
else bookmarkDao.insert(name, it)
}
}
}) {
Icon(
if (bookmark) Icons.Default.Star else Icons.Default.StarOutline,
contentDescription = null,
tint = Orange500
)
}
},
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.statusBars,
applyBottom = false
)
)
},
floatingActionButton = {
AnimatedVisibility(
!(model.fullscreen || scrollDirection < 0f),
enter = scaleIn(),
exit = scaleOut()
) {
FloatingActionButton(
modifier = Modifier.navigationBarsPadding(),
onClick = {
readerInfo?.let {
coroutineScope.launch {
sheetState.show()
}
coroutineScope.launch {
if (mangaListing?.itemID != it.listingItemID)
client.getItem(it.listingItemID, onListing = {
mangaListing = it
mangaListingRippleInteractionSource.addAll(
List(max(it.entries.size - mangaListingRippleInteractionSource.size, 0)) {
MutableInteractionSource()
}
)
coroutineScope.launch {
while (listState.layoutInfo.totalItemsCount != it.entries.size) {
delay(100)
}
val targetIndex = it.entries.indexOfFirst { it.itemID == itemID }
listState.scrollToItem(targetIndex)
mangaListingListSize?.let { sheetSize ->
val targetItem = listState.layoutInfo.visibleItemsInfo.first {
it.key == itemID
}
if (targetItem.offset == 0) {
listState.animateScrollBy(
-(sheetSize.height - navigationBarsPadding - targetItem.size)
)
}
delay(200)
with (mangaListingRippleInteractionSource[targetIndex]) {
val interaction = PressInteraction.Press(
Offset(sheetSize.width/2, targetItem.size/2f)
)
emit(interaction)
emit(PressInteraction.Release(interaction))
}
}
}
})
}
}
}
) {
Icon(
Icons.Default.List,
contentDescription = null
)
}
}
}
) { contentPadding ->
ReaderBase(
Modifier.padding(contentPadding),
model = model,
onScroll = { scrollDirection = it }
)
}
}
}
@Composable
fun Recent(navController: NavController) {
val model: RecentViewModel = viewModel()
val coroutineScope = rememberCoroutineScope()
var mangaListing: MangaListing? by rememberSaveable {mutableStateOf(null) }
val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
LaunchedEffect(Unit) {
model.load()
}
BackHandler {
if (state.isVisible) coroutineScope.launch { state.hide() }
else navController.popBackStack()
}
ModalBottomSheetLayout(
sheetState = state,
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
sheetContent = {
MangaListingBottomSheet(mangaListing) {
coroutineScope.launch {
client.getItem(it, onReader = {
launch {
state.snapTo(ModalBottomSheetValue.Hidden)
navController.navigate("manatoki.net/reader/${it.itemID}")
}
})
}
}
}
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text("최신 업데이트")
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
Icons.Default.NavigateBefore,
contentDescription = null
)
}
},
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.statusBars,
applyBottom = false
)
)
}
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
OverscrollPager(
currentPage = model.page,
prevPageAvailable = model.page > 1,
nextPageAvailable = model.page < 10,
nextPageTurnIndicatorOffset = rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
).calculateBottomPadding(),
onPageTurn = {
model.page = it
model.load()
}
) {
Box(Modifier.fillMaxSize()) {
LazyVerticalGrid(
GridCells.Adaptive(minSize = 200.dp),
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
)
) {
items(model.result) {
Thumbnail(
it,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(3f / 4)
.padding(8.dp)
) {
coroutineScope.launch {
mangaListing = null
state.show()
}
coroutineScope.launch {
client.getItem(it, onListing = {
mangaListing = it
})
}
}
}
}
if (model.loading)
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
}
}
}
@Composable
fun Search(navController: NavController) {
val model: SearchViewModel = viewModel()
var searchFocused by remember { mutableStateOf(false) }
val handleOffset by animateDpAsState(if (searchFocused) 0.dp else (-36).dp)
val drawerState = rememberSwipeableState(ModalTopSheetState.Hidden)
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
val coroutineScope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
model.search()
}
BackHandler {
when {
sheetState.isVisible -> coroutineScope.launch { sheetState.hide() }
drawerState.currentValue != ModalTopSheetState.Hidden ->
coroutineScope.launch { drawerState.animateTo(ModalTopSheetState.Hidden) }
else -> navController.popBackStack()
}
}
ModalBottomSheetLayout(
sheetState = sheetState,
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
sheetContent = {
MangaListingBottomSheet(mangaListing) {
coroutineScope.launch {
client.getItem(it, onReader = {
launch {
sheetState.snapTo(ModalBottomSheetValue.Hidden)
navController.navigate("manatoki.net/reader/${it.itemID}")
}
})
}
}
}
) {
Scaffold(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures { focusManager.clearFocus() }
},
topBar = {
TopAppBar(
title = {
TextField(
model.stx,
modifier = Modifier
.onFocusChanged {
searchFocused = it.isFocused
}
.fillMaxWidth(),
onValueChange = { model.stx = it },
placeholder = { Text("제목") },
textStyle = MaterialTheme.typography.subtitle1,
singleLine = true,
trailingIcon = {
if (model.stx != "" && searchFocused)
IconButton(onClick = { model.stx = "" }) {
Icon(
Icons.Default.Close,
contentDescription = null,
tint = contentColorFor(MaterialTheme.colors.primarySurface)
)
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
focusManager.clearFocus()
coroutineScope.launch {
drawerState.animateTo(ModalTopSheetState.Hidden)
}
coroutineScope.launch {
model.search()
}
}
),
colors = TextFieldDefaults.textFieldColors(
textColor = contentColorFor(MaterialTheme.colors.primarySurface),
placeholderColor = contentColorFor(MaterialTheme.colors.primarySurface).copy(alpha = 0.75f),
backgroundColor = Color.Transparent,
cursorColor = MaterialTheme.colors.secondary,
disabledIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
)
)
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
Icons.Default.NavigateBefore,
contentDescription = null
)
}
},
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.statusBars,
applyBottom = false
)
)
}
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
ModalTopSheetLayout(
modifier = Modifier.run {
if (drawerState.currentValue == ModalTopSheetState.Hidden)
offset(0.dp, handleOffset)
else
navigationBarsWithImePadding()
},
drawerState = drawerState,
drawerContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 0.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("작가")
TextField(model.artist, onValueChange = { model.artist = it })
Text("발행")
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
Chip("전체", model.publish.isEmpty()) {
model.publish = ""
}
availablePublish.forEach {
Chip(it, model.publish == it) {
model.publish = it
}
}
}
Text("초성")
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
Chip("전체", model.jaum.isEmpty()) {
model.jaum = ""
}
availableJaum.forEach {
Chip(it, model.jaum == it) {
model.jaum = it
}
}
}
Text("장르")
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
Chip("전체", model.tag.isEmpty()) {
model.tag.clear()
}
availableTag.forEach {
Chip(it, model.tag.contains(it)) {
if (model.tag.contains(it))
model.tag.remove(it)
else
model.tag[it] = it
}
}
}
Text("정렬")
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
Chip("기본", model.sst.isEmpty()) {
model.sst = ""
}
availableSst.entries.forEach { (k, v) ->
Chip(v, model.sst == k) {
model.sst = k
}
}
}
Box(
Modifier
.fillMaxWidth()
.height(8.dp))
}
}
) {
OverscrollPager(
currentPage = model.page,
prevPageAvailable = model.page > 1,
nextPageAvailable = model.page < model.maxPage,
nextPageTurnIndicatorOffset = rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
).calculateBottomPadding(),
onPageTurn = {
model.page = it
coroutineScope.launch {
model.search(resetPage = false)
}
}
) {
Box(Modifier.fillMaxSize()) {
LazyVerticalGrid(
GridCells.Adaptive(minSize = 200.dp),
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
)
) {
items(model.result) { item ->
Thumbnail(
Thumbnail(item.itemID, item.title, item.thumbnail),
modifier = Modifier
.fillMaxWidth()
.aspectRatio(3f / 4)
.padding(8.dp)
) {
coroutineScope.launch {
mangaListing = null
sheetState.show()
}
coroutineScope.launch {
client.getItem(it, onListing = {
mangaListing = it
})
}
}
}
}
if (model.loading)
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,326 @@
/*
* 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.sources.manatoki.composable
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.google.accompanist.insets.LocalWindowInsets
import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.rememberInsetsPaddingValues
import com.google.accompanist.insets.ui.Scaffold
import com.google.accompanist.insets.ui.TopAppBar
import io.ktor.client.*
import kotlinx.coroutines.launch
import org.kodein.di.compose.rememberInstance
import xyz.quaver.pupil.R
import xyz.quaver.pupil.proto.settingsDataStore
import xyz.quaver.pupil.sources.composable.SourceSelectDialog
import xyz.quaver.pupil.sources.manatoki.MangaListing
import xyz.quaver.pupil.sources.manatoki.ReaderInfo
import xyz.quaver.pupil.sources.manatoki.getItem
import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
@ExperimentalMaterialApi
@Composable
fun Main(navController: NavController) {
val model: MainViewModel = viewModel()
val client: HttpClient by rememberInstance()
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
val coroutineScope = rememberCoroutineScope()
val onListing: (MangaListing) -> Unit = {
mangaListing = it
}
val context = LocalContext.current
LaunchedEffect(Unit) {
context.settingsDataStore.updateData {
it.toBuilder()
.setRecentSource("manatoki.net")
.build()
}
}
val onReader: (ReaderInfo) -> Unit = { readerInfo ->
coroutineScope.launch {
sheetState.snapTo(ModalBottomSheetValue.Hidden)
navController.navigate("manatoki.net/reader/${readerInfo.itemID}")
}
}
var sourceSelectDialog by remember { mutableStateOf(false) }
if (sourceSelectDialog)
SourceSelectDialog(navController, "manatoki.net") { sourceSelectDialog = false }
LaunchedEffect(Unit) {
model.load()
}
BackHandler {
if (sheetState.currentValue == ModalBottomSheetValue.Hidden)
navController.popBackStack()
else
coroutineScope.launch {
sheetState.hide()
}
}
ModalBottomSheetLayout(
sheetState = sheetState,
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
sheetContent = {
MangaListingBottomSheet(mangaListing) {
coroutineScope.launch {
client.getItem(it, onListing, onReader)
}
}
}
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text("마나토끼")
},
actions = {
IconButton(onClick = { sourceSelectDialog = true }) {
Image(
painter = painterResource(id = R.drawable.manatoki),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
}
IconButton(onClick = { navController.navigate("settings") }) {
Icon(Icons.Default.Settings, contentDescription = null)
}
},
contentPadding = rememberInsetsPaddingValues(
insets = LocalWindowInsets.current.statusBars,
applyBottom = false
)
)
},
floatingActionButton = {
FloatingActionButton(
modifier = Modifier.navigationBarsPadding(),
onClick = {
navController.navigate("manatoki.net/search")
}
) {
Icon(
Icons.Default.Search,
contentDescription = null
)
}
}
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
Column(
Modifier
.padding(8.dp, 0.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
"최신화",
style = MaterialTheme.typography.h5
)
IconButton(onClick = { navController.navigate("manatoki.net/recent") }) {
Icon(
Icons.Default.Add,
contentDescription = null
)
}
}
LazyRow(
modifier = Modifier
.fillMaxWidth()
.height(210.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(model.recentUpload) { item ->
Thumbnail(item,
Modifier
.width(180.dp)
.aspectRatio(6 / 7f)) {
coroutineScope.launch {
mangaListing = null
sheetState.show()
}
coroutineScope.launch {
client.getItem(it, onListing, onReader)
}
}
}
}
Divider()
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
BoardButton("마나게시판", Color(0xFF007DB4))
BoardButton("유머/가십", Color(0xFFF09614))
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
BoardButton("역식자게시판", Color(0xFFA0C850))
BoardButton("원본게시판", Color(0xFFFF4500))
}
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("만화 목록", style = MaterialTheme.typography.h5)
IconButton(onClick = { navController.navigate("manatoki.net/search") }) {
Icon(
Icons.Default.Add,
contentDescription = null
)
}
}
LazyRow(
modifier = Modifier
.fillMaxWidth()
.height(210.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(model.mangaList) { item ->
Thumbnail(item,
Modifier
.width(180.dp)
.aspectRatio(6f / 7)) {
coroutineScope.launch {
mangaListing = null
sheetState.show()
}
coroutineScope.launch {
client.getItem(it, onListing, onReader)
}
}
}
}
Text("주간 베스트", style = MaterialTheme.typography.h5)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
model.topWeekly.forEachIndexed { index, item ->
Card(
modifier = Modifier.clickable {
coroutineScope.launch {
mangaListing = null
sheetState.show()
}
coroutineScope.launch {
client.getItem(item.itemID, onListing, onReader)
}
}
) {
Row(
modifier = Modifier.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.background(Color(0xFF64C3F5))
.width(24.dp)
.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text(
(index + 1).toString(),
color = Color.White,
textAlign = TextAlign.Center
)
}
Text(
item.title,
modifier = Modifier
.weight(1f)
.padding(0.dp, 4.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
item.count,
color = Color(0xFFFF4500)
)
}
}
}
}
Box(Modifier.navigationBarsPadding())
}
}
}
}
}

View File

@@ -0,0 +1,284 @@
/*
* 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.sources.manatoki.composable
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.NavigateBefore
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.StarOutline
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.google.accompanist.insets.LocalWindowInsets
import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.rememberInsetsPaddingValues
import com.google.accompanist.insets.ui.Scaffold
import com.google.accompanist.insets.ui.TopAppBar
import io.ktor.client.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.kodein.di.compose.rememberInstance
import xyz.quaver.pupil.R
import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.sources.composable.ReaderBase
import xyz.quaver.pupil.sources.composable.ReaderBaseViewModel
import xyz.quaver.pupil.sources.manatoki.MangaListing
import xyz.quaver.pupil.sources.manatoki.ReaderInfo
import xyz.quaver.pupil.sources.manatoki.getItem
import xyz.quaver.pupil.ui.theme.Orange500
import kotlin.math.max
private val imageUserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36"
@ExperimentalAnimationApi
@ExperimentalFoundationApi
@ExperimentalMaterialApi
@ExperimentalComposeUiApi
@Composable
fun Reader(navController: NavController) {
val model: ReaderBaseViewModel = viewModel()
val client: HttpClient by rememberInstance()
val database: AppDatabase by rememberInstance()
val bookmarkDao = database.bookmarkDao()
val coroutineScope = rememberCoroutineScope()
val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID")
var readerInfo: ReaderInfo? by rememberSaveable { mutableStateOf(null) }
LaunchedEffect(Unit) {
if (itemID != null)
client.getItem(itemID, onReader = {
readerInfo = it
model.load(it.urls) {
set("User-Agent", imageUserAgent)
}
})
else model.error = true
}
val bookmark by bookmarkDao.contains("manatoki.net", itemID ?: "").observeAsState(false)
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
val mangaListingRippleInteractionSource = remember { mutableStateListOf<MutableInteractionSource>() }
val navigationBarsPadding = LocalDensity.current.run {
rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
).calculateBottomPadding().toPx()
}
val listState = rememberLazyListState()
var scrollDirection by remember { mutableStateOf(0f) }
BackHandler {
when {
sheetState.isVisible -> coroutineScope.launch { sheetState.hide() }
model.fullscreen -> model.fullscreen = false
else -> navController.popBackStack()
}
}
var mangaListingListSize: Size? by remember { mutableStateOf(null) }
ModalBottomSheetLayout(
sheetState = sheetState,
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
sheetContent = {
MangaListingBottomSheet(
mangaListing,
currentItemID = itemID,
onListSize = {
mangaListingListSize = it
},
rippleInteractionSource = mangaListingRippleInteractionSource,
listState = listState
) {
coroutineScope.launch {
client.getItem(
it,
onReader = {
navController.navigate("manatoki.net/reader/${it.itemID}") {
popUpTo("manatoki.net/")
}
}
)
}
}
}
) {
Scaffold(
topBar = {
if (!model.fullscreen)
TopAppBar(
title = {
Text(
readerInfo?.title ?: stringResource(R.string.reader_loading),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
Icons.Default.NavigateBefore,
contentDescription = null
)
}
},
actions = {
IconButton({ }) {
Image(
painter = painterResource(R.drawable.manatoki),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
}
IconButton(onClick = {
itemID?.let {
coroutineScope.launch {
if (bookmark) bookmarkDao.delete("manatoki.net", it)
else bookmarkDao.insert("manatoki.net", it)
}
}
}) {
Icon(
if (bookmark) Icons.Default.Star else Icons.Default.StarOutline,
contentDescription = null,
tint = Orange500
)
}
},
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.statusBars,
applyBottom = false
)
)
},
floatingActionButton = {
AnimatedVisibility(
!(model.fullscreen || scrollDirection < 0f),
enter = scaleIn(),
exit = scaleOut()
) {
FloatingActionButton(
modifier = Modifier.navigationBarsPadding(),
onClick = {
readerInfo?.let {
coroutineScope.launch {
sheetState.show()
}
coroutineScope.launch {
if (mangaListing?.itemID != it.listingItemID)
client.getItem(it.listingItemID, onListing = {
mangaListing = it
mangaListingRippleInteractionSource.addAll(
List(max(it.entries.size - mangaListingRippleInteractionSource.size, 0)) {
MutableInteractionSource()
}
)
coroutineScope.launch {
while (listState.layoutInfo.totalItemsCount != it.entries.size) {
delay(100)
}
val targetIndex = it.entries.indexOfFirst { it.itemID == itemID }
listState.scrollToItem(targetIndex)
mangaListingListSize?.let { sheetSize ->
val targetItem = listState.layoutInfo.visibleItemsInfo.first {
it.key == itemID
}
if (targetItem.offset == 0) {
listState.animateScrollBy(
-(sheetSize.height - navigationBarsPadding - targetItem.size)
)
}
delay(200)
with (mangaListingRippleInteractionSource[targetIndex]) {
val interaction = PressInteraction.Press(
Offset(sheetSize.width/2, targetItem.size/2f)
)
emit(interaction)
emit(PressInteraction.Release(interaction))
}
}
}
})
}
}
}
) {
Icon(
Icons.Default.List,
contentDescription = null
)
}
}
}
) { contentPadding ->
ReaderBase(
Modifier.padding(contentPadding),
model = model,
onScroll = { scrollDirection = it }
)
}
}
}

View File

@@ -0,0 +1,157 @@
/*
* 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.sources.manatoki.composable
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NavigateBefore
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.google.accompanist.insets.LocalWindowInsets
import com.google.accompanist.insets.rememberInsetsPaddingValues
import com.google.accompanist.insets.ui.Scaffold
import com.google.accompanist.insets.ui.TopAppBar
import io.ktor.client.*
import kotlinx.coroutines.launch
import org.kodein.di.compose.rememberInstance
import xyz.quaver.pupil.sources.composable.OverscrollPager
import xyz.quaver.pupil.sources.manatoki.MangaListing
import xyz.quaver.pupil.sources.manatoki.getItem
import xyz.quaver.pupil.sources.manatoki.viewmodel.RecentViewModel
@ExperimentalFoundationApi
@ExperimentalMaterialApi
@Composable
fun Recent(navController: NavController) {
val model: RecentViewModel = viewModel()
val client: HttpClient by rememberInstance()
val coroutineScope = rememberCoroutineScope()
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
LaunchedEffect(Unit) {
model.load()
}
BackHandler {
if (state.isVisible) coroutineScope.launch { state.hide() }
else navController.popBackStack()
}
ModalBottomSheetLayout(
sheetState = state,
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
sheetContent = {
MangaListingBottomSheet(mangaListing) {
coroutineScope.launch {
client.getItem(it, onReader = {
launch {
state.snapTo(ModalBottomSheetValue.Hidden)
navController.navigate("manatoki.net/reader/${it.itemID}")
}
})
}
}
}
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text("최신 업데이트")
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
Icons.Default.NavigateBefore,
contentDescription = null
)
}
},
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.statusBars,
applyBottom = false
)
)
}
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
OverscrollPager(
currentPage = model.page,
prevPageAvailable = model.page > 1,
nextPageAvailable = model.page < 10,
nextPageTurnIndicatorOffset = rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
).calculateBottomPadding(),
onPageTurn = {
model.page = it
model.load()
}
) {
Box(Modifier.fillMaxSize()) {
LazyVerticalGrid(
GridCells.Adaptive(minSize = 200.dp),
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
)
) {
items(model.result) {
Thumbnail(
it,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(3f / 4)
.padding(8.dp)
) {
coroutineScope.launch {
mangaListing = null
state.show()
}
coroutineScope.launch {
client.getItem(it, onListing = {
mangaListing = it
})
}
}
}
}
if (model.loading)
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
}
}
}

View File

@@ -0,0 +1,340 @@
/*
* 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.sources.manatoki.composable
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.NavigateBefore
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.insets.LocalWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
import com.google.accompanist.insets.rememberInsetsPaddingValues
import com.google.accompanist.insets.ui.Scaffold
import com.google.accompanist.insets.ui.TopAppBar
import io.ktor.client.*
import kotlinx.coroutines.launch
import org.kodein.di.compose.rememberInstance
import xyz.quaver.pupil.sources.composable.ModalTopSheetLayout
import xyz.quaver.pupil.sources.composable.ModalTopSheetState
import xyz.quaver.pupil.sources.composable.OverscrollPager
import xyz.quaver.pupil.sources.manatoki.Chip
import xyz.quaver.pupil.sources.manatoki.MangaListing
import xyz.quaver.pupil.sources.manatoki.getItem
import xyz.quaver.pupil.sources.manatoki.viewmodel.*
@ExperimentalFoundationApi
@ExperimentalMaterialApi
@Composable
fun Search(navController: NavController) {
val model: SearchViewModel = viewModel()
val client: HttpClient by rememberInstance()
var searchFocused by remember { mutableStateOf(false) }
val handleOffset by animateDpAsState(if (searchFocused) 0.dp else (-36).dp)
val drawerState = rememberSwipeableState(ModalTopSheetState.Hidden)
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
val coroutineScope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
model.search()
}
BackHandler {
when {
sheetState.isVisible -> coroutineScope.launch { sheetState.hide() }
drawerState.currentValue != ModalTopSheetState.Hidden ->
coroutineScope.launch { drawerState.animateTo(ModalTopSheetState.Hidden) }
else -> navController.popBackStack()
}
}
ModalBottomSheetLayout(
sheetState = sheetState,
sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
sheetContent = {
MangaListingBottomSheet(mangaListing) {
coroutineScope.launch {
client.getItem(it, onReader = {
launch {
sheetState.snapTo(ModalBottomSheetValue.Hidden)
navController.navigate("manatoki.net/reader/${it.itemID}")
}
})
}
}
}
) {
Scaffold(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures { focusManager.clearFocus() }
},
topBar = {
TopAppBar(
title = {
TextField(
model.stx,
modifier = Modifier
.onFocusChanged {
searchFocused = it.isFocused
}
.fillMaxWidth(),
onValueChange = { model.stx = it },
placeholder = { Text("제목") },
textStyle = MaterialTheme.typography.subtitle1,
singleLine = true,
trailingIcon = {
if (model.stx != "" && searchFocused)
IconButton(onClick = { model.stx = "" }) {
Icon(
Icons.Default.Close,
contentDescription = null,
tint = contentColorFor(MaterialTheme.colors.primarySurface)
)
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
focusManager.clearFocus()
coroutineScope.launch {
drawerState.animateTo(ModalTopSheetState.Hidden)
}
coroutineScope.launch {
model.search()
}
}
),
colors = TextFieldDefaults.textFieldColors(
textColor = contentColorFor(MaterialTheme.colors.primarySurface),
placeholderColor = contentColorFor(MaterialTheme.colors.primarySurface).copy(alpha = 0.75f),
backgroundColor = Color.Transparent,
cursorColor = MaterialTheme.colors.secondary,
disabledIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
)
)
},
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
Icons.Default.NavigateBefore,
contentDescription = null
)
}
},
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.statusBars,
applyBottom = false
)
)
}
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
ModalTopSheetLayout(
modifier = Modifier.run {
if (drawerState.currentValue == ModalTopSheetState.Hidden)
offset(0.dp, handleOffset)
else
navigationBarsWithImePadding()
},
drawerState = drawerState,
drawerContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 0.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
var expanded by remember { mutableStateOf(false) }
val suggestedArtists = remember(model.artist) {
if (model.artist.isEmpty())
model.availableArtists
else
model
.availableArtists
.filter { it.contains(model.artist) }
.sortedBy { if (it.startsWith(model.artist)) 0 else 1 }
}.take(20)
Text("작가")
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = !expanded }) {
TextField(
model.artist,
onValueChange = { model.artist = it },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
}
)
ExposedDropdownMenu(expanded, onDismissRequest = { expanded = false }) {
suggestedArtists.forEach {
DropdownMenuItem(onClick = { model.artist = it; expanded = false }) {
Text(it)
}
}
}
}
Text("발행")
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
Chip("전체", model.publish.isEmpty()) {
model.publish = ""
}
availablePublish.forEach {
Chip(it, model.publish == it) {
model.publish = it
}
}
}
Text("초성")
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
Chip("전체", model.jaum.isEmpty()) {
model.jaum = ""
}
availableJaum.forEach {
Chip(it, model.jaum == it) {
model.jaum = it
}
}
}
Text("장르")
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
Chip("전체", model.tag.isEmpty()) {
model.tag.clear()
}
availableTag.forEach {
Chip(it, model.tag.contains(it)) {
if (model.tag.contains(it))
model.tag.remove(it)
else
model.tag[it] = it
}
}
}
Text("정렬")
FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
Chip("기본", model.sst.isEmpty()) {
model.sst = ""
}
availableSst.entries.forEach { (k, v) ->
Chip(v, model.sst == k) {
model.sst = k
}
}
}
Box(Modifier.fillMaxWidth().height(8.dp))
}
}
) {
OverscrollPager(
currentPage = model.page,
prevPageAvailable = model.page > 1,
nextPageAvailable = model.page < model.maxPage,
nextPageTurnIndicatorOffset = rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
).calculateBottomPadding(),
onPageTurn = {
model.page = it
coroutineScope.launch {
model.search(resetPage = false)
}
}
) {
Box(Modifier.fillMaxSize()) {
LazyVerticalGrid(
GridCells.Adaptive(minSize = 200.dp),
contentPadding = rememberInsetsPaddingValues(
LocalWindowInsets.current.navigationBars
)
) {
items(model.result) { item ->
Thumbnail(
Thumbnail(item.itemID, item.title, item.thumbnail),
modifier = Modifier
.fillMaxWidth()
.aspectRatio(3f / 4)
.padding(8.dp)
) {
coroutineScope.launch {
mangaListing = null
sheetState.show()
}
coroutineScope.launch {
client.getItem(it, onListing = {
mangaListing = it
})
}
}
}
}
if (model.loading)
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
}
}
}
}

View File

@@ -20,14 +20,14 @@ package xyz.quaver.pupil.sources.manatoki
import android.os.Parcelable
import androidx.collection.LruCache
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.google.common.util.concurrent.RateLimiter
import io.ktor.client.*
@@ -90,13 +90,15 @@ data class ReaderInfo(
@ExperimentalMaterialApi
@Composable
fun Chip(text: String, selected: Boolean = false, onClick: () -> Unit = { }) {
Card(
onClick = onClick,
backgroundColor = if (selected) MaterialTheme.colors.secondary else MaterialTheme.colors.surface,
shape = RoundedCornerShape(8.dp),
elevation = 4.dp
) {
Text(text, modifier = Modifier.padding(4.dp))
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
Card(
onClick = onClick,
backgroundColor = if (selected) MaterialTheme.colors.secondary else MaterialTheme.colors.surface,
shape = RoundedCornerShape(8.dp),
elevation = 4.dp
) {
Text(text, modifier = Modifier.padding(4.dp))
}
}
}

View File

@@ -140,6 +140,8 @@ class SearchViewModel(app: Application) : AndroidViewModel(app), DIAware {
var page by mutableStateOf(1)
var maxPage by mutableStateOf(0)
val availableArtists = mutableStateListOf<String>()
var loading by mutableStateOf(false)
private set
@@ -154,6 +156,7 @@ class SearchViewModel(app: Application) : AndroidViewModel(app), DIAware {
loading = true
result.clear()
availableArtists.clear()
if (resetPage) page = 1
searchJob = launch {
@@ -179,6 +182,13 @@ class SearchViewModel(app: Application) : AndroidViewModel(app), DIAware {
maxPage = doc.getElementsByClass("pagination").first()!!.getElementsByTag("a").maxOf { it.text().toIntOrNull() ?: 0 }
doc.select("select > option").forEach {
val value = it.ownText()
if (value.isNotEmpty())
availableArtists.add(value)
}
doc.getElementsByClass("list-item").forEach {
val itemID =
it.selectFirst(".img-item > a")!!.attr("href").takeLastWhile { it != '/' }

View File

@@ -7,9 +7,9 @@ buildscript {
}
dependencies {
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("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN_VERSION}")
classpath("org.jetbrains.kotlin:kotlin-android-extensions:${Versions.KOTLIN_VERSION}")
classpath("org.jetbrains.kotlin:kotlin-serialization:${Versions.KOTLIN_VERSION}")
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

26
buildSrc/build.gradle.kts Normal file
View File

@@ -0,0 +1,26 @@
/*
* 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/>.
*/
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
google()
}

View File

@@ -0,0 +1 @@
implementation-classpath=/home/tom5079/Workspace/Pupil/buildSrc/build/classes/java/main\:/home/tom5079/Workspace/Pupil/buildSrc/build/classes/groovy/main\:/home/tom5079/Workspace/Pupil/buildSrc/build/classes/kotlin/main\:/home/tom5079/Workspace/Pupil/buildSrc/build/resources/main

View File

@@ -0,0 +1,47 @@
/*
* 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/>.
*/
const val GROUP_ID = "xyz.quaver"
const val VERSION = "6.0.0-alpha01"
object Versions {
const val KOTLIN_VERSION = "1.6.0"
const val JETPACK_COMPOSE = "1.1.0-rc01"
const val ACCOMPANIST = "0.22.0-rc"
}
object JetpackCompose {
const val UI = "androidx.compose.ui:ui:${Versions.JETPACK_COMPOSE}"
const val UI_TOOLING = "androidx.compose.ui:ui-tooling:${Versions.JETPACK_COMPOSE}"
const val FOUNDATION = "androidx.compose.foundation:foundation:${Versions.JETPACK_COMPOSE}"
const val MATERIAL = "androidx.compose.material:material:${Versions.JETPACK_COMPOSE}"
const val MATERIAL_ICONS = "androidx.compose.material:material-icons-extended:${Versions.JETPACK_COMPOSE}"
const val RUNTIME_LIVEDATA = "androidx.compose.runtime:runtime-livedata:${Versions.JETPACK_COMPOSE}"
const val UI_UTIL = "androidx.compose.ui:ui-util:${Versions.JETPACK_COMPOSE}"
const val ANIMATION = "androidx.compose.animation:animation:${Versions.JETPACK_COMPOSE}"
}
object Accompanist {
const val FLOW_LAYOUT = "com.google.accompanist:accompanist-flowlayout:${Versions.ACCOMPANIST}"
const val APPCOMPAT_THEME = "com.google.accompanist:accompanist-appcompat-theme:${Versions.ACCOMPANIST}"
const val INSETS = "com.google.accompanist:accompanist-insets:${Versions.ACCOMPANIST}"
const val INSETS_UI = "com.google.accompanist:accompanist-insets-ui:${Versions.ACCOMPANIST}"
const val DRAWABLE_PAINTER = "com.google.accompanist:accompanist-drawablepainter:${Versions.ACCOMPANIST}"
const val SYSTEM_UI_CONTROLLER = "com.google.accompanist:accompanist-systemuicontroller:${Versions.ACCOMPANIST}"
}