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>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
<option value="$PROJECT_DIR$/buildSrc" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveModulePerSourceSet" value="false" />
|
<option name="resolveModulePerSourceSet" value="false" />
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ android {
|
|||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.0.5"
|
kotlinCompilerExtensionVersion = Versions.JETPACK_COMPOSE
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
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-coroutines-android:1.5.2-native-mt")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
|
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.activity:activity-compose:1.4.0")
|
||||||
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
|
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
|
||||||
|
|
||||||
implementation("com.google.accompanist:accompanist-flowlayout:0.20.3")
|
implementation(JetpackCompose.FOUNDATION)
|
||||||
implementation("com.google.accompanist:accompanist-appcompat-theme:0.20.3")
|
implementation(JetpackCompose.UI)
|
||||||
implementation("com.google.accompanist:accompanist-insets:0.20.3")
|
implementation(JetpackCompose.UI_UTIL)
|
||||||
implementation("com.google.accompanist:accompanist-insets-ui:0.20.3")
|
implementation(JetpackCompose.UI_TOOLING)
|
||||||
implementation("com.google.accompanist:accompanist-drawablepainter:0.20.3")
|
implementation(JetpackCompose.ANIMATION)
|
||||||
implementation("com.google.accompanist:accompanist-systemuicontroller:0.20.3")
|
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")
|
implementation("io.coil-kt:coil-compose:1.4.0")
|
||||||
|
|
||||||
@@ -104,8 +105,6 @@ dependencies {
|
|||||||
implementation("androidx.fragment:fragment-ktx:1.4.0")
|
implementation("androidx.fragment:fragment-ktx:1.4.0")
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.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.biometric:biometric:1.1.0")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.7.1")
|
implementation("androidx.work:work-runtime-ktx:2.7.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.sources.composable
|
package xyz.quaver.pupil.sources.composable
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.animation.core.TweenSpec
|
import androidx.compose.animation.core.TweenSpec
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
@@ -184,18 +185,13 @@ fun ModalTopSheetLayout(
|
|||||||
available: Offset,
|
available: Offset,
|
||||||
source: NestedScrollSource
|
source: NestedScrollSource
|
||||||
): Offset {
|
): Offset {
|
||||||
return if (drawerState.offset.value < 0f && source == NestedScrollSource.Drag)
|
return if (source == NestedScrollSource.Drag)
|
||||||
Offset(0f, drawerState.performDrag(available.y))
|
Offset(0f, drawerState.performDrag(available.y))
|
||||||
else
|
else
|
||||||
Offset.Zero
|
Offset.Zero
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
override suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero
|
||||||
val toFling = available.y
|
|
||||||
return if (toFling > 0 && drawerState.offset.value < 0f) {
|
|
||||||
available
|
|
||||||
} else Velocity.Zero
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
drawerState.performFling(available.y)
|
drawerState.performFling(available.y)
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ import xyz.quaver.pupil.ui.theme.Orange500
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.sign
|
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(
|
@OptIn(
|
||||||
ExperimentalMaterialApi::class,
|
ExperimentalMaterialApi::class,
|
||||||
@@ -105,8 +104,6 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
|
|
||||||
private val logger = newLogger(LoggerFactory.default)
|
private val logger = newLogger(LoggerFactory.default)
|
||||||
|
|
||||||
private val client: HttpClient by instance()
|
|
||||||
|
|
||||||
override val name = "manatoki.net"
|
override val name = "manatoki.net"
|
||||||
override val iconResID = R.drawable.manatoki
|
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 android.os.Parcelable
|
||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.common.util.concurrent.RateLimiter
|
import com.google.common.util.concurrent.RateLimiter
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
@@ -90,6 +90,7 @@ data class ReaderInfo(
|
|||||||
@ExperimentalMaterialApi
|
@ExperimentalMaterialApi
|
||||||
@Composable
|
@Composable
|
||||||
fun Chip(text: String, selected: Boolean = false, onClick: () -> Unit = { }) {
|
fun Chip(text: String, selected: Boolean = false, onClick: () -> Unit = { }) {
|
||||||
|
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
|
||||||
Card(
|
Card(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
backgroundColor = if (selected) MaterialTheme.colors.secondary else MaterialTheme.colors.surface,
|
backgroundColor = if (selected) MaterialTheme.colors.secondary else MaterialTheme.colors.surface,
|
||||||
@@ -98,6 +99,7 @@ fun Chip(text: String, selected: Boolean = false, onClick: () -> Unit = { }) {
|
|||||||
) {
|
) {
|
||||||
Text(text, modifier = Modifier.padding(4.dp))
|
Text(text, modifier = Modifier.padding(4.dp))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cache = LruCache<String, Any>(50)
|
private val cache = LruCache<String, Any>(50)
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ class SearchViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
var page by mutableStateOf(1)
|
var page by mutableStateOf(1)
|
||||||
var maxPage by mutableStateOf(0)
|
var maxPage by mutableStateOf(0)
|
||||||
|
|
||||||
|
val availableArtists = mutableStateListOf<String>()
|
||||||
|
|
||||||
var loading by mutableStateOf(false)
|
var loading by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -154,6 +156,7 @@ class SearchViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
|
|
||||||
loading = true
|
loading = true
|
||||||
result.clear()
|
result.clear()
|
||||||
|
availableArtists.clear()
|
||||||
if (resetPage) page = 1
|
if (resetPage) page = 1
|
||||||
|
|
||||||
searchJob = launch {
|
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 }
|
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 {
|
doc.getElementsByClass("list-item").forEach {
|
||||||
val itemID =
|
val itemID =
|
||||||
it.selectFirst(".img-item > a")!!.attr("href").takeLastWhile { it != '/' }
|
it.selectFirst(".img-item > a")!!.attr("href").takeLastWhile { it != '/' }
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ buildscript {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:7.0.4")
|
classpath("com.android.tools.build:gradle:7.0.4")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN_VERSION}")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-android-extensions:1.5.31")
|
classpath("org.jetbrains.kotlin:kotlin-android-extensions:${Versions.KOTLIN_VERSION}")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.31")
|
classpath("org.jetbrains.kotlin:kotlin-serialization:${Versions.KOTLIN_VERSION}")
|
||||||
classpath("com.google.gms:google-services:4.3.10")
|
classpath("com.google.gms:google-services:4.3.10")
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
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