Dependency upgrade
[Manatoki] Implemented Artist suggestions
This commit is contained in:
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -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" />
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 != '/' }
|
||||
|
||||
@@ -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
26
buildSrc/build.gradle.kts
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
47
buildSrc/src/main/kotlin/Config.kt
Normal file
47
buildSrc/src/main/kotlin/Config.kt
Normal 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}"
|
||||
}
|
||||
Reference in New Issue
Block a user