diff --git a/Models/build.gradle b/Models/build.gradle index 81b311acb..bd75516f9 100644 --- a/Models/build.gradle +++ b/Models/build.gradle @@ -40,4 +40,9 @@ dependencies { //Rx implementation rxkotlin + + //Coroutines + implementation coroutinesCore + implementation coroutinesAndroid + implementation coroutinesRX } \ No newline at end of file diff --git a/Models/src/main/java/com/programmersbox/models/Models.kt b/Models/src/main/java/com/programmersbox/models/Models.kt index 912aeb603..a18a91f1e 100644 --- a/Models/src/main/java/com/programmersbox/models/Models.kt +++ b/Models/src/main/java/com/programmersbox/models/Models.kt @@ -2,6 +2,8 @@ package com.programmersbox.models import io.reactivex.Single import io.reactivex.subjects.BehaviorSubject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* import java.io.Serializable data class ItemModel( @@ -13,6 +15,7 @@ data class ItemModel( ) : Serializable { val extras = mutableMapOf() fun toInfoModel() = source.getItemInfo(this) + fun toInfoModelFlow() = source.getItemInfoFlow(this) } data class InfoModel( @@ -37,6 +40,7 @@ data class ChapterModel( ) : Serializable { var uploadedTime: Long? = null fun getChapterInfo() = source.getChapterInfo(this) + fun getChapterInfoFlow() = source.getChapterInfoFlow(this) val extras = mutableMapOf() } @@ -62,18 +66,74 @@ interface ApiService : Serializable { val canPlay: Boolean get() = true val canDownload: Boolean get() = true fun getRecent(page: Int = 1): Single> + fun getRecentFlow(page: Int = 1): Flow> = flow { emit(recent(page)) }.dispatchIo() + suspend fun recent(page: Int): List = emptyList() + fun getList(page: Int = 1): Single> + fun getListFlow(page: Int = 1): Flow> = flow { emit(allList(page)) }.dispatchIo() + suspend fun allList(page: Int): List = emptyList() + fun getItemInfo(model: ItemModel): Single + fun getItemInfoFlow(model: ItemModel): Flow> = flow { + emit( + try { + Result.success(itemInfo(model)) + } catch (e: Exception) { + e.printStackTrace() + Result.failure(e) + } + ) + } + .dispatchIo() + + suspend fun itemInfo(model: ItemModel): InfoModel = error("Need to create an itemInfo") + + suspend fun search(searchText: CharSequence, page: Int = 1, list: List): List = + list.filter { it.title.contains(searchText, true) } + fun searchList(searchText: CharSequence, page: Int = 1, list: List): Single> = Single.create { e -> e.onSuccess(list.filter { it.title.contains(searchText, true) }) } + fun searchListFlow(searchText: CharSequence, page: Int = 1, list: List): Flow> = + flow { emit(search(searchText, page, list)) } + + fun searchSourceList(searchText: CharSequence, page: Int = 1, list: List): Flow> = flow { + if (searchText.isBlank()) throw Exception("No search necessary") + emitAll(searchListFlow(searchText, page, list)) + } + .dispatchIo() + .catch { + it.printStackTrace() + emitAll(flow { emit(list.filter { it.title.contains(searchText, true) }) }) + } + fun getChapterInfo(chapterModel: ChapterModel): Single> + fun getChapterInfoFlow(chapterModel: ChapterModel): Flow> = flow { emit(chapterInfo(chapterModel)) }.dispatchIo() + suspend fun chapterInfo(chapterModel: ChapterModel): List = emptyList() fun getSourceByUrl(url: String): Single = Single.create { it.onSuccess(ItemModel("", "", url, "", this)) } + fun getSourceByUrlFlow(url: String): Flow = flow { emit(sourceByUrl(url)) } + .dispatchIo() + .catch { + it.printStackTrace() + emit(ItemModel("", "", url, "", this@ApiService)) + } + + suspend fun sourceByUrl(url: String): ItemModel = error("Not setup") + val serviceName: String get() = this::class.java.name + + fun Flow>.dispatchIoAndCatchList() = this + .dispatchIo() + .catch { + it.printStackTrace() + emit(emptyList()) + } + + fun Flow.dispatchIo() = this.flowOn(Dispatchers.IO) } val sourcePublish = BehaviorSubject.create() diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/AllFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/AllFragment.kt index ce733dcf3..6adb53803 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/AllFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/AllFragment.kt @@ -56,14 +56,15 @@ import io.reactivex.rxkotlin.addTo import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.BehaviorSubject -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import androidx.compose.material3.MaterialTheme as M3MaterialTheme import androidx.compose.material3.contentColorFor as m3ContentColorFor class AllViewModel(dao: ItemDao, context: Context? = null) : ViewModel() { - val searchPublisher = BehaviorSubject.createDefault>(emptyList()) + var searchText by mutableStateOf("") + var searchList by mutableStateOf>(emptyList()) var isSearching by mutableStateOf(false) @@ -111,32 +112,28 @@ class AllViewModel(dao: ItemDao, context: Context? = null) : ViewModel() { } private fun sourceLoadCompose(context: Context?, sources: ApiService) { - sources - .getList(count) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError { context?.showErrorToast() } - .onErrorReturnItem(emptyList()) - .doOnSubscribe { isRefreshing = true } - .subscribeBy { - sourceList.addAll(it) - isRefreshing = false - } - .addTo(disposable) + viewModelScope.launch { + sources + .getListFlow(count) + .dispatchIoAndCatchList { context?.showErrorToast() } + .onStart { isRefreshing = true } + .onEach { + sourceList.addAll(it) + isRefreshing = false + } + .collect() + } } - fun search(searchText: String) { - sourcePublish.value - ?.searchList(searchText, 1, sourceList) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.doOnSubscribe { isSearching = true } - ?.onErrorReturnItem(sourceList) - ?.subscribeBy { - searchPublisher.onNext(it) - isSearching = false - } - ?.addTo(disposable) + fun search() { + viewModelScope.launch { + sourcePublish.value + ?.searchSourceList(searchText, 1, sourceList) + ?.onStart { isSearching = true } + ?.onEach { searchList = it } + ?.onCompletion { isSearching = false } + ?.collect() + } } override fun onCleared() { @@ -233,8 +230,7 @@ fun AllView( sheetPeekHeight = ButtonDefaults.MinHeight + 4.dp, sheetContent = { val focusManager = LocalFocusManager.current - val searchList by allVm.searchPublisher.subscribeAsState(initial = emptyList()) - var searchText by rememberSaveable { mutableStateOf("") } + val searchList = allVm.searchList val searchTopAppBarScrollState = rememberTopAppBarScrollState() val scrollBehavior = remember { TopAppBarDefaults.pinnedScrollBehavior(searchTopAppBarScrollState) } Scaffold( @@ -261,8 +257,8 @@ fun AllView( ) { Text(stringResource(R.string.search)) } androidx.compose.material3.OutlinedTextField( - value = searchText, - onValueChange = { searchText = it }, + value = allVm.searchText, + onValueChange = { allVm.searchText = it }, label = { Text( stringResource( @@ -274,7 +270,7 @@ fun AllView( trailingIcon = { Row(verticalAlignment = Alignment.CenterVertically) { Text(searchList.size.toString()) - IconButton(onClick = { searchText = "" }) { + IconButton(onClick = { allVm.searchText = "" }) { Icon(Icons.Default.Cancel, null) } } @@ -286,7 +282,7 @@ fun AllView( keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() - allVm.search(searchText) + allVm.search() }) ) } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt b/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt index 45e76b9ed..27bcc6b82 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt @@ -17,10 +17,7 @@ import androidx.compose.material.icons.filled.BrokenImage import androidx.compose.material.icons.filled.BrowseGallery import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -114,7 +111,12 @@ abstract class BaseMainActivity : AppCompatActivity() { OtakuMaterialTheme(navController, genericInfo) { val showAllItem by showAll.collectAsState(false) - com.google.accompanist.navigation.material.ModalBottomSheetLayout(bottomSheetNavigator) { + com.google.accompanist.navigation.material.ModalBottomSheetLayout( + bottomSheetNavigator, + sheetBackgroundColor = MaterialTheme.colorScheme.surface, + sheetContentColor = MaterialTheme.colorScheme.onSurface, + scrimColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.32f) + ) { androidx.compose.material3.Scaffold( bottomBar = { Column { diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/DetailsFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/DetailsFragment.kt index 2e8ef6c30..62f45bcc2 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/DetailsFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/DetailsFragment.kt @@ -81,9 +81,7 @@ import io.reactivex.rxkotlin.addTo import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import me.onebone.toolbar.CollapsingToolbarScaffold @@ -240,18 +238,27 @@ class DetailViewModel( var description: String by mutableStateOf("") - private val itemSub = itemModel?.url?.let { url -> - Cached.cache[url]?.let { Single.create { emitter -> emitter.onSuccess(it) } } ?: itemModel.toInfoModel() - } - ?.doOnError { context.showErrorToast() } - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribeBy { - info = it - description = it.description - setup(it) - Cached.cache[it.url] = it + init { + viewModelScope.launch { + itemModel?.url?.let { url -> + Cached.cache[url]?.let { flow> { emit(Result.success(it)) } } ?: itemModel.toInfoModelFlow() + } + ?.dispatchIo() + ?.catch { + it.printStackTrace() + context.showErrorToast() + } + ?.onEach { + if(it.isSuccess) { + info = it.getOrThrow() + description = it.getOrThrow().description + setup(it.getOrThrow()) + Cached.cache[it.getOrThrow().url] = it.getOrThrow() + } + } + ?.collect() } + } private val englishTranslator = TranslateItems() @@ -322,7 +329,6 @@ class DetailViewModel( override fun onCleared() { super.onCleared() - itemSub?.dispose() disposable.dispose() itemListener.unregister() chapterListener.unregister() diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/GlobalSearchFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/GlobalSearchFragment.kt index 54b128e35..b7409b9c9 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/GlobalSearchFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/GlobalSearchFragment.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import androidx.lifecycle.ViewModel import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import coil.request.ImageRequest @@ -66,6 +67,7 @@ import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import me.onebone.toolbar.CollapsingToolbarScaffold import me.onebone.toolbar.ScrollStrategy @@ -93,24 +95,21 @@ class GlobalSearchViewModel( } fun searchForItems() { - Observable.combineLatest( - info.searchList() - .fastMap { a -> - a - .searchList(searchText, list = emptyList()) - .timeout(5, TimeUnit.SECONDS) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .onErrorReturnItem(emptyList()) - .map { SearchModel(a.serviceName, it) } - .toObservable() - } - ) { it.filterIsInstance().filter { s -> s.data.isNotEmpty() } } - .doOnSubscribe { isRefreshing = true } - .doOnComplete { isRefreshing = false } - .onErrorReturnItem(emptyList()) - .subscribe { searchListPublisher = it } - .addTo(disposable) + viewModelScope.launch { + combine( + info.searchList() + .fastMap { a -> + a + .searchSourceList(searchText, list = emptyList()) + .dispatchIoAndCatchList() + .map { SearchModel(a.serviceName, it) } + } + ) { it.filterIsInstance().filter { s -> s.data.isNotEmpty() } } + .onCompletion { isRefreshing = false } + .onStart { isRefreshing = true } + .onEach { searchListPublisher = it } + .collect() + } } override fun onCleared() { diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/NotificationFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/NotificationFragment.kt index fb7777e1e..b7fa2123a 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/NotificationFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/NotificationFragment.kt @@ -58,6 +58,7 @@ import com.programmersbox.favoritesdatabase.NotificationItem import com.programmersbox.favoritesdatabase.toDbModel import com.programmersbox.favoritesdatabase.toItemModel import com.programmersbox.gsonutils.toJson +import com.programmersbox.helpfulutils.notificationManager import com.programmersbox.sharedutils.MainLogo import com.programmersbox.uiviews.utils.* import io.reactivex.Completable @@ -68,7 +69,10 @@ import io.reactivex.rxkotlin.addTo import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.util.* import java.util.concurrent.TimeUnit @@ -156,18 +160,18 @@ fun NotificationsScreen( confirmButton = { TextButton( onClick = { - db.deleteAllNotifications() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeBy { + scope.launch { + val number = db.deleteAllNotificationsFlow() + launch(Dispatchers.Main) { onDismiss() Toast.makeText( context, - context.getString(R.string.deleted_notifications, it), + context.getString(R.string.deleted_notifications, number), Toast.LENGTH_SHORT ).show() + context.notificationManager.cancel(42) } - .addTo(vm.disposable) + } } ) { Text(stringResource(R.string.yes)) } }, @@ -387,24 +391,24 @@ private fun NotificationItem( interactionSource = interactionSource, indication = rememberRipple() ) { - genericInfo - .toSource(item.source) - ?.let { source -> - Cached.cache[item.url]?.let { - Single.create { emitter -> - emitter.onSuccess( - it - .toDbModel() - .toItemModel(source) - ) - } - } ?: source.getSourceByUrl(item.url) - } - ?.subscribeOn(Schedulers.io()) - ?.doOnError { context.showErrorToast() } - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribeBy { navController.navigateToDetails(it) } - ?.addTo(vm.disposable) + scope.launch { + genericInfo + .toSource(item.source) + ?.let { source -> + Cached.cache[item.url]?.let { + flow { + emit( + it + .toDbModel() + .toItemModel(source) + ) + } + } ?: source.getSourceByUrlFlow(item.url) + } + ?.dispatchIo() + ?.onEach { navController.navigateToDetails(it) } + ?.collect() + } } ) { Row { diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/RecentFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/RecentFragment.kt index b41eafc1d..51414f66e 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/RecentFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/RecentFragment.kt @@ -41,7 +41,7 @@ import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import androidx.compose.material3.MaterialTheme as M3MaterialTheme @@ -91,18 +91,19 @@ class RecentViewModel(dao: ItemDao, context: Context? = null) : ViewModel() { } private fun sourceLoadCompose(context: Context?, sources: ApiService) { - sources - .getRecent(count) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError { context?.showErrorToast() } - .onErrorReturnItem(emptyList()) - .doOnSubscribe { isRefreshing = true } - .subscribeBy { - sourceList.addAll(it) - isRefreshing = false - } - .addTo(disposable) + viewModelScope.launch { + sources + .getRecentFlow(count) + .dispatchIoAndCatchList() + .catch { + context?.showErrorToast() + emit(emptyList()) + } + .onStart { isRefreshing = true } + .onCompletion { isRefreshing = false } + .onEach { sourceList.addAll(it) } + .collect() + } } override fun onCleared() { diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/SettingsFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/SettingsFragment.kt index 56af41e77..c9992e97e 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/SettingsFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/SettingsFragment.kt @@ -58,11 +58,12 @@ import com.programmersbox.sharedutils.* import com.programmersbox.uiviews.utils.* import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.rxkotlin.Observables -import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.io.File @@ -124,7 +125,6 @@ fun SettingScreen( val topAppBarScrollState = rememberTopAppBarScrollState() val scrollBehavior = remember { TopAppBarDefaults.pinnedScrollBehavior(topAppBarScrollState) } - val listState = rememberScrollState() Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -137,7 +137,7 @@ fun SettingScreen( ) { p -> Column( modifier = Modifier - .verticalScroll(listState) + .verticalScroll(rememberScrollState()) .padding(p) .padding(bottom = 10.dp) ) { @@ -474,14 +474,13 @@ class NotificationViewModel(dao: ItemDao) : ViewModel() { var savedNotifications by mutableStateOf(0) private set - private val sub = dao.getAllNotificationCount() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeBy { savedNotifications = it } - - override fun onCleared() { - super.onCleared() - sub.dispose() + init { + viewModelScope.launch { + dao.getAllNotificationCountFlow() + .dispatchIo() + .onEach { savedNotifications = it } + .collect() + } } } @@ -494,7 +493,7 @@ private fun NotificationSettings( notificationClick: () -> Unit ) { val dao = remember { ItemDatabase.getInstance(context).itemDao() } - val notiViewModel: NotificationViewModel = viewModel(factory = factoryCreate { NotificationViewModel(dao = dao) }) + val notiViewModel: NotificationViewModel = viewModel { NotificationViewModel(dao = dao) } ShowWhen(notiViewModel.savedNotifications > 0) { CategorySetting { Text(stringResource(R.string.notifications_category_title)) } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/ContextUtils.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/ContextUtils.kt index b4269adcf..69ad6cd33 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/utils/ContextUtils.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/ContextUtils.kt @@ -63,7 +63,7 @@ import io.reactivex.rxkotlin.addTo import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.PublishSubject -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* import kotlinx.coroutines.rx2.asObservable import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -262,6 +262,9 @@ class BatteryInformation(val context: Context) { val batteryLevelAlert = PublishSubject.create() val batteryInfoItem = PublishSubject.create() + val batteryLevel by lazy { MutableStateFlow(0f) } + val batteryInfo by lazy { MutableSharedFlow() } + enum class BatteryViewType(val icon: GoogleMaterial.Icon, val composeIcon: ImageVector) { CHARGING_FULL(GoogleMaterial.Icon.gmd_battery_charging_full, Icons.Default.BatteryChargingFull), DEFAULT(GoogleMaterial.Icon.gmd_battery_std, Icons.Default.BatteryStd), @@ -304,6 +307,35 @@ class BatteryInformation(val context: Context) { .addTo(disposable) } + suspend fun composeSetupFlow( + normalBatteryColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.White, + subscribe: suspend (Pair) -> Unit + ) { + combine( + combine( + batteryLevel, + context.batteryPercent + ) { b, d -> b <= d } + .map { if (it) androidx.compose.ui.graphics.Color.Red else normalBatteryColor }, + combine( + batteryInfo, + context.batteryPercent + ) { b, d -> b to d } + .map { + when { + it.first.isCharging -> BatteryViewType.CHARGING_FULL + it.first.percent <= it.second -> BatteryViewType.ALERT + it.first.percent >= 95 -> BatteryViewType.FULL + it.first.health == BatteryHealth.UNKNOWN -> BatteryViewType.UNKNOWN + else -> BatteryViewType.DEFAULT + } + } + .distinctUntilChanged { t1, t2 -> t1 != t2 }, + ) { l, b -> l to b } + .onEach(subscribe) + .collect() + } + fun setup( disposable: CompositeDisposable, normalBatteryColor: Int = Color.WHITE, diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/FlowUtils.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/FlowUtils.kt new file mode 100644 index 000000000..053f1332d --- /dev/null +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/FlowUtils.kt @@ -0,0 +1,16 @@ +package com.programmersbox.uiviews.utils + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn + +fun Flow>.dispatchIoAndCatchList(action: (Throwable) -> Unit = {}) = this + .dispatchIo() + .catch { + it.printStackTrace() + emit(emptyList()) + action(it) + } + +fun Flow.dispatchIo() = this.flowOn(Dispatchers.IO) \ No newline at end of file diff --git a/UIViews/src/main/res/values/strings.xml b/UIViews/src/main/res/values/strings.xml index ff5a5cc6e..40da9f7bc 100644 --- a/UIViews/src/main/res/values/strings.xml +++ b/UIViews/src/main/res/values/strings.xml @@ -85,6 +85,8 @@ Choose a Theme Theme Battery Alert Percentage + View Type + Choose between a flowing view or a single page view Press to Check for Updates Update Available Current Source: %s diff --git a/anime_sources/src/main/java/com/programmersbox/anime_sources/Sources.kt b/anime_sources/src/main/java/com/programmersbox/anime_sources/Sources.kt index 6a0a656af..00496c2e1 100644 --- a/anime_sources/src/main/java/com/programmersbox/anime_sources/Sources.kt +++ b/anime_sources/src/main/java/com/programmersbox/anime_sources/Sources.kt @@ -68,8 +68,8 @@ abstract class ShowApi( internal val recentPath: String ) : ApiService { - private fun recent(page: Int = 1) = "$baseUrl/$recentPath${recentPage(page)}".toJsoup() - private fun all(page: Int = 1) = "$baseUrl/$allPath${allPage(page)}".toJsoup() + internal fun recentPath(page: Int = 1) = "$baseUrl/$recentPath${recentPage(page)}".toJsoup() + internal fun all(page: Int = 1) = "$baseUrl/$allPath${allPage(page)}".toJsoup() internal open fun recentPage(page: Int): String = "" internal open fun allPage(page: Int): String = "" @@ -83,7 +83,7 @@ abstract class ShowApi( protected fun searchListNonSingle(searchText: CharSequence, page: Int, list: List): List = if (searchText.isEmpty()) list else list.filter { it.title.contains(searchText, true) } - override fun getRecent(page: Int) = Single.create { it.onSuccess(recent(page)) } + override fun getRecent(page: Int) = Single.create { it.onSuccess(recentPath(page)) } .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .flatMap { getRecent(it) } diff --git a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/AnimeKisa.kt b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/AnimeKisa.kt index 0cc0c3c3a..c01be8713 100644 --- a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/AnimeKisa.kt +++ b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/AnimeKisa.kt @@ -62,6 +62,21 @@ abstract class AnimeKisa(allPath: String) : ShowApi( .let(emitter::onSuccess) } + override suspend fun recent(page: Int): List { + return recentPath(page) + .select("div.listAnimes") + .select("div.episode-box-2") + .fastMap { + ItemModel( + title = it.select("div.title-box").text(), + description = "", + imageUrl = it.select("img").attr("abs:src"), + url = it.select("a.an").next().select("a.an").attr("abs:href"), + source = this + ) + } + } + override fun getList(doc: Document): Single> = Single.create { emitter -> doc .select("a.an") @@ -77,6 +92,20 @@ abstract class AnimeKisa(allPath: String) : ShowApi( .let(emitter::onSuccess) } + override suspend fun allList(page: Int): List { + return all(page) + .select("a.an") + .fastMap { + ItemModel( + title = it.text(), + description = "", + imageUrl = "", + url = it.attr("abs:href"), + source = this + ) + } + } + override fun getItemInfo(source: ItemModel, doc: Document): Single = Single.create { emitter -> InfoModel( source = this, @@ -99,6 +128,28 @@ abstract class AnimeKisa(allPath: String) : ShowApi( .let(emitter::onSuccess) } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = model.url.toJsoup() + return InfoModel( + source = this, + url = model.url, + title = model.title, + description = doc.selectFirst("div.infodes2")?.text().orEmpty(), + imageUrl = doc.select("div.infopicbox").select("img").attr("abs:src"), + genres = doc.select("a.infoan").eachText(), + chapters = doc.select("a.infovan").fastMap { + ChapterModel( + name = it.text(), + url = it.attr("abs:href"), + uploaded = dateFormat.format(Date(it.select("div.timeS").attr("time").toLong() * 1000)), + sourceUrl = model.url, + source = this + ) + }, + alternativeNames = emptyList() + ) + } + override fun searchList(searchText: CharSequence, page: Int, list: List): Single> { return Single.create> { getApi("$baseUrl/search") { addEncodedQueryParameter("q", searchText.toString()) } @@ -121,6 +172,24 @@ abstract class AnimeKisa(allPath: String) : ShowApi( .onErrorResumeNext(super.searchList(searchText, page, list)) } + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + return getApi("$baseUrl/search") { addEncodedQueryParameter("q", searchText.toString()) } + .let { (it as? ApiResponse.Success)?.body } + ?.let { Jsoup.parse(it) } + ?.select("div.similarbox > a.an") + ?.mapNotNull { + if (it.attr("href") == "/") null + else + ItemModel( + title = it.select("div > div > div > div > div.similardd").text(), + description = "", + imageUrl = "$baseUrl${it.select("img.coveri").attr("src")}", + url = "$baseUrl${it.attr("href")}", + source = this + ) + }.orEmpty() + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { val doc = chapterModel.url.toJsoup() val downloadUrl = "var VidStreaming = \"(.*?)\";".toRegex() @@ -141,6 +210,24 @@ abstract class AnimeKisa(allPath: String) : ShowApi( it.onSuccess(downloadUrl) } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + val doc = chapterModel.url.toJsoup() + return "var VidStreaming = \"(.*?)\";".toRegex() + .find(doc.toString())?.groups?.get(1)?.value?.replace("load.php", "download") + ?.toJsoup() + ?.select("div.dowload") + ?.select("a") + ?.fastMap { a -> + Storage( + link = a.attr("abs:href"), + source = chapterModel.url, + quality = a.text(), + sub = "Yes" + ).apply { headers["referer"] = chapterModel.url } + } + .orEmpty() + } + override fun getSourceByUrl(url: String): Single = Single.create { emitter -> val doc = url.toJsoup() ItemModel( @@ -152,4 +239,15 @@ abstract class AnimeKisa(allPath: String) : ShowApi( ).let(emitter::onSuccess) } + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = url.toJsoup() + return ItemModel( + title = doc.select("div.infodesbox").select("h1").text(), + description = doc.select("div.infodes2").text(), + imageUrl = doc.select("div.infopicbox").select("img").attr("abs:src"), + url = url, + source = this + ) + } + } \ No newline at end of file diff --git a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Crunchyroll.kt b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Crunchyroll.kt index 64c511d74..9b17f6f10 100644 --- a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Crunchyroll.kt +++ b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Crunchyroll.kt @@ -199,6 +199,22 @@ object CrunchyRoll : ShowApi( .let(emitter::onSuccess) } + override suspend fun recent(page: Int): List { + return Jsoup.parse(crUnblock.geoBypassRequest("$baseUrl/videos/anime/popular/ajax_page?pg=1").text) + .select("li.group-item") + .fastMap { + ItemModel( + title = it.select("span.series-title").text(), + description = "", + imageUrl = it.select("span.img-holder") + .select("img") + .attr("src"), + url = fixUrl(it.select("a").attr("href")), + source = Sources.CRUNCHYROLL + ) + } + } + override fun getList(page: Int): Single> = Single.create { emitter -> emitter.onSuccess(Jsoup.parse(crUnblock.geoBypassRequest("$baseUrl/videos/anime/popular/ajax_page?pg=2").text)) } @@ -223,6 +239,22 @@ object CrunchyRoll : ShowApi( .let(emitter::onSuccess) } + override suspend fun allList(page: Int): List { + return Jsoup.parse(crUnblock.geoBypassRequest("$baseUrl/videos/anime/popular/ajax_page?pg=2").text) + .select("li.group-item") + .fastMap { + ItemModel( + title = it.select("span.series-title").text(), + description = "", + imageUrl = it.select("span.img-holder") + .select("img") + .attr("src"), + url = fixUrl(it.select("a").attr("href")), + source = Sources.CRUNCHYROLL + ) + } + } + private data class CrunchyAnimeData( val name: String, val img: String, @@ -253,6 +285,25 @@ object CrunchyRoll : ShowApi( } .onErrorResumeNext(super.searchList(searchText, page, list)) + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + return crUnblock.geoBypassRequest("http://www.crunchyroll.com/ajax/?req=RpcApiSearch_GetSearchCandidates") + .text + .split("*/")[0].replace("\\/", "/") + .split("\n").mapNotNull { s -> if (!s.startsWith("/")) s else null }.joinToString("\n") + .fromJson() + ?.data + ?.filter { data -> data.name.similarity(searchText.toString()) >= .6 || data.name.contains(searchText, true) } + ?.fastMap { d -> + ItemModel( + title = d.name, + description = "", + imageUrl = d.img.replace("small", "full"), + url = fixUrl(d.link), + source = Sources.CRUNCHYROLL + ) + } + .orEmpty() + } override fun getSourceByUrl(url: String): Single = Single.create { emitter -> try { @@ -277,6 +328,24 @@ object CrunchyRoll : ShowApi( } } + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = Jsoup.parse(crUnblock.geoBypassRequest(fixUrl(url)).text) + val p = doc.select(".description") + + val description = if (p.select(".more").text().trim().isNotEmpty()) { + p.select(".more").text().trim() + } else { + p.select("span").text().trim() + } + return ItemModel( + source = Sources.CRUNCHYROLL, + title = doc.selectFirst("#showview-content-header .ellipsis")?.text()?.trim().orEmpty(), + url = url, + description = description, + imageUrl = doc.selectFirst(".poster")?.attr("src").orEmpty(), + ) + } + override fun getItemInfo(model: ItemModel): Single = Single.create { emitter -> val doc = Jsoup.parse(crUnblock.geoBypassRequest(fixUrl(model.url)).text) @@ -352,6 +421,77 @@ object CrunchyRoll : ShowApi( override fun getItemInfo(source: ItemModel, doc: Document): Single = Single.never() + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = Jsoup.parse(crUnblock.geoBypassRequest(fixUrl(model.url)).text) + + val sub = ArrayList() + val dub = ArrayList() + + doc + .select(".season") + .fastForEach { + val seasonName = it.selectFirst("a.season-dropdown")?.text()?.trim() + it.select(".episode").forEach { ep -> + val epTitle = ep.selectFirst(".short-desc")?.text() + val epNum = episodeNumRegex.find(ep.selectFirst("span.ellipsis")?.text().toString())?.destructured?.component1() + + if (seasonName == null) { + val epi = ChapterModel( + "$epNum: $epTitle", + fixUrl(ep.attr("href")), + ep.select("div.episode-progress-bar").select("div.episode_progress").attr("media_id"), + model.url, + Sources.CRUNCHYROLL + ) + sub.add(epi) + } else if (seasonName.contains("(HD)")) { + // do nothing (filters our premium eps from one piece) + } else if (seasonName.contains("Dub") || seasonName.contains("Russian")) { + val epi = ChapterModel( + "$epNum: $epTitle (Dub)", + fixUrl(ep.attr("href")), + ep.select("div.episode-progress-bar").select("div.episode_progress").attr("media_id"), + model.url, + Sources.CRUNCHYROLL + ) + dub.add(epi) + } else { + val epi = ChapterModel( + "$epNum: $epTitle", + fixUrl(ep.attr("href")), + ep.select("div.episode-progress-bar").select("div.episode_progress").attr("media_id"), + model.url, + Sources.CRUNCHYROLL + ) + sub.add(epi) + } + } + } + + val p = doc.selectFirst(".description") + + val description = if ( + p?.selectFirst(".more") != null && + !p.selectFirst(".more")?.text()?.trim().isNullOrEmpty() + ) { + p.selectFirst(".more")?.text()?.trim() + } else { + p?.selectFirst("span")?.text()?.trim() + } + .orEmpty() + + return InfoModel( + source = Sources.CRUNCHYROLL, + title = model.title, + url = model.url, + alternativeNames = emptyList(), + description = description, + imageUrl = model.imageUrl, + genres = doc.select(".large-margin-bottom > ul:nth-child(2) li:nth-child(2) a").map { it.text() }, + chapters = sub + dub + ) + } + data class Subtitles( val language: String, val url: String, @@ -428,4 +568,56 @@ object CrunchyRoll : ShowApi( } .timeout(15, TimeUnit.SECONDS) .onErrorReturnItem(emptyList()) + + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + val contentRegex = Regex("""vilos\.config\.media = (\{.+\})""") + val response = crUnblock.geoBypassRequest(chapterModel.url) + + val hlsHelper = M3u8Helper() + + val dat = contentRegex.find(response.text)?.destructured?.component1() + + return if (!dat.isNullOrEmpty()) { + val json = dat.fromJson() + + val streams = ArrayList() + + for (stream in json?.streams.orEmpty()) { + if ( + listOf( + "adaptive_hls", "adaptive_dash", + "multitrack_adaptive_hls_v2", + "vo_adaptive_dash", "vo_adaptive_hls" + ).contains(stream.format) + ) { + if (stream.audio_lang == "jaJP" && (listOf(null, "enUS").contains(stream.hardsub_lang)) && listOf( + "m3u", + "m3u8" + ).contains(hlsHelper.absoluteExtensionDetermination(stream.url)) + ) { + stream.title = if (stream.hardsub_lang == "enUS") "Hardsub" else "Raw" + streams.add(stream) + } + } + } + + + streams.flatMap { stream -> + try { + hlsHelper.m3u8Generation(M3u8Helper.M3u8Stream(stream.url, null), false).fastMap { + Storage( + link = it.streamUrl, + source = chapterModel.url, + filename = "${chapterModel.name}.mp4", + quality = "${stream.title}: ${stream.resolution} - ${stream.format} - ${getQualityFromName(it.quality.toString()).name}", + sub = getQualityFromName(it.quality.toString()).name + ) + } + } catch (e: Exception) { + e.printStackTrace() + emptyList() + } + } + } else emptyList() + } } diff --git a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/GogoAnimeVC.kt b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/GogoAnimeVC.kt index 0f09e4ba3..8f94fa6dd 100644 --- a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/GogoAnimeVC.kt +++ b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/GogoAnimeVC.kt @@ -62,6 +62,25 @@ object GogoAnimeVC : ShowApi( s.onSuccess(f) } + override suspend fun recent(page: Int): List { + val params = mapOf("page" to "$page", "type" to "1") + return Jsoup.connect("https://ajax.gogo-load.com/ajax/page-recent-release.html") + .data(params) + .headers(headers) + .get() + .select("ul.items") + .select("li") + .fastMap { + ItemModel( + title = it.select("a").attr("title"), + description = it.text(), + imageUrl = it.select("img").attr("abs:src"), + url = "$baseUrl/category/" + it.select("a").attr("href").replace(Regex("(-episode-(\\d+))"), ""), + source = Sources.GOGOANIME_VC + ) + } + } + override fun getList(page: Int): Single> = Single.create { s -> val params = mapOf("page" to "$page", "type" to "2") val f = Jsoup.connect("https://ajax.gogo-load.com/ajax/page-recent-release.html") @@ -83,6 +102,25 @@ object GogoAnimeVC : ShowApi( s.onSuccess(f) } + override suspend fun allList(page: Int): List { + val params = mapOf("page" to "$page", "type" to "2") + return Jsoup.connect("https://ajax.gogo-load.com/ajax/page-recent-release.html") + .data(params) + .headers(headers) + .get() + .select("ul.items") + .select("li") + .fastMap { + ItemModel( + title = it.select("a").attr("title"), + description = it.text(), + imageUrl = it.select("img").attr("abs:src"), + url = "$baseUrl/category/" + it.select("a").attr("href").replace(Regex("(-episode-(\\d+))"), ""), + source = Sources.GOGOANIME_VC + ) + } + } + override fun getSourceByUrl(url: String): Single = Single.create { s -> val doc = (if (!url.contains(baseUrl)) "$baseUrl$url" else url).toJsoup() @@ -110,6 +148,32 @@ object GogoAnimeVC : ShowApi( .let(s::onSuccess) } + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = (if (!url.contains(baseUrl)) "$baseUrl$url" else url).toJsoup() + + val animeBody = doc.selectFirst(".anime_info_body_bg") + val title = animeBody?.selectFirst("h1")?.text().orEmpty() + val poster = animeBody?.selectFirst("img")?.attr("src").orEmpty() + + var description: String? = null + + animeBody?.select("p.type")?.fastForEach { + when (it.selectFirst("span")?.text()?.trim()) { + "Plot Summary:" -> { + description = it.text().replace("Plot Summary:", "").trim() + } + } + } + + return ItemModel( + title = title, + description = description.orEmpty(), + imageUrl = poster, + url = url, + source = Sources.GOGOANIME_VC + ) + } + override fun searchList(searchText: CharSequence, page: Int, list: List): Single> = Single.create> { s -> Jsoup.connect("$baseUrl/search.html?keyword=$searchText").get() @@ -127,6 +191,19 @@ object GogoAnimeVC : ShowApi( } .onErrorResumeNext(super.searchList(searchText, page, list)) + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + return Jsoup.connect("$baseUrl/search.html?keyword=$searchText").get() + .select(""".last_episodes li""") + .fastMap { + ItemModel( + title = it.selectFirst(".name")?.text()?.replace(" (Dub)", "").orEmpty(), + description = it.text(), + imageUrl = it.selectFirst("img")?.attr("src").orEmpty(), + url = fixUrl(it.selectFirst(".name > a")?.attr("href").orEmpty()), + source = Sources.GOGOANIME_VC + ) + } + } override fun getRecent(doc: Document): Single> = Single.create { it.onSuccess(emptyList()) } override fun getList(doc: Document): Single> = Single.create { it.onSuccess(emptyList()) } @@ -187,6 +264,56 @@ object GogoAnimeVC : ShowApi( override fun getItemInfo(source: ItemModel, doc: Document): Single = Single.never() + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = Jsoup.connect(model.url).get() + + val animeBody = doc.selectFirst(".anime_info_body_bg") + val title = animeBody?.selectFirst("h1")?.text() ?: model.title + val poster = animeBody?.selectFirst("img")?.attr("src") ?: model.imageUrl + + var description: String? = null + val genre = mutableListOf() + + animeBody?.select("p.type")?.fastForEach { + when (it.selectFirst("span")?.text()?.trim()) { + "Plot Summary:" -> { + description = it.text().replace("Plot Summary:", "").trim() + } + "Genre:" -> { + genre.addAll(it.select("a").fastMap { g -> g.attr("title") }) + } + } + } + + val animeId = doc.selectFirst("#movie_id")?.attr("value") + val params = mapOf("ep_start" to "0", "ep_end" to "2000", "id" to animeId) + + val chapters = Jsoup.connect(episodeloadApi) + .data(params) + .get() + .select("a") + .fastMap { + ChapterModel( + "Episode " + it.selectFirst(".name")?.text()?.replace("EP", "")?.trim().orEmpty(), + fixUrl(it.attr("href").trim()), + "", + model.url, + Sources.GOGOANIME_VC + ) + } + + return InfoModel( + source = Sources.GOGOANIME_VC, + title = title, + url = model.url, + alternativeNames = emptyList(), + description = description.orEmpty(), + imageUrl = poster, + genres = genre, + chapters = chapters + ) + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { s -> val doc = Jsoup.connect(chapterModel.url).get() val iframe = "https:" + doc.selectFirst("div.play-video > iframe")?.attr("src").orEmpty() @@ -249,6 +376,67 @@ object GogoAnimeVC : ShowApi( .let(s::onSuccess) } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + val doc = Jsoup.connect(chapterModel.url).get() + val iframe = "https:" + doc.selectFirst("div.play-video > iframe")?.attr("src").orEmpty() + val link = iframe.replace("streaming.php", "download") + return Jsoup.connect(link) + .headers(mapOf("Referer" to iframe)) + .get() + .select(".dowload > a") + .fastMap { + if (it.hasAttr("download")) { + val qual = if (it.text().contains("HDP")) + "1080" + else + qualityRegex.find(it.text())?.destructured?.component1().toString() + listOf( + Storage( + link = it.attr("href"), + source = link, + filename = "${chapterModel.name}.mp4", + quality = qual, + sub = getQualityFromName(qual).value.toString() + ) + ) + } else { + val url = it.attr("href") + extractors + .flatMap { e -> + if (url.startsWith(e.mainUrl)) { + //println(url + "\t" + e.name) + e.getUrl(url) + } else emptyList() + /*listOf( + Storage( + link = it.attr("href"), + source = link, + filename = "${chapterModel.name}.mp4", + quality = it.text(), + sub = it.text() + ) + )*/ + } + //.distinctBy { it.link } + /*if(url.startsWith(XStreamCdn.mainUrl)) + XStreamCdn.getUrl(url) + else + listOf( + Storage( + link = it.attr("href"), + source = link, + filename = "${chapterModel.name}.mp4", + quality = it.text(), + sub = it.text() + ) + )*/ + } + } + .flatten() + //.filter { it.link?.endsWith(".mp4") == true } + .sortedByDescending { it.sub?.toIntOrNull() } + } + private val packedRegex = Regex("""eval\(function\(p,a,c,k,e,.*\)\)""") private fun getPacked(string: String): String? = packedRegex.find(string)?.value private fun getAndUnpack(string: String): String? = JsUnpacker(getPacked(string)).unpack() diff --git a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Kawaiifu.kt b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Kawaiifu.kt index 808018b6c..796f7d1ad 100644 --- a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Kawaiifu.kt +++ b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Kawaiifu.kt @@ -37,6 +37,20 @@ object Kawaiifu : ShowApi( .let(emitter::onSuccess) } + override suspend fun recent(page: Int): List { + return recentPath(page) + .select(".today-update .item") + .fastMap { + ItemModel( + title = it.selectFirst("img")?.attr("alt").orEmpty(), + description = it.select("div.info").select("p").text(), + imageUrl = it.selectFirst("img")?.attr("src").orEmpty(), + url = it.selectFirst("a")?.attr("href").orEmpty(), + source = Sources.KAWAIIFU + ) + } + } + override fun getList(doc: Document): Single> = Single.create { emitter -> doc.select(".section") .select(".list-film > .item") @@ -52,6 +66,21 @@ object Kawaiifu : ShowApi( .let(emitter::onSuccess) } + override suspend fun allList(page: Int): List { + return all(page) + .select(".section") + .select(".list-film > .item") + .fastMap { + ItemModel( + title = it.select("img").attr("alt"), + description = it.selectFirst("p.txtstyle2")?.select("span.cot1")?.text().orEmpty(), + imageUrl = it.selectFirst("img")?.attr("src").orEmpty(), + url = it.selectFirst("a")?.attr("abs:href").orEmpty(), + source = Sources.KAWAIIFU + ) + } + } + override fun searchList(searchText: CharSequence, page: Int, list: List): Single> = Single.create> { s -> Jsoup.connect("$baseUrl/search-movie?keyword=$searchText").get() @@ -69,6 +98,20 @@ object Kawaiifu : ShowApi( } .onErrorResumeNext(super.searchList(searchText, page, list)) + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + return Jsoup.connect("$baseUrl/search-movie?keyword=$searchText").get() + .select(".item") + .fastMap { + ItemModel( + title = it.selectFirst("img")?.attr("alt").orEmpty(), + description = it.text(), + imageUrl = it.selectFirst("img")?.attr("src").orEmpty(), + url = it.selectFirst("a")?.attr("href").orEmpty(), + source = Sources.KAWAIIFU + ) + } + } + override fun getItemInfo(source: ItemModel, doc: Document): Single = Single.create { emitter -> InfoModel( source = Sources.KAWAIIFU, @@ -105,6 +148,42 @@ object Kawaiifu : ShowApi( .let(emitter::onSuccess) } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = model.url.toJsoup() + return InfoModel( + source = Sources.KAWAIIFU, + title = model.title, + url = model.url, + alternativeNames = emptyList(), + description = doc.select(".sub-desc p") + .filter { it: Element -> it.select("strong").isEmpty() && it.select("iframe").isEmpty() } + .joinToString("\n") { it.text() }, + imageUrl = model.imageUrl, + genres = doc.select(".table a[href*=\"/tag/\"]").fastMap { tag -> tag.text() }, + chapters = try { + doc.selectFirst("a[href*=\".html-episode\"]") + ?.attr("href") + ?.toJsoup() + ?.selectFirst(".list-ep") + ?.select("li") + ?.fastMap { + ChapterModel( + if (it.text().trim().toIntOrNull() != null) "Episode ${it.text().trim()}" else it.text().trim(), + it.selectFirst("a")?.attr("href").orEmpty(), + "", + model.url, + Sources.KAWAIIFU + ) + } + ?.reversed() + .orEmpty() + } catch (e: Exception) { + e.printStackTrace() + emptyList() + } + ) + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { emitter -> val data = chapterModel.url @@ -152,6 +231,50 @@ object Kawaiifu : ShowApi( emitter.onSuccess(servers) } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + val data = chapterModel.url + val doc = data.toJsoup() + + val episodeNum = if (data.contains("ep=")) data.split("ep=")[1].split("&")[0].toIntOrNull() else null + + return doc.select(".list-server") + .fastMap { + val serverName = it.selectFirst(".server-name")?.text().orEmpty() + val episodes = it.select(".list-ep > li > a").map { episode -> Pair(episode.attr("href"), episode.text()) } + val episode = if (episodeNum == null) episodes[0] else episodes.mapNotNull { ep -> + if ((if (ep.first.contains("ep=")) ep.first.split("ep=")[1].split("&")[0].toIntOrNull() else null) == episodeNum) { + ep + } else null + }[0] + Pair(serverName, episode) + } + .fastMap { + if (it.second.first == data) { + val sources = doc.select("video > source") + .fastMap { source -> Pair(source.attr("src"), source.attr("data-quality")) } + Triple(it.first, sources, it.second.second) + } else { + val html = it.second.first.toJsoup() + + val sources = html.select("video > source") + .fastMap { source -> Pair(source.attr("src"), source.attr("data-quality")) } + Triple(it.first, sources, it.second.second) + } + } + .fastMap { + it.second.fastMap { source -> + Storage( + link = source.first, + source = chapterModel.url, + filename = "${chapterModel.name}.mp4", + quality = it.first + "-" + source.second, + sub = getQualityFromName(source.second).value.toString() + ) + } + } + .flatten() + } + override fun getSourceByUrl(url: String): Single = Single.create { emitter -> val doc = url.toJsoup() ItemModel( @@ -165,4 +288,17 @@ object Kawaiifu : ShowApi( ).let(emitter::onSuccess) } + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = url.toJsoup() + return ItemModel( + title = doc.selectFirst(".title")?.text().orEmpty(), + description = doc.select(".sub-desc p") + .filter { it: Element -> it.select("strong").isEmpty() && it.select("iframe").isEmpty() } + .joinToString("\n") { it.text() }, + imageUrl = doc.selectFirst("a.thumb > img")?.attr("src").orEmpty(), + url = url, + source = this + ) + } + } \ No newline at end of file diff --git a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Sflix.kt b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Sflix.kt index 19da5b4b6..0bf4658e4 100644 --- a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Sflix.kt +++ b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Sflix.kt @@ -7,6 +7,7 @@ import android.webkit.* import androidx.compose.ui.util.fastMap import com.programmersbox.anime_sources.ShowApi import com.programmersbox.anime_sources.Sources +import com.programmersbox.anime_sources.toJsoup import com.programmersbox.anime_sources.utilities.* import com.programmersbox.gsonutils.fromJson import com.programmersbox.models.ChapterModel @@ -64,6 +65,26 @@ abstract class Sflix(baseUrl: String, private val servName: String) : ShowApi( .let(s::onSuccess) } + override suspend fun recent(page: Int): List { + return recentPath(page) + .select("section.block_area.block_area_home.section-id-02") + .select("div.film-poster") + .fastMap { + val img = it.select("img") + val title = img.attr("title") + val posterUrl = img.attr("data-src") + val href = fixUrl(it.select("a").attr("href")) + + ItemModel( + title = title, + description = "", + imageUrl = posterUrl, + url = href, + source = sourceName + ) + } + } + override fun getList(doc: Document): Single> = Single.create { s -> val map = listOf( "div#trending-movies", @@ -91,6 +112,32 @@ abstract class Sflix(baseUrl: String, private val servName: String) : ShowApi( s.onSuccess(items) } + override suspend fun allList(page: Int): List { + val map = listOf( + "div#trending-movies", + "div#trending-tv", + ) + return map.flatMap { key -> + all(page) + .select(key) + .select("div.film-poster") + .fastMap { + val img = it.select("img") + val title = img.attr("title") + val posterUrl = img.attr("data-src") + val href = fixUrl(it.select("a").attr("href")) + + ItemModel( + title = title, + description = "", + imageUrl = posterUrl, + url = href, + source = sourceName + ) + } + } + } + override fun searchList(searchText: CharSequence, page: Int, list: List): Single> = Single.create> { s -> Jsoup.connect("$baseUrl/search/${searchText.toString().replace(" ", "-")}").get() @@ -112,6 +159,24 @@ abstract class Sflix(baseUrl: String, private val servName: String) : ShowApi( } .onErrorResumeNext(super.searchList(searchText, page, list)) + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + return Jsoup.connect("$baseUrl/search/${searchText.toString().replace(" ", "-")}").get() + .select("div.flw-item") + .fastMap { + val title = it.select("h2.film-name").text() + val href = fixUrl(it.select("a").attr("href")) + val year = it.select("span.fdi-item").text() + val image = it.select("img").attr("data-src") + ItemModel( + title = title, + description = year, + imageUrl = image, + url = href, + source = sourceName + ) + } + } + override fun getItemInfo(source: ItemModel, doc: Document): Single = Single.create { emitter -> val details = doc.select("div.detail_page-watch") @@ -188,6 +253,80 @@ abstract class Sflix(baseUrl: String, private val servName: String) : ShowApi( .let(emitter::onSuccess) } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val details = model.url.toJsoup().select("div.detail_page-watch") + val img = details.select("img.film-poster-img") + val posterUrl = img.attr("src") + val title = img.attr("title") + + val plot = details.select("div.description").text().replace("Overview:", "").trim() + + val isMovie = model.url.contains("/movie/") + + val idRegex = Regex(""".*-(\d+)""") + val dataId = details.attr("data-id") + val id = if (dataId.isNullOrEmpty()) idRegex.find(model.url)?.groupValues?.get(1).orEmpty() else dataId + + val episodes = if (isMovie) { + val episodesUrl = "$baseUrl/ajax/movie/episodes/$id" + val episodes = get(episodesUrl).text + + // Supported streams, they're identical + val sourceId = Jsoup.parse(episodes).select("a").firstOrNull { + it.select("span").text().trim().equals("RapidStream", ignoreCase = true) + || it.select("span").text().trim().equals("Vidcloud", ignoreCase = true) + }?.attr("data-id") + + val webViewUrl = "$baseUrl${sourceId?.let { ".$it" } ?: ""}".replace("/movie/", "/watch-movie/") + listOf( + ChapterModel( + title, + webViewUrl, + "", + model.url, + this@Sflix.sourceName + ) + ) + } else { + val seasonsHtml = get("$baseUrl/ajax/v2/tv/seasons/$id").text + val seasonsDocument = Jsoup.parse(seasonsHtml) + + seasonsDocument.select("div.dropdown-menu.dropdown-menu-model > a").flatMapIndexed { season, element -> + val seasonId = element.attr("data-id") + if (seasonId.isNullOrBlank()) emptyList() else { + + val seasonHtml = get("$baseUrl/ajax/v2/season/episodes/$seasonId").text + val seasonDocument = Jsoup.parse(seasonHtml) + seasonDocument.select("div.flw-item.film_single-item.episode-item.eps-item") + .mapIndexed { _, it -> + val episodeImg = it.select("img") + val episodeTitle = episodeImg.attr("title") + val episodeData = it.attr("data-id") + + ChapterModel( + "S${season + 1}: $episodeTitle", + "${model.url}:::$episodeData", + "", + model.url, + this@Sflix.sourceName + ) + } + } + } + } + + return InfoModel( + source = this@Sflix.sourceName, + title = title, + url = model.url, + alternativeNames = emptyList(), + description = plot, + imageUrl = posterUrl, + genres = emptyList(), + chapters = episodes.reversed() + ) + } + override fun getSourceByUrl(url: String): Single = Single.create { emitter -> ItemModel( title = "", @@ -198,6 +337,16 @@ abstract class Sflix(baseUrl: String, private val servName: String) : ShowApi( ).let(emitter::onSuccess) } + override suspend fun sourceByUrl(url: String): ItemModel { + return ItemModel( + title = "", + description = "", + imageUrl = "", + url = url, + source = this + ) + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { emitter -> //println(chapterModel.url.toJsoup()) @@ -240,6 +389,44 @@ abstract class Sflix(baseUrl: String, private val servName: String) : ShowApi( emitter.onSuccess(d) } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + // To transfer url:::id + val split = chapterModel.url.split(":::") + // Only used for tv series + val url = if (split.size == 2) { + val episodesUrl = "$baseUrl/ajax/v2/episode/servers/${split[1]}" + val episodes = get(episodesUrl).text + + // Supported streams, they're identical + val sourceId = Jsoup.parse(episodes).select("a").firstOrNull { + it.select("span").text().trim().equals("RapidStream", ignoreCase = true) + || it.select("span").text().trim().equals("Vidcloud", ignoreCase = true) + }?.attr("data-id") + + "${split[0]}${sourceId?.let { ".$it" } ?: ""}".replace("/tv/", "/watch-tv/") + } else { + chapterModel.url + } + + val sources = get( + url, + interceptor = WebViewResolver(Regex("""/getSources""")) + ).text + + val mapped = sources.fromJson() + + val list = listOf( + mapped?.sources to "source 1", + mapped?.sources1 to "source 2", + mapped?.sources2 to "source 3", + mapped?.sourcesBackup to "source backup" + ) + + return list.flatMap { subList -> + subList.first?.fastMap { it?.toExtractorLink(chapterModel, subList.second).orEmpty() }.orEmpty() + }.flatten() + } + private fun SourcesDope.toExtractorLink(caller: ChapterModel, name: String): List? { return this.file?.let { file -> val isM3u8 = URI(this.file).path.endsWith(".m3u8") || this.type.equals("hls", ignoreCase = true) diff --git a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Vidstreaming.kt b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Vidstreaming.kt index 5b460619b..9830eb3ef 100644 --- a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Vidstreaming.kt +++ b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Vidstreaming.kt @@ -66,6 +66,20 @@ abstract class VidstreamingTemplate( .let(it::onSuccess) } + override suspend fun recent(page: Int): List { + return recentPath(page) + .select("li.video-block") + .fastMap { + ItemModel( + title = it.select("div.name").text(), + description = "", + imageUrl = it.select("div.picture").select("img").attr("abs:src"), + url = it.select("a").first()?.attr("abs:href").orEmpty(), + source = this + ) + } + } + override fun getList(doc: Document): Single> = Single.create { doc .select("li.video-block") @@ -81,6 +95,20 @@ abstract class VidstreamingTemplate( .let(it::onSuccess) } + override suspend fun allList(page: Int): List { + return all(page) + .select("li.video-block") + .fastMap { + ItemModel( + title = it.select("div.name").text(), + description = "", + imageUrl = it.select("div.picture").select("img").attr("abs:src"), + url = it.select("a").first()?.attr("abs:href").orEmpty(), + source = this + ) + } + } + override fun getItemInfo(source: ItemModel, doc: Document): Single = Single.create { InfoModel( source = this, @@ -103,6 +131,28 @@ abstract class VidstreamingTemplate( .let(it::onSuccess) } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = model.url.toJsoup() + return InfoModel( + source = this, + title = model.title, + url = model.url, + alternativeNames = emptyList(), + description = doc.select("div.post-entry").text(), + imageUrl = model.imageUrl, + genres = emptyList(), + chapters = doc.select("div.video-info-left > ul.listing > li.video-block > a").fastMap { + ChapterModel( + it.select("div.name").text(), + it.select("a").attr("abs:href"), + it.select("span.date").text(), + model.url, + this + ) + } + ) + } + override fun getSourceByUrl(url: String): Single = Single.create { val doc = url.toJsoup() ItemModel( @@ -121,6 +171,23 @@ abstract class VidstreamingTemplate( .let(it::onSuccess) } + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = url.toJsoup() + return ItemModel( + title = doc.select("div.video-details").select("span.date").text(), + description = doc.select("div.post-entry").text(), + imageUrl = doc + .select("div.video-info-left > ul.listing > li.video-block > a") + .select("div.picture") + .select("img") + .randomOrNull() + ?.attr("abs:src") + .orEmpty(), + url = url, + source = this + ) + } + override fun searchList(searchText: CharSequence, page: Int, list: List): Single> { return if (searchText.isEmpty()) super.searchList(searchText, page, list) else Single.create> { @@ -141,6 +208,20 @@ abstract class VidstreamingTemplate( .onErrorResumeNext(super.searchList(searchText, page, list)) } + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + return "$searchUrl/search.html?keyword=${searchText.split(" ").joinToString("%20")}".toJsoup() + .select("li.video-block") + .fastMap { + ItemModel( + title = it.select("div.name").text(), + description = "", + imageUrl = it.select("div.picture").select("img").attr("abs:src"), + url = it.select("a").first()?.attr("abs:href").orEmpty(), + source = this + ) + } + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> { return Single.create> { emitter -> @@ -181,6 +262,35 @@ abstract class VidstreamingTemplate( .onErrorReturnItem(emptyList()) } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + val v = chapterModel.url.toJsoup().select("div.play-video").select("iframe").attr("abs:src") + + val s = v.toJsoup() + + val servers = s.select(".list-server-items > .linkserver").mapNotNull { li -> + if (!li?.attr("data-video").isNullOrEmpty()) { + li.text() to fixUrl(li.attr("data-video"), baseUrl) + } else { + null + } + } + + return servers + .map { l -> + //println(l) + extractors.flatMap { e -> + //println(e.name) + if (l.second.startsWith(e.mainUrl)) { + //println(url + "\t" + e.name) + e.getUrl(l.second) + } else emptyList() + } + } + .filter { it.isNotEmpty() } + .flatten() + .distinctBy { it.link } + } + data class Xstream(val success: Boolean?, val player: Any?, val data: List?, val captions: Any?, val is_vr: Boolean?) data class XstreamData(val file: String?, val label: String?, val type: String?) diff --git a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/WcoStream.kt b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/WcoStream.kt index 98faa6001..e2856cf39 100644 --- a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/WcoStream.kt +++ b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/WcoStream.kt @@ -76,6 +76,23 @@ object WcoStream : ShowApi( .onErrorResumeNext(super.searchList(searchText, page, list)) } + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + return Jsoup.connect("$baseUrl/search") + .data("catara", searchText.toString()) + .data("konuara", "series") + .post() + .select("div.cerceve") + .fastMap { + ItemModel( + title = it.select("a").attr("title"), + description = "", + imageUrl = it.select("img").attr("abs:src"), + url = it.select("a").attr("abs:href"), + source = this + ) + } + } + override fun getRecent(doc: Document): Single> = getList(doc) /*Single.create { emitter -> //https://www.wcostream.com/anime/mushi-shi-english-dubbed-guide @@ -118,6 +135,23 @@ object WcoStream : ShowApi( .let(emitter::onSuccess) } + override suspend fun recent(page: Int): List = allList(page) + + override suspend fun allList(page: Int): List { + return recentPath(page) + .select("div.ddmcc") + .select("li") + .fastMap { + ItemModel( + title = it.select("a").text(), + description = "", + imageUrl = "", + url = it.select("a").attr("abs:href"), + source = this + ) + } + } + override fun getItemInfo(source: ItemModel, doc: Document): Single = Single.create { emitter -> InfoModel( source = this, @@ -141,6 +175,29 @@ object WcoStream : ShowApi( .let(emitter::onSuccess) } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = model.url.toJsoup() + return InfoModel( + source = this, + url = model.url, + title = model.title, + description = doc.select("div.iltext").text(), + imageUrl = doc.select("div#cat-img-desc").select("img").attr("abs:src"), + genres = doc.select("div#cat-genre").select("div.wcobtn").eachText(), + chapters = doc.select("div#catlist-listview").select("ul").select("li") + .fastMap { + ChapterModel( + name = it.select("a").text(), + url = it.select("a").attr("abs:href"), + uploaded = "", + sourceUrl = model.url, + source = this + ) + }, + alternativeNames = emptyList() + ) + } + override fun getSourceByUrl(url: String): Single = Single.create { try { val doc = url.toJsoup() @@ -157,6 +214,17 @@ object WcoStream : ShowApi( } } + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = url.toJsoup() + return ItemModel( + title = doc.select("div#content").select("h2[title]").text(), + description = doc.select("div.iltext").text(), + imageUrl = doc.select("div#cat-img-desc").select("img").attr("abs:src"), + url = url, + source = this + ) + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { emitter -> val f = Jsoup.connect(chapterModel.url) @@ -278,6 +346,105 @@ object WcoStream : ShowApi( emitter.onSuccess(listOf(f)) } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + return Jsoup.connect(chapterModel.url) + .header("X-Requested-With", "XMLHttpRequest") + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win 64; x64; rv:69.0) Gecko/20100101 Firefox/69.0") + .followRedirects(true) + .get() + .let { + val q = StringBuilder() + + it + .select("meta[itemprop=embedURL]") + //.alsoPrint() + .next() + .toString() + .let { + val ending = " - ([0-9]+)".toRegex().find(it)?.groups?.get(1)?.value + "var (.*?) = \\[(.*?)\\];".toRegex().find(it)?.groups?.get(2)?.value + ?.replace("\"", "") + ?.split(",") + ?.fastForEach { + it.trim() + .decodeBase64() + ?.utf8() + //.alsoPrint() + ?.replace("[^0-9]+".toRegex(), "") + //.alsoPrint() + ?.toIntOrNull() + ?.minus(ending?.toIntOrNull() ?: 20945957) + ?.toChar() + //.alsoPrint() + ?.let { q.append(it) } + } + } + //.alsoPrint() + + //println(q) + + val hiddenUrl = Jsoup.parse(q.toString()).select("iframe").attr("src") + //println("$baseUrl$hiddenUrl") + + val q2 = Jsoup.connect("$baseUrl$hiddenUrl").get() + //println(q2) + + val q3 = q2 + .select("div#d-player") + .select("p.text-center") + .select("a") + .attr("href") + //.replace("embed-adh", "embed-adh-html5") + + val d = if (q2.toString().contains("embed-adh-html5")) q3.replace("embed-adh", "embed-adh-html5") else q3 + + val q4 = Jsoup.connect(d) + .followRedirects(true) + .get() + .select("source").attr("abs:src") + + //val u = "get\\(\"(.*?)\"\\)".toRegex() + + //if(u.containsMatchIn(q2.toString())) { + + //println(u) + + /*val g = getJsonApi("$baseUrl${u.find(q2.toString())?.groups?.get(1)?.value}") { + header("Referer", "$baseUrl$hiddenUrl") + header( + "User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36" + ) + header("Accept", "* / *") + header("X-Requested-With", "XMLHttpRequest") + } + + val quality = if (g?.hd?.isNotEmpty() == true) g.hd else g?.enc*/ + + Storage( + link = getApiFinalUrl(q4),//q4,//"${g?.cdn}/getvid?evid=$quality", + source = chapterModel.url, + quality = "Good", + sub = "Yes" + ).apply { + headers["Accept"] = "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5" + /*headers["Host"] = "${g?.cdn}/getvid?evid=$quality" + .split("//") + .lastOrNull() + ?.split("/") + ?.firstOrNull() + ?.split("?") + ?.firstOrNull() + .toString()*/ + headers["User-Agent"] = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36" + headers["X-Requested-With"] = "XMLHttpRequest" + headers["Referer"] = "$baseUrl$hiddenUrl".replace("https://wcostream.com", "https://www.wcostream.com") + headers["Mimetype"] = "video/mp4" + }.let { listOf(it) } + } + } + @WorkerThread private fun getApiFinalUrl(url: String, builder: okhttp3.Request.Builder.() -> Unit = {}): String? { val request = okhttp3.Request.Builder() @@ -329,6 +496,28 @@ object WcoStreamCC : ShowApi( //it.onSuccess(emptyList()) } + override suspend fun recent(page: Int): List { + return getJsonApi("$baseUrl/$allPath")?.html + ?.asJsoup() + ?.select("div.flw-item") + ?.fastMap { + val filmDetail = it.select("> div.film-detail") + val filmPoster = it.select("> div.film-poster") + val nameHeader = filmDetail.select("> h3.film-name > a") + val title = nameHeader.text().replace(" (Dub)", "") + val href = nameHeader.attr("href") + .replace("/watch/", "/anime/") + .replace("-episode-.*".toRegex(), "/") + ItemModel( + title = title, + description = "", + imageUrl = filmPoster.select("> img").attr("data-src"), + url = href, + source = Sources.WCOSTREAMCC + ) + }.orEmpty() + } + override fun getList(page: Int): Single> = Single.create { getJsonApi("$baseUrl/$allPath")?.html?.asJsoup() ?.select("div.flw-item") @@ -352,6 +541,27 @@ object WcoStreamCC : ShowApi( //it.onSuccess(emptyList()) } + override suspend fun allList(page: Int): List { + return getJsonApi("$baseUrl/$allPath")?.html?.asJsoup() + ?.select("div.flw-item") + ?.fastMap { + val filmDetail = it.select("> div.film-detail") + val filmPoster = it.select("> div.film-poster") + val nameHeader = filmDetail.select("> h3.film-name > a") + val title = nameHeader.text().replace(" (Dub)", "") + val href = nameHeader.attr("href") + .replace("/watch/", "/anime/") + .replace("-episode-.*".toRegex(), "/") + ItemModel( + title = title, + description = "", + imageUrl = filmPoster.select("> img").attr("data-src"), + url = href, + source = Sources.WCOSTREAMCC + ) + }.orEmpty() + } + override fun getRecent(doc: Document): Single> = Single.never() override fun getList(doc: Document): Single> = Single.never() @@ -380,6 +590,31 @@ object WcoStreamCC : ShowApi( .let(emitter::onSuccess) } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = model.url.toJsoup() + return InfoModel( + source = Sources.WCOSTREAMCC, + url = model.url, + title = model.title, + description = doc.select(".description > p").text().trim(), + imageUrl = doc.select(".film-poster-img").attr("src"), + genres = doc.select("div.elements div.row > div:nth-child(1) > div.row-line:nth-child(5) > a") + .fastMap { it?.text()?.trim().toString() }, + chapters = doc.select(".tab-content .nav-item > a") + .fastMap { + ChapterModel( + name = it.text(), + url = it.attr("href"), + uploaded = "", + sourceUrl = model.url, + source = Sources.WCOSTREAMCC + ) + } + .reversed(), + alternativeNames = emptyList() + ) + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { emitter -> chapterModel.url.toJsoup() .select("#servers-list > ul > li") @@ -394,6 +629,19 @@ object WcoStreamCC : ShowApi( .let(emitter::onSuccess) } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + return chapterModel.url.toJsoup() + .select("#servers-list > ul > li") + .fastMap { + mapOf( + "link" to it?.selectFirst("a")?.attr("data-embed"), + "title" to it?.selectFirst("span")?.text()?.trim() + ) + } + .fastMap { WcoStreamExtractor.getUrl(it["link"].orEmpty()) } + .flatten() + } + private fun fixAnimeLink(url: String): String { val regex = "watch/([a-zA-Z\\-0-9]*)-episode".toRegex() val (aniId) = regex.find(url)!!.destructured @@ -422,7 +670,29 @@ object WcoStreamCC : ShowApi( ) } .let(emitter::onSuccess) + } + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + val url = "$baseUrl/search" + return Jsoup.connect(url) + .data("keyword", searchText.toString()) + .get() + .select(".film_list-wrap > .flw-item") + .fastMap { + val href = fixAnimeLink(it.select("a").attr("href")) + val img = fixUrl(it.select("img").attr("data-src")) + val title = it.select("img").attr("title") + val year = it.select(".film-detail.film-detail-fix > div > span:nth-child(1)").text().toIntOrNull() + val type = it.select(".film-detail.film-detail-fix > div > span:nth-child(3)").text() + + ItemModel( + title = title, + description = "Year: $year\nType: $type", + imageUrl = img, + url = href, + source = Sources.WCOSTREAMCC + ) + } } override fun getSourceByUrl(url: String): Single = Single.create { emitter -> @@ -437,4 +707,15 @@ object WcoStreamCC : ShowApi( .let(emitter::onSuccess) } + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = url.toJsoup() + return ItemModel( + source = Sources.WCOSTREAMCC, + url = url, + title = doc.select("meta[name=\"title\"]").attr("content").split("| W")[0], + description = doc.select(".description > p").text().trim(), + imageUrl = doc.select(".film-poster-img").attr("src") + ) + } + } \ No newline at end of file diff --git a/animeworld/build.gradle b/animeworld/build.gradle index c7cbf8bab..b38a2b2af 100644 --- a/animeworld/build.gradle +++ b/animeworld/build.gradle @@ -6,7 +6,7 @@ plugins { id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' id 'com.mikepenz.aboutlibraries.plugin' - id "com.starter.easylauncher" version "5.0.1" + id "com.starter.easylauncher" version "5.1.2" } android { diff --git a/build.gradle b/build.gradle index 739ae228a..060b14be7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = "1.7.0" - ext.latestAboutLibsRelease = "10.3.0" + ext.latestAboutLibsRelease = "10.3.1" ext.coroutinesVersion = "1.6.3" @@ -32,7 +32,7 @@ buildscript { ext.constraintlayout = 'androidx.constraintlayout:constraintlayout:2.1.4' ext.swiperefresh = 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - ext.jsoup = 'org.jsoup:jsoup:1.15.1' + ext.jsoup = 'org.jsoup:jsoup:1.15.2' ext.crashlytics = 'com.google.firebase:firebase-crashlytics:18.2.8' ext.analytics = 'com.google.firebase:firebase-analytics:20.1.0' @@ -57,7 +57,7 @@ buildscript { ext.jetpack = "1.3.0-alpha01" ext.jetpackCompiler = "1.2.0" - ext.accompanist = "0.24.12-rc" + ext.accompanist = "0.24.13-rc" ext.composeUi = "androidx.compose.ui:ui:$jetpack" // Tooling support (Previews, etc.) @@ -73,13 +73,13 @@ buildscript { // Integration with activities ext.composeActivity = 'androidx.activity:activity-compose:1.5.0' // Integration with ViewModels - ext.composeLifecycle = 'androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1' + ext.composeLifecycle = 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0' // Integration with observables ext.composeRuntimeLivedata = "androidx.compose.runtime:runtime-livedata:$jetpack" ext.composeRuntimeRxjava2 = "androidx.compose.runtime:runtime-rxjava2:$jetpack" - ext.composeMaterialThemeAdapter = "com.google.android.material:compose-theme-adapter:1.1.13" - ext.composeMaterial3ThemeAdapter = "com.google.android.material:compose-theme-adapter-3:1.0.13" - ext.landscapistGlide = "com.github.skydoves:landscapist-glide:1.5.2" + ext.composeMaterialThemeAdapter = "com.google.android.material:compose-theme-adapter:1.1.14" + ext.composeMaterial3ThemeAdapter = "com.google.android.material:compose-theme-adapter-3:1.0.14" + ext.landscapistGlide = "com.github.skydoves:landscapist-glide:1.5.3" ext.composeConstraintLayout = "androidx.constraintlayout:constraintlayout-compose:1.0.1" ext.composeAnimation = "androidx.compose.animation:animation:$jetpack" ext.materialPlaceholder = "com.google.accompanist:accompanist-placeholder-material:$accompanist" @@ -148,6 +148,17 @@ buildscript { firebaseCrash = [ crash: [crashlytics, analytics] ] datastore = [ datastore: [datastore, datastorePref] ] + + ktorVersion = "1.6.7" + + ktor = [ + ktor: [ + "io.ktor:ktor-client-core:$ktorVersion", + "io.ktor:ktor-client-auth:$ktorVersion", + "io.ktor:ktor-client-android:$ktorVersion", + "io.ktor:ktor-client-logging:$ktorVersion" + ] + ] } repositories { @@ -156,11 +167,11 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' - classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:10.3.0" + classpath 'com.google.gms:google-services:4.3.13' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1' + classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:10.3.1" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0e8b42da8..fb2c97015 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/manga_sources/build.gradle b/manga_sources/build.gradle index 06ba84ecb..788c73e90 100644 --- a/manga_sources/build.gradle +++ b/manga_sources/build.gradle @@ -66,4 +66,6 @@ dependencies { implementation project(':Models') implementation koin.koin + + implementation ktor.ktor } \ No newline at end of file diff --git a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/AsuraScans.kt b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/AsuraScans.kt index 8aae31979..7ae49c12a 100644 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/AsuraScans.kt +++ b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/AsuraScans.kt @@ -40,6 +40,27 @@ object AsuraScans : ApiService, KoinComponent { it.onSuccess(models) } + override suspend fun recent(page: Int): List { + return cloudflare( + helper, + "$baseUrl/manga/?page=$page&order=update", + "referer" to baseUrl, + "user-agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", + ).execute().asJsoup() + .select("div.bs") + .fastMap { + val s = it.select("div.bsx > a") + + ItemModel( + title = s.attr("title"), + description = "", + url = s.attr("abs:href"), + imageUrl = it.select("div.limit img").let { if (it.hasAttr("data-src")) it.attr("abs:data-src") else it.attr("abs:src") }, + source = Sources.ASURA_SCANS + ) + } + } + override fun getList(page: Int): Single> = Single.create { val models = cloudflare( helper, @@ -64,6 +85,28 @@ object AsuraScans : ApiService, KoinComponent { it.onSuccess(models) } + override suspend fun allList(page: Int): List { + return cloudflare( + helper, + "$baseUrl/manga/?page=$page&order=popular", + "referer" to baseUrl, + "user-agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", + ).execute().asJsoup() + .select("div.bs") + .fastMap { + + val s = it.select("div.bsx > a") + + ItemModel( + title = s.attr("title"), + description = "", + url = s.attr("abs:href"), + imageUrl = it.select("div.limit img").let { if (it.hasAttr("data-src")) it.attr("abs:data-src") else it.attr("abs:src") }, + source = Sources.ASURA_SCANS + ) + } + } + override fun searchList(searchText: CharSequence, page: Int, list: List): Single> = try { if (searchText.isBlank()) { super.searchList(searchText, page, list) @@ -111,6 +154,40 @@ object AsuraScans : ApiService, KoinComponent { } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val request = cloudflare( + helper, + model.url, + "referer" to baseUrl, + "user-agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", + ).execute().asJsoup() + + val i = request.select("div.bigcontent, div.animefull, div.main-info") + + val c = request.select("div.bxcl ul li, div.cl ul li, ul li:has(div.chbox):has(div.eph-num)") + .fastMap { + val urlElement = it.select(".lchx > a, span.leftoff a, div.eph-num > a").first()!! + ChapterModel( + url = urlElement.attr("abs:href"), + name = if (urlElement.select("span.chapternum").isNotEmpty()) urlElement.select("span.chapternum").text() else urlElement.text(), + sourceUrl = model.url, + source = Sources.ASURA_SCANS, + uploaded = it.select("span.rightoff, time, span.chapterdate").firstOrNull()?.text().orEmpty() + ) + } + + return InfoModel( + title = model.title, + description = i.select("div.desc p, div.entry-content p").joinToString("\n") { it.text() }, + url = model.url, + imageUrl = model.imageUrl, + chapters = c, + genres = i.select("span:contains(Genre) a, .mgen a").fastMap { element -> element.text() }.distinct(), + alternativeNames = emptyList(), + source = Sources.ASURA_SCANS + ) + } + override fun getSourceByUrl(url: String): Single = Single.create { val request = cloudflare( @@ -134,6 +211,25 @@ object AsuraScans : ApiService, KoinComponent { } + override suspend fun sourceByUrl(url: String): ItemModel { + val request = cloudflare( + helper, + url, + "referer" to baseUrl, + "user-agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", + ).execute().asJsoup() + + val i = request.select("div.bigcontent, div.animefull, div.main-info") + + return ItemModel( + title = i.select("h1.entry-title").text(), + description = i.select("div.desc p, div.entry-content p").joinToString("\n") { it.text() }, + url = url, + imageUrl = i.select("div.thumb img").let { if (it.hasAttr("data-src")) it.attr("abs:data-src") else it.attr("abs:src") }, + source = Sources.ASURA_SCANS + ) + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { val request = cloudflare( @@ -151,4 +247,17 @@ object AsuraScans : ApiService, KoinComponent { } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + return cloudflare( + helper, + chapterModel.url, + "referer" to chapterModel.url, + "user-agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", + ).execute().asJsoup() + .select("div.rdminimal img[loading*=lazy]") + .filterNot { it.attr("abs:src").isNullOrEmpty() } + .fastMap { it.attr("abs:src") } + .fastMap { Storage(link = it, source = chapterModel.url, quality = "Good", sub = "Yes") } + } + } \ No newline at end of file diff --git a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaFourLife.kt b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaFourLife.kt index 00c1887ec..348e3fe45 100644 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaFourLife.kt +++ b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaFourLife.kt @@ -10,6 +10,9 @@ import com.programmersbox.gsonutils.getApi import com.programmersbox.gsonutils.getJsonApi import com.programmersbox.models.* import io.reactivex.Single +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flow import org.jsoup.Jsoup import java.text.SimpleDateFormat import java.util.* @@ -58,6 +61,22 @@ object MangaFourLife : ApiService { } .flatMap { super.searchList(searchText, page, it) } + override fun searchListFlow(searchText: CharSequence, page: Int, list: List): Flow> = flow { + if (mangaList.isEmpty()) { + mangaList.addAll( + ("vm\\.Directory = (.*?.*;)".toRegex() + .find(getApi("https://manga4life.com/search/?sort=lt&desc=true").toString()) + ?.groupValues?.get(1)?.dropLast(1) + ?.fromJson>() + ?.sortedByDescending { m -> m.lt?.let { 1000 * it.toDouble() } } + ?.fastMap(toMangaModel) ?: getApiVersion()) + .orEmpty() + ) + } + emit(mangaList) + } + .flatMapMerge { super.searchListFlow(searchText, page, list) } + override fun getRecent(page: Int): Single> = Single.create { emitter -> try { getManga(page).let(emitter::onSuccess) @@ -66,6 +85,8 @@ object MangaFourLife : ApiService { } } + override suspend fun recent(page: Int): List = getManga(page) + override fun getList(page: Int): Single> = Single.create { emitter -> try { getManga(page).let(emitter::onSuccess) @@ -74,6 +95,8 @@ object MangaFourLife : ApiService { } } + override suspend fun allList(page: Int): List = getManga(page) + private fun getApiVersion() = getJsonApi>("https://manga4life.com/_search.php")?.fastMap { ItemModel( title = it.s.toString(), @@ -137,6 +160,43 @@ object MangaFourLife : ApiService { ) } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = Jsoup.connect(model.url).get() + val description = doc.select("div.BoxBody > div.row").select("div.Content").text() + val genres = "\"genre\":[^:]+(?=,|\$)".toRegex().find(doc.html()) + ?.groupValues?.get(0)?.removePrefix("\"genre\": ")?.fromJson>().orEmpty() + val altNames = "\"alternateName\":[^:]+(?=,|\$)".toRegex().find(doc.html()) + ?.groupValues?.get(0)?.removePrefix("\"alternateName\": ")?.fromJson>().orEmpty() + return InfoModel( + title = model.title, + description = description, + url = model.url, + imageUrl = model.imageUrl, + chapters = "vm.Chapters = (.*?);".toRegex().find(doc.html()) + ?.groupValues?.get(0)?.removePrefix("vm.Chapters = ")?.removeSuffix(";") + ?.fromJson>()?.fastMap { + ChapterModel( + name = chapterImage(it.Chapter!!), + url = "https://manga4life.com/read-online/${ + model.url.split("/") + .last() + }${chapterURLEncode(it.Chapter)}", + uploaded = it.Date.toString(), + sourceUrl = model.url, + source = this + ).apply { + try { + uploadedTime = dateFormat.parse(uploaded.substringBefore(" "))?.time + } catch (_: Exception) { + } + } + }.orEmpty(), + genres = genres, + alternativeNames = altNames, + source = this + ) + } + override fun getSourceByUrl(url: String): Single = Single.create { val doc = Jsoup.connect(url).get() val title = doc.select("li.list-group-item, li.d-none, li.d-sm-block").select("h1").text() @@ -152,6 +212,19 @@ object MangaFourLife : ApiService { ) } + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = Jsoup.connect(url).get() + val title = doc.select("li.list-group-item, li.d-none, li.d-sm-block").select("h1").text() + val description = doc.select("div.BoxBody > div.row").select("div.Content").text() + return ItemModel( + title = title, + description = description, + url = url, + imageUrl = doc.select("img.img-fluid, img.bottom-5").attr("abs:src"), + source = this + ) + } + private fun chapterURLEncode(e: String): String { var index = "" val t = e.substring(0, 1).toInt() @@ -192,6 +265,28 @@ object MangaFourLife : ApiService { .let(emitter::onSuccess) } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + val document = Jsoup.connect(chapterModel.url).get() + val script = document.select("script:containsData(MainFunction)").first()!!.data() + val curChapter = script.substringAfter("vm.CurChapter = ").substringBefore(";").fromJson()!! + + val pageTotal = curChapter["Page"].string.toInt() + + val host = "https://" + script.substringAfter("vm.CurPathName = \"").substringBefore("\"") + val titleURI = script.substringAfter("vm.IndexName = \"").substringBefore("\"") + val seasonURI = curChapter["Directory"].string + .let { if (it.isEmpty()) "" else "$it/" } + val path = "$host/manga/$titleURI/$seasonURI" + + val chNum = chapterImage(curChapter["Chapter"].string) + + return IntRange(1, pageTotal).mapIndexed { i, _ -> + val imageNum = (i + 1).toString().let { "000$it" }.let { it.substring(it.length - 3) } + "$path$chNum-$imageNum.png" + } + .fastMap { Storage(link = it, source = chapterModel.url, quality = "Good", sub = "Yes") } + } + override val canScroll: Boolean = true private data class Life(val i: String?, val s: String?, val a: List?) diff --git a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaHere.kt b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaHere.kt index ec449af89..3da50286c 100644 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaHere.kt +++ b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaHere.kt @@ -37,6 +37,21 @@ object MangaHere : ApiService { .let { emitter.onSuccess(it) } } + override suspend fun recent(page: Int): List { + return Jsoup.connect("$baseUrl/directory/$page.htm?latest") + .header("Referer", baseUrl) + .cookie("isAdult", "1").get() + .select(".manga-list-1-list li").fastMap { + ItemModel( + title = it.select("a").first()!!.attr("title"), + description = "", + url = it.select("a").first()!!.attr("abs:href"), + imageUrl = it.select("img.manga-list-1-cover").first()?.attr("src") ?: "", + source = this + ).apply { extras["Referer"] = baseUrl } + }.filter { it.title.isNotEmpty() } + } + override fun getList(page: Int): Single> = Single.create { emitter -> Jsoup.connect("$baseUrl/directory/$page.htm") .header("Referer", baseUrl) @@ -53,6 +68,21 @@ object MangaHere : ApiService { .let { emitter.onSuccess(it) } } + override suspend fun allList(page: Int): List { + return Jsoup.connect("$baseUrl/directory/$page.htm") + .header("Referer", baseUrl) + .cookie("isAdult", "1").get() + .select(".manga-list-1-list li").fastMap { + ItemModel( + title = it.select("a").first()!!.attr("title"), + description = "", + url = it.select("a").first()!!.attr("abs:href"), + imageUrl = it.select("img.manga-list-1-cover").first()?.attr("src") ?: "", + source = this + ).apply { extras["Referer"] = baseUrl } + }.filter { it.title.isNotEmpty() } + } + override fun searchList(searchText: CharSequence, page: Int, list: List): Single> = try { if (searchText.isBlank()) throw Exception("No search necessary") Single.create { emitter -> @@ -94,6 +124,40 @@ object MangaHere : ApiService { super.searchList(searchText, page, list) } + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + val url = "$baseUrl/search".toHttpUrlOrNull()!!.newBuilder().apply { + addEncodedQueryParameter("page", page.toString()) + addEncodedQueryParameter("title", searchText.toString()) + addEncodedQueryParameter("sort", null) + addEncodedQueryParameter("stype", 1.toString()) + addEncodedQueryParameter("name", null) + addEncodedQueryParameter("author_method", "cw") + addEncodedQueryParameter("author", null) + addEncodedQueryParameter("artist_method", "cw") + addEncodedQueryParameter("artist", null) + addEncodedQueryParameter("rating_method", "eq") + addEncodedQueryParameter("rating", null) + addEncodedQueryParameter("released_method", "eq") + addEncodedQueryParameter("released", null) + }.build() + val request = Request.Builder() + .url(url) + .cacheControl(CacheControl.Builder().maxAge(10, TimeUnit.MINUTES).build()) + .build() + val client = OkHttpClient().newCall(request).execute() + return Jsoup.parse(client.body?.string()).select(".manga-list-4-list > li") + .fastMap { + ItemModel( + title = it.select("a").first()!!.attr("title"), + description = it.select("p.manga-list-4-item-tip").last()!!.text(), + url = "$baseUrl${it.select(".manga-list-4-item-title > a").first()!!.attr("href")}", + imageUrl = it.select("img.manga-list-4-cover").first()!!.attr("abs:src"), + source = this + ).apply { extras["Referer"] = baseUrl } + } + .filter { it.title.isNotEmpty() } + } + override fun getItemInfo(model: ItemModel): Single = Single.create { emitter -> val doc = Jsoup.connect(model.url).header("Referer", baseUrl).get() emitter.onSuccess( @@ -118,6 +182,28 @@ object MangaHere : ApiService { ) } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = Jsoup.connect(model.url).header("Referer", baseUrl).get() + return InfoModel( + title = model.title, + description = doc.select("p.fullcontent").text(), + url = model.url, + imageUrl = doc.select("img.detail-info-cover-img").select("img[src^=http]").attr("abs:src"), + chapters = doc.select("div[id=chapterlist]").select("ul.detail-main-list").select("li").map { + ChapterModel( + name = it.select("a").select("p.title3").text(), + url = it.select("a").attr("abs:href"), + uploaded = it.select("a").select("p.title2").text(), + sourceUrl = model.url, + source = this + ).apply { uploadedTime = parseChapterDate(uploaded) } + }, + genres = doc.select("p.detail-info-right-tag-list").select("a").eachText(), + alternativeNames = emptyList(), + source = this + ).apply { extras["Referer"] = baseUrl } + } + private fun parseChapterDate(date: String): Long { return if ("Today" in date || " ago" in date) { Calendar.getInstance().apply { @@ -159,6 +245,17 @@ object MangaHere : ApiService { } } + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = Jsoup.connect(url).header("Referer", baseUrl).get() + return ItemModel( + title = doc.select("span.detail-info-right-title-font").text(), + description = doc.select("p.fullcontent").text(), + url = url, + imageUrl = doc.select("img.detail-info-cover-img").select("img[src^=http]").attr("abs:src"), + source = this + ) + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { it.onSuccess( pageListParse(Jsoup.connect(chapterModel.url).header("Referer", baseUrl).get()) @@ -170,6 +267,15 @@ object MangaHere : ApiService { ) } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + return pageListParse(Jsoup.connect(chapterModel.url).header("Referer", baseUrl).get()) + .fastMap { p -> + Storage(link = p, source = chapterModel.url, quality = "Good", sub = "Yes").apply { + headers["Referer"] = baseUrl + } + } + } + private fun pageListParse(document: Document): List { val bar = document.select("script[src*=chapter_bar]") val duktape = Duktape.create() diff --git a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaPark.kt b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaPark.kt index 35dc9b4a0..c705ab582 100644 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaPark.kt +++ b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaPark.kt @@ -7,6 +7,8 @@ import com.programmersbox.manga_sources.utilities.* import com.programmersbox.models.* import com.squareup.duktape.Duktape import io.reactivex.Single +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonPrimitive @@ -45,18 +47,31 @@ object MangaPark : ApiService, KoinComponent { super.searchList(searchText, page, list) } + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + return cloudflare(helper, "${baseUrl.v3Url()}/search?word=$searchText&page=$page").execute().asJsoup() + .browseToItemModel("div#search-list div.col") + } + override fun getList(page: Int): Single> = Single.create { emitter -> cloudflare(helper, "${baseUrl.v3Url()}/browse?sort=d007&page=$page").execute().asJsoup() .browseToItemModel() .let { emitter.onSuccess(it) } } + override suspend fun allList(page: Int): List { + return cloudflare(helper, "${baseUrl.v3Url()}/browse?sort=d007&page=$page").execute().asJsoup().browseToItemModel() + } + override fun getRecent(page: Int): Single> = Single.create { emitter -> cloudflare(helper, "${baseUrl.v3Url()}/browse?sort=update&page=$page").execute().asJsoup() .browseToItemModel() .let { emitter.onSuccess(it) } } + override suspend fun recent(page: Int): List { + return cloudflare(helper, "${baseUrl.v3Url()}/browse?sort=update&page=$page").execute().asJsoup().browseToItemModel() + } + private fun Document.browseToItemModel(query: String = "div#subject-list div.col") = select(query) .map { ItemModel( @@ -109,6 +124,43 @@ object MangaPark : ApiService, KoinComponent { } } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = cloudflare(helper, model.url.v3Url()).execute().asJsoup() + return try { + val infoElement = doc.select("div#mainer div.container-fluid") + InfoModel( + title = model.title, + description = model.description, + url = model.url, + imageUrl = model.imageUrl, + chapters = chapterListParse(helper.cloudflareClient.newCall(chapterListRequest(model)).execute(), model.url.v3Url()), + genres = infoElement.select("div.attr-item:contains(genres) span span").fastMap { it.text().trim() }, + alternativeNames = emptyList(), + source = this + ) + } catch (e: Exception) { + e.printStackTrace() + val genres = mutableListOf() + val alternateNames = mutableListOf() + doc.select(".attr > tbody > tr").forEach { + when (it.getElementsByTag("th").first()!!.text().trim().lowercase(Locale.getDefault())) { + "genre(s)" -> genres.addAll(it.getElementsByTag("a").fastMap(Element::text)) + "alternative" -> alternateNames.addAll(it.text().split("l")) + } + } + InfoModel( + title = model.title, + description = doc.select("p.summary").text(), + url = model.url, + imageUrl = model.imageUrl, + chapters = chapterListParse(doc, model.url), + genres = genres, + alternativeNames = alternateNames, + source = this + ) + } + } + private fun chapterListRequest(manga: ItemModel): Request { return GET(manga.url) } @@ -279,6 +331,20 @@ object MangaPark : ApiService, KoinComponent { } } + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = cloudflare(helper, url).execute().asJsoup() + val infoElement = doc.select("div#mainer div.container-fluid") + return ItemModel( + title = infoElement.select("h3.item-title").text(), + description = infoElement.select("div.limit-height-body") + .select("h5.text-muted, div.limit-html") + .joinToString("\n\n", transform = Element::text), + url = url, + imageUrl = infoElement.select("div.detail-set div.attr-cover img").attr("abs:src"), + source = this + ) + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { emitter -> val duktape = Duktape.create() @@ -303,5 +369,27 @@ object MangaPark : ApiService, KoinComponent { .let { emitter.onSuccess(it) } } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + val duktape = Duktape.create() + val script = cloudflare(helper, chapterModel.url).execute().asJsoup().select("script").html() + val imgCdnHost = script.substringAfter("const imgCdnHost = \"").substringBefore("\";") + val imgPathLisRaw = script.substringAfter("const imgPathLis = ").substringBefore(";") + val imgPathLis = Json.parseToJsonElement(imgPathLisRaw).jsonArray + val amPass = script.substringAfter("const amPass = ").substringBefore(";") + val amWord = script.substringAfter("const amWord = ").substringBefore(";") + + val decryptScript = cryptoJS + "CryptoJS.AES.decrypt($amWord, $amPass).toString(CryptoJS.enc.Utf8);" + + val imgWordLisRaw = duktape.evaluate(decryptScript).toString() + val imgWordLis = Json.parseToJsonElement(imgWordLisRaw).jsonArray + + return imgWordLis.mapIndexed { i, imgWordE -> + val imgPath = imgPathLis[i].jsonPrimitive.content + val imgWord = imgWordE.jsonPrimitive.content + "$imgCdnHost$imgPath?$imgWord" + } + .fastMap { Storage(link = it, source = chapterModel.url, quality = "Good", sub = "Yes") } + } + override val canScroll: Boolean = true } \ No newline at end of file diff --git a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/NineAnime.kt b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/NineAnime.kt index 650f349c1..b7dcc3e7f 100644 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/NineAnime.kt +++ b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/NineAnime.kt @@ -3,6 +3,8 @@ package com.programmersbox.manga_sources.manga import androidx.compose.ui.util.fastMap import com.programmersbox.models.* import io.reactivex.Single +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* import org.jsoup.Jsoup import java.text.ParseException import java.text.SimpleDateFormat @@ -19,6 +21,19 @@ object NineAnime : ApiService { "Accept-Language" to "en-US,en;q=0.5" ) + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + return Jsoup.connect("$baseUrl/search/?name=$searchText&page=$page.html").followRedirects(true).get() + .select("div.post").fastMap { + ItemModel( + title = it.select("p.title a").text(), + description = "", + url = it.select("p.title a").attr("abs:href"), + imageUrl = it.select("img").attr("abs:src"), + source = this@NineAnime + ) + } + } + override fun searchList(searchText: CharSequence, page: Int, list: List): Single> = try { if (searchText.isBlank()) throw Exception("No search necessary") Single.create { emitter -> @@ -54,6 +69,19 @@ object NineAnime : ApiService { ) } + override suspend fun allList(page: Int): List { + return Jsoup.connect("$baseUrl/category/index_$page.html").followRedirects(true).get() + .select("div.post").fastMap { + ItemModel( + title = it.select("p.title a").text(), + description = "", + url = it.select("p.title a").attr("abs:href"), + imageUrl = it.select("img").attr("abs:src"), + source = this@NineAnime + ) + } + } + override fun getItemInfo(model: ItemModel): Single = Single.create { emitter -> val doc = try { Jsoup.connect("${model.url}?waring=1").followRedirects(true).get() @@ -85,6 +113,30 @@ object NineAnime : ApiService { ) } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = Jsoup.connect("${model.url}?waring=1").followRedirects(true).get() + val genreAndDescription = doc.select("div.manga-detailmiddle") + return InfoModel( + title = model.title, + description = genreAndDescription.select("p.mobile-none").text(), + url = model.url, + imageUrl = model.imageUrl, + chapters = doc.select("ul.detail-chlist li").fastMap { + ChapterModel( + name = it.select("a").select("span").firstOrNull()?.text() ?: it.text() ?: it.select("a").text(), + url = it.select("a").attr("abs:href"), + uploaded = it.select("span.time").text(), + sourceUrl = model.url, + source = this@NineAnime + ).apply { uploadedTime = uploaded.toDate() } + }, + genres = genreAndDescription.select("p:has(span:contains(Genre)) a").fastMap { it.text() }, + alternativeNames = doc.select("div.detail-info").select("p:has(span:contains(Alternative))").text() + .removePrefix("Alternative(s):").split(";"), + source = this@NineAnime + ) + } + override fun getSourceByUrl(url: String): Single = Single.create { try { val doc = Jsoup.connect(url).get() @@ -103,6 +155,36 @@ object NineAnime : ApiService { } } + override fun getSourceByUrlFlow(url: String): Flow = flow { + val doc = Jsoup.connect(url).get() + val genreAndDescription = doc.select("div.manga-detailmiddle") + emit( + ItemModel( + title = doc.select("div.manga-detail > h1").select("h1").text(), + description = genreAndDescription.select("p.mobile-none").text(), + url = url, + imageUrl = doc.select("img.detail-cover").attr("abs:src"), + source = this@NineAnime + ) + ) + } + .catch { + it.printStackTrace() + emitAll(super.getSourceByUrlFlow(url)) + } + + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = Jsoup.connect(url).get() + val genreAndDescription = doc.select("div.manga-detailmiddle") + return ItemModel( + title = doc.select("div.manga-detail > h1").select("h1").text(), + description = genreAndDescription.select("p.mobile-none").text(), + url = url, + imageUrl = doc.select("img.detail-cover").attr("abs:src"), + source = this@NineAnime + ) + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { emitter -> try { val doc = Jsoup.connect(chapterModel.url) @@ -118,6 +200,15 @@ object NineAnime : ApiService { } } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + val doc = Jsoup.connect(chapterModel.url) + .header("Referer", "$baseUrl/manga/").followRedirects(true).get() + val script = doc.select("script:containsData(all_imgs_url)").firstOrNull()?.data() ?: throw Exception("all_imgsurl not found") + return Regex(""""(http.*)",""").findAll(script).map { it.groupValues[1] } + .map { Storage(link = it, source = chapterModel.url, quality = "Good", sub = "Yes") } + .toList() + } + /*override fun getPageInfo(chapterModel: ChapterModel): PageModel { val doc = Jsoup.connect(chapterModel.url).header("Referer", "$baseUrl/manga.").followRedirects(true).get() val script = doc.select("script:containsData(all_imgs_url)").firstOrNull()?.data() ?: return PageModel(emptyList()) @@ -141,6 +232,19 @@ object NineAnime : ApiService { ) } + override suspend fun recent(page: Int): List { + return Jsoup.connect("$baseUrl/category/index_$page.html?sort=updated").followRedirects(true).get() + .select("div.post").fastMap { + ItemModel( + title = it.select("p.title a").text(), + description = "", + url = it.select("p.title a").attr("abs:href"), + imageUrl = it.select("img").attr("abs:src"), + source = this@NineAnime + ) + } + } + /*private fun getUrlWithoutDomain(orig: String): String { return try { val uri = URI(orig) diff --git a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/Tsumino.kt b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/Tsumino.kt index a7c44a9f4..6050be199 100644 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/Tsumino.kt +++ b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/Tsumino.kt @@ -45,6 +45,27 @@ object Tsumino : ApiService { super.searchList(searchText, page, list) } + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + val body = FormBody.Builder() + .add("PageNumber", page.toString()) + .add("Text", searchText.toString()) + .add("Sort", "Newest") + .add("List", "0") + .add("Length", "0") + .build() + return getJsonApiPost("$baseUrl/Search/Operate/", body) + ?.data + ?.fastMap { + ItemModel( + title = it.entry?.title.toString(), + description = "${it.entry?.duration}", + url = it.entry?.id.toString(), + imageUrl = it.entry?.thumbnailUrl ?: it.entry?.thumbnailTemplateUrl ?: "", + source = Tsumino + ) + }.orEmpty() + } + override fun getRecent(page: Int): Single> = Single.create { emitter -> getJsonApi("$baseUrl/Search/Operate/?PageNumber=$page&Sort=Newest") ?.data @@ -60,6 +81,20 @@ object Tsumino : ApiService { .let(emitter::onSuccess) } + override suspend fun recent(page: Int): List { + return getJsonApi("$baseUrl/Search/Operate/?PageNumber=$page&Sort=Newest") + ?.data + ?.fastMap { + ItemModel( + title = it.entry?.title.toString(), + description = "${it.entry?.duration}", + url = it.entry?.id.toString(), + imageUrl = it.entry?.thumbnailUrl ?: it.entry?.thumbnailTemplateUrl ?: "", + source = Tsumino + ) + }.orEmpty() + } + override fun getList(page: Int): Single> = Single.create { emitter -> getJsonApi("$baseUrl/Search/Operate/?PageNumber=$page&Sort=Popularity") ?.data @@ -75,6 +110,20 @@ object Tsumino : ApiService { .let(emitter::onSuccess) } + override suspend fun allList(page: Int): List { + return getJsonApi("$baseUrl/Search/Operate/?PageNumber=$page&Sort=Popularity") + ?.data + ?.fastMap { + ItemModel( + title = it.entry?.title.toString(), + description = "${it.entry?.duration}", + url = it.entry?.id.toString(), + imageUrl = it.entry?.thumbnailUrl ?: it.entry?.thumbnailTemplateUrl ?: "", + source = Tsumino + ) + }.orEmpty() + } + override fun getItemInfo(model: ItemModel): Single = Single.create { val doc = Jsoup.connect("$baseUrl/entry/${model.url}").get() it.onSuccess( @@ -99,6 +148,28 @@ object Tsumino : ApiService { ) } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = Jsoup.connect("$baseUrl/entry/${model.url}").get() + return InfoModel( + title = model.title, + description = getDesc(doc), + url = "$baseUrl/entry/${model.url}", + imageUrl = model.imageUrl, + chapters = listOf( + ChapterModel( + url = model.url, + name = doc.select("#Pages").text(), + uploaded = "", + sourceUrl = model.url, + source = Tsumino + ) + ), + genres = doc.select("#Tag a").eachText(), + alternativeNames = emptyList(), + source = Tsumino + ) + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { emitter -> chapterModel.name.toIntOrNull()?.let { 1..it } ?.map { "https://content.tsumino.com/thumbs/${chapterModel.url}/$it" } @@ -107,6 +178,13 @@ object Tsumino : ApiService { .let(emitter::onSuccess) } + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + return chapterModel.name.toIntOrNull()?.let { 1..it } + ?.map { "https://content.tsumino.com/thumbs/${chapterModel.url}/$it" } + .orEmpty() + .fastMap { Storage(link = it, source = chapterModel.url, quality = "Good", sub = "Yes") } + } + private fun getDesc(document: Document): String { val stringBuilder = StringBuilder() val parodies = document.select("#Parody a") diff --git a/mangaworld/build.gradle b/mangaworld/build.gradle index ddce667fb..0b14e38fd 100644 --- a/mangaworld/build.gradle +++ b/mangaworld/build.gradle @@ -6,7 +6,7 @@ plugins { id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' id 'com.mikepenz.aboutlibraries.plugin' - id "com.starter.easylauncher" version "5.0.1" + id "com.starter.easylauncher" version "5.1.2" } android { diff --git a/mangaworld/src/main/java/com/programmersbox/mangaworld/ContextUtils.kt b/mangaworld/src/main/java/com/programmersbox/mangaworld/ContextUtils.kt index ddd2bf3c7..83134ba07 100644 --- a/mangaworld/src/main/java/com/programmersbox/mangaworld/ContextUtils.kt +++ b/mangaworld/src/main/java/com/programmersbox/mangaworld/ContextUtils.kt @@ -222,6 +222,9 @@ class ChaptersGet private constructor(private val chaptersContex: Context) { val PAGE_PADDING = intPreferencesKey("page_padding") val Context.pagePadding get() = dataStore.data.map { it[PAGE_PADDING] ?: 4 } +val LIST_OR_PAGER = booleanPreferencesKey("list_or_padding") +val Context.listOrPager get() = dataStore.data.map { it[LIST_OR_PAGER] ?: true } + val showOrHideNav = BehaviorSubject.createDefault(true) /** diff --git a/mangaworld/src/main/java/com/programmersbox/mangaworld/ReadActivity.kt b/mangaworld/src/main/java/com/programmersbox/mangaworld/ReadActivity.kt index 43478f715..d42da2490 100644 --- a/mangaworld/src/main/java/com/programmersbox/mangaworld/ReadActivity.kt +++ b/mangaworld/src/main/java/com/programmersbox/mangaworld/ReadActivity.kt @@ -29,17 +29,17 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.BottomSheetScaffold import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* -import androidx.compose.material.rememberBottomSheetScaffoldState +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset @@ -48,18 +48,15 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import androidx.compose.ui.util.fastMap @@ -80,6 +77,10 @@ import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade import com.bumptech.glide.util.ViewPreloadSizeProvider import com.github.piasy.biv.BigImageViewer +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.PagerState +import com.google.accompanist.pager.VerticalPager +import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.android.gms.ads.AdRequest @@ -114,9 +115,7 @@ import io.reactivex.rxkotlin.addTo import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import org.koin.android.ext.android.inject import java.io.File import kotlin.math.roundToInt @@ -128,21 +127,21 @@ class ReadViewModel( context: Context, val genericInfo: GenericInfo, val headers: MutableMap = mutableMapOf(), - model: Single>? = handle + model: Flow>? = handle .get("currentChapter") ?.fromJson(ChapterModel::class.java to ChapterModelDeserializer(genericInfo)) - ?.getChapterInfo() + ?.getChapterInfoFlow() ?.map { headers.putAll(it.flatMap { it.headers.toList() }) it.mapNotNull(Storage::link) - } - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.doOnError { Toast.makeText(context, it.localizedMessage, Toast.LENGTH_SHORT).show() }, + }, + /*?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread())*/ + //?.doOnError { Toast.makeText(context, it.localizedMessage, Toast.LENGTH_SHORT).show() }, isDownloaded: Boolean = handle.get("downloaded")?.toBooleanStrict() ?: false, filePath: File? = handle.get("filePath")?.let { File(it) }, - modelPath: Single>? = if (isDownloaded && filePath != null) { - Single.create> { + modelPath: Flow>? = if (isDownloaded && filePath != null) { + /*Single.create> { filePath .listFiles() ?.sortedBy { f -> f.name.split(".").first().toInt() } @@ -151,7 +150,17 @@ class ReadViewModel( ?.let(it::onSuccess) ?: it.onError(Throwable("Cannot find files")) } .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + .observeOn(AndroidSchedulers.mainThread())*/ + flow> { + filePath + .listFiles() + ?.sortedBy { f -> f.name.split(".").first().toInt() } + ?.fastMap(File::toUri) + ?.fastMap(Uri::toString) + ?.let { emit(it) } ?: Throwable("Cannot find files") + } + .catch { emit(emptyList()) } + .flowOn(Dispatchers.IO) } else { model }, @@ -188,12 +197,7 @@ class ReadViewModel( val disposable = CompositeDisposable() - val list by lazy { - handle.get("allChapters") - ?.fromJson>(ChapterModel::class.java to ChapterModelDeserializer(genericInfo)) - .orEmpty() - .also(::println) - } + var list by mutableStateOf>(emptyList()) private val mangaUrl by lazy { handle.get("mangaInfoUrl") ?: "" } @@ -210,16 +214,27 @@ class ReadViewModel( private set init { - batteryInformation.composeSetup( - disposable, - androidx.compose.ui.graphics.Color.White - ) { - batteryColor = it.first - batteryIcon = it.second + viewModelScope.launch(Dispatchers.IO) { + batteryInformation.composeSetupFlow( + androidx.compose.ui.graphics.Color.White + ) { + batteryColor = it.first + batteryIcon = it.second + } } - val url = handle.get("mangaUrl") ?: "" - currentChapter = list.indexOfFirst { l -> l.url == url } + viewModelScope.launch(Dispatchers.IO) { + val url = handle.get("mangaUrl") ?: "" + + handle.getStateFlow("allChapters", "") + .map { it.fromJson>(ChapterModel::class.java to ChapterModelDeserializer(genericInfo)).orEmpty() } + .onEach { + list = it + currentChapter = it.indexOfFirst { l -> l.url == url } + } + .flowOn(Dispatchers.Main) + .collect() + } loadPages(modelPath) } @@ -242,40 +257,36 @@ class ReadViewModel( .addTo(disposable) item - .getChapterInfo() + .getChapterInfoFlow() .map { it.mapNotNull(Storage::link) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe { pageList.clear() } - .subscribeBy { pages: List -> pageList.addAll(pages) } - .addTo(disposable) + .let { loadPages(it) } } } - private fun loadPages(modelPath: Single>?) { - modelPath - ?.doOnSubscribe { - isLoadingPages.value = true - pageList.clear() - } - ?.subscribeBy { - pageList.addAll(it) - isLoadingPages.value = false - } - ?.addTo(disposable) + private fun loadPages(modelPath: Flow>?) { + viewModelScope.launch { + modelPath + ?.onStart { + isLoadingPages.value = true + pageList.clear() + } + ?.onEach { + pageList.addAll(it) + isLoadingPages.value = false + } + ?.collect() + } } fun refresh() { headers.clear() loadPages( list.getOrNull(currentChapter) - ?.getChapterInfo() + ?.getChapterInfoFlow() ?.map { headers.putAll(it.flatMap { it.headers.toList() }) it.mapNotNull(Storage::link) } - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) ) } @@ -286,6 +297,7 @@ class ReadViewModel( } +@OptIn(ExperimentalPagerApi::class) @ExperimentalMaterial3Api @ExperimentalMaterialApi @ExperimentalComposeUiApi @@ -316,8 +328,8 @@ fun ReadView() { DisposableEffect(LocalContext.current) { val batteryInfo = context.battery { readVm.batteryPercent = it.percent - readVm.batteryInformation.batteryLevelAlert(it.percent) - readVm.batteryInformation.batteryInfoItem(it) + readVm.batteryInformation.batteryLevel.tryEmit(it.percent) + readVm.batteryInformation.batteryInfo.tryEmit(it) } onDispose { context.unregisterReceiver(batteryInfo) } } @@ -329,8 +341,11 @@ fun ReadView() { LaunchedEffect(readVm.pageList) { BigImageViewer.prefetch(*readVm.pageList.fastMap(Uri::parse).toTypedArray()) } + val listOrPager by context.listOrPager.collectAsState(initial = true) + + val pagerState = rememberPagerState() val listState = rememberLazyListState() - val currentPage by remember { derivedStateOf { listState.firstVisibleItemIndex } } + val currentPage by remember { derivedStateOf { if (listOrPager) listState.firstVisibleItemIndex else pagerState.currentPage } } val paddingPage by context.pagePadding.collectAsState(initial = 4) var settingsPopup by remember { mutableStateOf(false) } @@ -344,7 +359,7 @@ fun ReadView() { onDismissRequest = { settingsPopup = false }, title = { Text(stringResource(R.string.settings)) }, text = { - Column { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { SliderSetting( scope = scope, settingIcon = Icons.Default.BatteryAlert, @@ -364,6 +379,15 @@ fun ReadView() { initialValue = runBlocking { context.dataStore.data.first()[PAGE_PADDING] ?: 4 }, range = 0f..10f ) + Divider() + val activity = LocalActivity.current + SwitchSetting( + settingTitle = { Text(stringResource(R.string.list_or_pager_title)) }, + summaryValue = { Text(stringResource(R.string.list_or_pager_description)) }, + value = listOrPager, + updateValue = { scope.launch { activity.updatePref(LIST_OR_PAGER, it) } }, + settingIcon = { Icon(Icons.Default.Pages, null) } + ) } }, confirmButton = { TextButton(onClick = { settingsPopup = false }) { Text(stringResource(R.string.ok)) } } @@ -376,19 +400,28 @@ fun ReadView() { activity.runOnUiThread { Toast.makeText(context, R.string.addedChapterItem, Toast.LENGTH_SHORT).show() } } - val scaffoldState = rememberBottomSheetScaffoldState() + val listShowItems = (listState.isScrolledToTheEnd() || listState.isScrolledToTheBeginning()) && listOrPager + val pagerShowItems = (pagerState.currentPage == 0 || pagerState.currentPage >= pages.size) && !listOrPager - BackHandler(scaffoldState.bottomSheetState.isExpanded || scaffoldState.drawerState.isOpen) { + val showItems = readVm.showInfo || listShowItems || pagerShowItems + + val topAppBarScrollState = rememberTopAppBarScrollState() + val scrollBehavior = remember { TopAppBarDefaults.pinnedScrollBehavior(topAppBarScrollState) } + + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) + + BackHandler(drawerState.isOpen || sheetState.isVisible) { scope.launch { when { - scaffoldState.bottomSheetState.isExpanded -> scaffoldState.bottomSheetState.collapse() - scaffoldState.drawerState.isOpen -> scaffoldState.drawerState.close() + drawerState.isOpen -> drawerState.close() + sheetState.isVisible -> sheetState.hide() } } } - BottomSheetScaffold( - scaffoldState = scaffoldState, + ModalBottomSheetLayout( + sheetState = sheetState, sheetContent = { val sheetTopAppBarScrollState = rememberTopAppBarScrollState() val sheetScrollBehavior = remember { TopAppBarDefaults.pinnedScrollBehavior(sheetTopAppBarScrollState) } @@ -400,7 +433,7 @@ fun ReadView() { title = { Text(readVm.list.getOrNull(readVm.currentChapter)?.name.orEmpty()) }, actions = { PageIndicator(Modifier, currentPage + 1, pages.size) }, navigationIcon = { - IconButton(onClick = { scope.launch { scaffoldState.bottomSheetState.collapse() } }) { + IconButton(onClick = { scope.launch { sheetState.hide() } }) { Icon(Icons.Default.Close, null) } } @@ -423,7 +456,7 @@ fun ReadView() { } } ) { p -> - if (scaffoldState.bottomSheetState.isExpanded) { + if (sheetState.isVisible) { LazyVerticalGrid( columns = adaptiveGridCell(), contentPadding = p, @@ -444,8 +477,8 @@ fun ReadView() { ) .clickable { scope.launch { - if (currentPage == i) scaffoldState.bottomSheetState.collapse() - listState.animateScrollToItem(i) + if (currentPage == i) sheetState.hide() + if (listOrPager) listState.animateScrollToItem(i) else pagerState.animateScrollToPage(i) } } ) { @@ -491,10 +524,10 @@ fun ReadView() { } } }, - sheetGesturesEnabled = false, - sheetPeekHeight = 0.dp, - drawerContent = if (readVm.list.size > 1) { - { + ) { + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { val drawerTopAppBarScrollState = rememberTopAppBarScrollState() val drawerScrollBehavior = remember { TopAppBarDefaults.pinnedScrollBehavior(drawerTopAppBarScrollState) } Scaffold( @@ -523,7 +556,7 @@ fun ReadView() { } } ) { p -> - if (scaffoldState.drawerState.isOpen) { + if (drawerState.isOpen) { LazyColumn( state = rememberLazyListState(readVm.currentChapter.coerceIn(0, readVm.list.lastIndex)), contentPadding = p, @@ -567,129 +600,121 @@ fun ReadView() { } } } - } - } else null - ) { - - val showItems = readVm.showInfo || listState.isScrolledToTheEnd() - - /*val scrollBehavior = remember { TopAppBarDefaults.enterAlwaysScrollBehavior() }*/ - //val currentOffset = animateFloatAsState(targetValue = if(showInfo) 0f else scrollBehavior.offsetLimit) - //if(showInfo) scrollBehavior.offset = currentOffset.value// else scrollBehavior.offset = currentOffset.value - - val topAppBarScrollState = rememberTopAppBarScrollState() - val scrollBehavior = remember { TopAppBarDefaults.pinnedScrollBehavior(topAppBarScrollState) } - - val topBarHeight = 32.dp//28.dp - val topBarHeightPx = with(LocalDensity.current) { topBarHeight.roundToPx().toFloat() } - val topBarOffsetHeightPx = remember { mutableStateOf(0f) } - - val toolbarHeight = 64.dp - val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() } - val toolbarOffsetHeightPx = remember { mutableStateOf(0f) } - - val nestedScrollConnection = remember { - object : NestedScrollConnection { - //by scrollBehavior.nestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.y - - val newTopOffset = topBarOffsetHeightPx.value + delta - if (topBarOffsetHeightPx.value != newTopOffset) - topBarOffsetHeightPx.value = newTopOffset.coerceIn(-topBarHeightPx, 0f) - - val newOffset = toolbarOffsetHeightPx.value + delta - if (toolbarOffsetHeightPx.value != newOffset) - toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f) - - return scrollBehavior.nestedScrollConnection.onPreScroll(available, source)//Offset.Zero - } - } - } - - Scaffold( - //TODO: This stuff will be used again once we find a way to keep the top and bottom bars out when reaching the bottom - // and animating the top and bottom bars away - modifier = Modifier.nestedScroll(nestedScrollConnection), - /*topBar = { - TopBar( - scrollBehavior = scrollBehavior, - modifier = Modifier - .height(topBarHeight) - .align(Alignment.TopCenter) - .alpha(animateTopBar), - pages = pages, - currentPage = currentPage - ) }, - bottomBar = { - BottomBar( - modifier = Modifier - .height(toolbarHeight) - .align(Alignment.BottomCenter) - .alpha(animateTopBar), - scrollBehavior = scrollBehavior, - onPageSelectClick = { scope.launch { scaffoldState.bottomSheetState.expand() } }, - onSettingsClick = { settingsPopup = true }, - chapterChange = ::showToast - ) - }*/ - ) { p -> - //TODO: If/when swipe refresh gains a swipe up to refresh, make the swipe up go to the next chapter - SwipeRefresh( - state = swipeState, - onRefresh = { readVm.refresh() }, - modifier = Modifier.padding(p) - ) { - Box(Modifier.fillMaxSize()) { - LazyColumn( - state = listState, - verticalArrangement = Arrangement.spacedBy(LocalContext.current.dpToPx(paddingPage).dp), - contentPadding = PaddingValues( - top = topBarHeight, - bottom = toolbarHeight + gesturesEnabled = readVm.list.size > 1 + ) { + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + AnimatedVisibility( + visible = showItems, + enter = slideInVertically() + fadeIn(), + exit = slideOutVertically() + fadeOut() + ) { + TopBar( + scrollBehavior = scrollBehavior, + pages = pages, + currentPage = currentPage, + vm = readVm ) + } + }, + bottomBar = { + AnimatedVisibility( + visible = showItems, + enter = slideInVertically { it / 2 } + fadeIn(), + exit = slideOutVertically { it / 2 } + fadeOut() ) { - reader(pages, readVm) { - readVm.showInfo = !readVm.showInfo - if (!readVm.showInfo) { - toolbarOffsetHeightPx.value = -toolbarHeightPx - topBarOffsetHeightPx.value = -topBarHeightPx - } - } + BottomBar( + scrollBehavior = scrollBehavior, + onPageSelectClick = { scope.launch { sheetState.show() } }, + onSettingsClick = { settingsPopup = true }, + chapterChange = ::showToast, + vm = readVm + ) + } + } + ) { _ -> + SwipeRefresh( + state = swipeState, + onRefresh = { readVm.refresh() }, + indicatorPadding = PaddingValues(top = 64.dp) + ) { + val padding = PaddingValues(top = 64.dp, bottom = 80.dp) + val spacing = LocalContext.current.dpToPx(paddingPage).dp + Crossfade(targetState = listOrPager) { + if (it) ListView(listState, padding, pages, readVm, spacing) { readVm.showInfo = !readVm.showInfo } + else PagerView(pagerState, padding, pages, readVm, spacing) { readVm.showInfo = !readVm.showInfo } } + } + } + } + } +} - val animateTopBar by animateFloatAsState( - if (showItems) 1f - else 1f - (-topBarOffsetHeightPx.value.roundToInt() / topBarHeightPx) - ) +@Composable +fun ListView( + listState: LazyListState, + contentPadding: PaddingValues, + pages: List, + readVm: ReadViewModel, + itemSpacing: Dp, + onClick: () -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState, + verticalArrangement = Arrangement.spacedBy(itemSpacing), + contentPadding = contentPadding + ) { reader(pages, readVm, onClick) } +} - TopBar( - scrollBehavior = scrollBehavior, - modifier = Modifier - .height(topBarHeight) - .align(Alignment.TopCenter) - .alpha(animateTopBar) - .offset { IntOffset(x = 0, y = if (showItems) 0 else (topBarOffsetHeightPx.value.roundToInt())) }, - pages = pages, - currentPage = currentPage, - vm = readVm - ) +@OptIn(ExperimentalPagerApi::class) +@Composable +fun PagerView(pagerState: PagerState, contentPadding: PaddingValues, pages: List, vm: ReadViewModel, itemSpacing: Dp, onClick: () -> Unit) { + VerticalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + count = pages.size + 1, + itemSpacing = itemSpacing, + contentPadding = contentPadding, + key = { it } + ) { page -> pages.getOrNull(page)?.let { ChapterPage(it, onClick, vm.headers) } ?: LastPageReached(vm = vm) } +} - BottomBar( - modifier = Modifier - .height(toolbarHeight) - .align(Alignment.BottomCenter) - .alpha(animateTopBar) - .offset { IntOffset(x = 0, y = if (showItems) 0 else (-toolbarOffsetHeightPx.value.roundToInt())) }, - scrollBehavior = scrollBehavior, - onPageSelectClick = { scope.launch { scaffoldState.bottomSheetState.expand() } }, - onSettingsClick = { settingsPopup = true }, - chapterChange = ::showToast, - vm = readVm - ) +@Composable +private fun LastPageReached(vm: ReadViewModel) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + stringResource(id = R.string.lastPage), + style = M3MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + ) + if (vm.currentChapter <= 0) { + Text( + stringResource(id = R.string.reachedLastChapter), + style = M3MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + ) + } + if (BuildConfig.BUILD_TYPE == "release") { + val context = LocalContext.current + AndroidView( + modifier = Modifier.fillMaxWidth(), + factory = { + AdView(it).apply { + setAdSize(AdSize.BANNER) + adUnitId = context.getString(R.string.ad_unit_id) + loadAd(vm.ad) + } } - } + ) } } } @@ -794,42 +819,7 @@ private fun LazyListScope.reader(pages: List, vm: ReadViewModel, onClick }*/ items(pages, key = { it }, contentType = { it }) { ChapterPage(it, onClick, vm.headers) } - - item { - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - stringResource(id = R.string.lastPage), - style = M3MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally) - ) - if (vm.currentChapter <= 0) { - Text( - stringResource(id = R.string.reachedLastChapter), - style = M3MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally) - ) - } - if (BuildConfig.BUILD_TYPE == "release") { - val context = LocalContext.current - AndroidView( - modifier = Modifier.fillMaxWidth(), - factory = { - AdView(it).apply { - setAdSize(AdSize.BANNER) - adUnitId = context.getString(R.string.ad_unit_id) - loadAd(vm.ad) - } - } - ) - } - } - } + item { LastPageReached(vm = vm) } } @Composable @@ -907,13 +897,11 @@ private fun ZoomableImage( val scope = rememberCoroutineScope() var showTheThing by remember { mutableStateOf(true) } - if (showTheThing) + if (showTheThing) { GlideImage( imageModel = remember(painter) { GlideUrl(painter) { headers } }, contentScale = ContentScale.FillWidth, - loading = { - androidx.compose.material3.CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) - }, + loading = { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) }, failure = { Text( stringResource(R.string.pressToRefresh), @@ -941,6 +929,7 @@ private fun ZoomableImage( scaleY = scaleAnim } ) + } } } @@ -1159,6 +1148,7 @@ private fun PageIndicator(modifier: Modifier = Modifier, currentPage: Int, pageC private fun Context.dpToPx(dp: Int): Int = (dp * resources.displayMetrics.density).toInt() private fun LazyListState.isScrolledToTheEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 +private fun LazyListState.isScrolledToTheBeginning() = layoutInfo.visibleItemsInfo.firstOrNull()?.index == 0 @Composable private fun GoBackButton(modifier: Modifier = Modifier) { diff --git a/novel_sources/src/main/java/com/programmersbox/novel_sources/novels/WuxiaWorld.kt b/novel_sources/src/main/java/com/programmersbox/novel_sources/novels/WuxiaWorld.kt index aa24f5ce7..f237e4339 100644 --- a/novel_sources/src/main/java/com/programmersbox/novel_sources/novels/WuxiaWorld.kt +++ b/novel_sources/src/main/java/com/programmersbox/novel_sources/novels/WuxiaWorld.kt @@ -37,6 +37,20 @@ object WuxiaWorld : ApiService { super.searchList(searchText, page, list) } + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + return Jsoup.connect("$baseUrl/search.ajax?type=&query=$searchText").followRedirects(true).post() + //.also { println(it) } + .select("li.option").fastMap { + ItemModel( + title = it.select("a").text(), + description = "", + url = it.select("a").attr("abs:href"), + imageUrl = it.select("img").attr("abs:src"), + source = this + ) + } + } + override fun getRecent(page: Int): Single> = Single.create { val pop = "/wuxia-list?view=list&page=$page" "$baseUrl$pop".toJsoup() @@ -59,6 +73,27 @@ object WuxiaWorld : ApiService { .let(it::onSuccess) } + override suspend fun recent(page: Int): List { + val pop = "/wuxia-list?view=list&page=$page" + return "$baseUrl$pop".toJsoup() + .select("div.update_item") + .fastMap { + ItemModel( + title = it + .select("h3") + .select("a.tooltip") + .attr("title"), + description = "", + imageUrl = it.select("img").attr("abs:src"), + url = it + .select("h3") + .select("a.tooltip") + .attr("abs:href"), + source = Sources.WUXIAWORLD + ) + } + } + override fun getList(page: Int): Single> = Single.create { val pop = "/wuxia-list?view=list&sort=popularity&page=$page" "$baseUrl$pop".toJsoup() @@ -81,6 +116,27 @@ object WuxiaWorld : ApiService { .let(it::onSuccess) } + override suspend fun allList(page: Int): List { + val pop = "/wuxia-list?view=list&sort=popularity&page=$page" + return "$baseUrl$pop".toJsoup() + .select("div.update_item") + .fastMap { + ItemModel( + title = it + .select("h3") + .select("a.tooltip") + .attr("title"), + description = "", + imageUrl = it.select("img").attr("abs:src"), + url = it + .select("h3") + .select("a.tooltip") + .attr("abs:href"), + source = Sources.WUXIAWORLD + ) + } + } + override fun getItemInfo(model: ItemModel): Single = Single.create { val info = model.url.toJsoup() @@ -112,6 +168,34 @@ object WuxiaWorld : ApiService { } + override suspend fun itemInfo(model: ItemModel): InfoModel { + val info = model.url.toJsoup() + + return InfoModel( + source = Sources.WUXIAWORLD, + url = model.url, + title = model.title, + description = info.select("meta[name='description']").attr("content"), + imageUrl = model.imageUrl, + genres = emptyList(), + chapters = info + .select("div.chapter-list") + .select("div.row") + .select("span") + .select("a") + .fastMap { + ChapterModel( + name = it.attr("title"), + url = it.attr("abs:href"), + uploaded = "", + sourceUrl = model.url, + source = Sources.WUXIAWORLD + ) + }, + alternativeNames = emptyList() + ) + } + override fun getSourceByUrl(url: String): Single = Single.create { try { val doc = url.toJsoup() @@ -129,6 +213,17 @@ object WuxiaWorld : ApiService { } } + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = url.toJsoup() + return ItemModel( + title = doc.title(), + description = doc.select("meta[name='description']").attr("content"), + imageUrl = doc.select("link[rel='image_src']").attr("href"), + url = url, + source = Sources.WUXIAWORLD + ) + } + override fun getChapterInfo(chapterModel: ChapterModel): Single> = Single.create { it.onSuccess( listOf( @@ -141,4 +236,15 @@ object WuxiaWorld : ApiService { ) ) } + + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + return listOf( + Storage( + link = chapterModel.url.toJsoup().select("div.content-area").html(), + source = chapterModel.url, + quality = "Good", + sub = "Yes" + ) + ) + } } \ No newline at end of file diff --git a/novelworld/build.gradle b/novelworld/build.gradle index ec41bd38d..f8a5e9670 100644 --- a/novelworld/build.gradle +++ b/novelworld/build.gradle @@ -6,7 +6,7 @@ plugins { id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' id 'com.mikepenz.aboutlibraries.plugin' - id "com.starter.easylauncher" version "5.0.1" + id "com.starter.easylauncher" version "5.1.2" } android {