diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 8119f4960..a8d14217a 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,6 +1,7 @@ + diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 26ed64364..c59ef7c3f 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -5,12 +5,30 @@ + + + + + + + + + + + + + + + + + + diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 31812d011..a00698dbc 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -24,6 +24,7 @@ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 9a55c2de1..fdf8d994a 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/Models/src/main/java/com/programmersbox/models/ApiService.kt b/Models/src/main/java/com/programmersbox/models/ApiService.kt index f7c22095c..4bcf6ee20 100644 --- a/Models/src/main/java/com/programmersbox/models/ApiService.kt +++ b/Models/src/main/java/com/programmersbox/models/ApiService.kt @@ -1,7 +1,14 @@ package com.programmersbox.models +import android.app.Application +import android.content.pm.PackageInfo +import android.graphics.drawable.Drawable import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import java.io.Serializable interface ApiService : Serializable { @@ -78,4 +85,42 @@ interface ApiService : Serializable { fun Flow.dispatchIo() = this.flowOn(Dispatchers.IO) } -val sourceFlow = MutableStateFlow(null) \ No newline at end of file +interface ApiServicesCatalog { + fun createSources(): List + val name: String +} + +interface ExternalApiServicesCatalog : ApiServicesCatalog { + suspend fun initialize(app: Application) + + fun getSources(): List + override fun createSources(): List = getSources().map { it.apiService } + + val hasRemoteSources: Boolean + suspend fun getRemoteSources(): List = emptyList() + + fun shouldReload(packageName: String, packageInfo: PackageInfo): Boolean = false +} + +data class RemoteSources( + val name: String, + val packageName: String, + val version: String, + val iconUrl: String, + val downloadLink: String, + val sources: List, +) + +data class Sources( + val name: String, + val baseUrl: String, + val version: String +) + +data class SourceInformation( + val apiService: ApiService, + val name: String, + val icon: Drawable?, + val packageName: String, + val catalog: ApiServicesCatalog? = null +) diff --git a/Models/src/main/java/com/programmersbox/models/Models.kt b/Models/src/main/java/com/programmersbox/models/Models.kt index 3e49e6ada..e8dbcf210 100644 --- a/Models/src/main/java/com/programmersbox/models/Models.kt +++ b/Models/src/main/java/com/programmersbox/models/Models.kt @@ -10,6 +10,7 @@ data class ItemModel( val source: ApiService ) : Serializable { val extras = mutableMapOf() + val otherExtras = mutableMapOf() fun toInfoModel() = source.getItemInfoFlow(this) } @@ -36,6 +37,7 @@ data class ChapterModel( var uploadedTime: Long? = null fun getChapterInfo() = source.getChapterInfoFlow(this) val extras = mutableMapOf() + val otherExtras = mutableMapOf() } class NormalLink(var normal: Normal? = null) diff --git a/UIViews/build.gradle.kts b/UIViews/build.gradle.kts index c0917d021..6fbaf397f 100644 --- a/UIViews/build.gradle.kts +++ b/UIViews/build.gradle.kts @@ -5,7 +5,7 @@ plugins { id("otaku-library") id("androidx.navigation.safeargs.kotlin") id("kotlinx-serialization") - id("com.google.protobuf") version "0.9.3" + id("com.google.protobuf") version "0.9.4" alias(libs.plugins.ksp) } @@ -111,8 +111,13 @@ dependencies { implementation(libs.bundles.protobuf) + implementation(libs.bundles.ktorLibs) + //Multiplatform implementation(projects.imageloader) + + //Extension Loader + api(projects.sharedutils.extensionloader) } protobuf { diff --git a/UIViews/src/main/AndroidManifest.xml b/UIViews/src/main/AndroidManifest.xml index ba4214c79..e01238153 100644 --- a/UIViews/src/main/AndroidManifest.xml +++ b/UIViews/src/main/AndroidManifest.xml @@ -2,15 +2,18 @@ + + + diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/AboutLibrariesScreen.kt b/UIViews/src/main/java/com/programmersbox/uiviews/AboutLibrariesScreen.kt index cc1c55de4..ba25359a5 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/AboutLibrariesScreen.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/AboutLibrariesScreen.kt @@ -7,11 +7,40 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Badge +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.contentColorFor +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -353,7 +382,7 @@ private fun DefaultHeader( } } description?.let { - Divider(modifier = Modifier.padding(horizontal = 4.dp)) + HorizontalDivider(modifier = Modifier.padding(horizontal = 4.dp)) Text(it, textAlign = TextAlign.Center) } } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt b/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt index 9847d7fca..2b282e179 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle +import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate @@ -30,9 +31,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BrowseGallery +import androidx.compose.material.icons.filled.Extension import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.List import androidx.compose.material.icons.filled.Notifications @@ -60,6 +64,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -87,9 +92,9 @@ import com.google.accompanist.navigation.material.ExperimentalMaterialNavigation import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.ItemDatabase import com.programmersbox.helpfulutils.notificationManager -import com.programmersbox.models.sourceFlow import com.programmersbox.sharedutils.AppUpdate import com.programmersbox.sharedutils.MainLogo import com.programmersbox.sharedutils.updateAppCheck @@ -106,6 +111,7 @@ import com.programmersbox.uiviews.notifications.NotificationsScreen import com.programmersbox.uiviews.notifications.cancelNotification import com.programmersbox.uiviews.recent.RecentView import com.programmersbox.uiviews.settings.ComposeSettingsDsl +import com.programmersbox.uiviews.settings.ExtensionList import com.programmersbox.uiviews.settings.GeneralSettings import com.programmersbox.uiviews.settings.InfoSettings import com.programmersbox.uiviews.settings.NotificationSettings @@ -147,6 +153,9 @@ abstract class BaseMainActivity : AppCompatActivity() { private val settingsHandling: SettingsHandling by inject() + private val sourceRepository by inject() + private val currentSourceRepository by inject() + protected abstract fun onCreate() @Composable @@ -161,7 +170,8 @@ abstract class BaseMainActivity : AppCompatActivity() { @OptIn( ExperimentalMaterialNavigationApi::class, ExperimentalMaterial3Api::class, - ExperimentalMaterial3WindowSizeClassApi::class + ExperimentalMaterial3WindowSizeClassApi::class, + ExperimentalMaterialApi::class ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -176,6 +186,11 @@ abstract class BaseMainActivity : AppCompatActivity() { remember { ChromeCustomTabsNavigator(this) } ) + val scope = rememberCoroutineScope() + BackHandler(bottomSheetNavigator.sheetState.isVisible) { + scope.launch { bottomSheetNavigator.sheetState.hide() } + } + val systemUiController = rememberSystemUiController() val customPreferences = remember { ComposeSettingsDsl().apply(genericInfo.composeCustomPreferences(navController)) } @@ -320,7 +335,8 @@ abstract class BaseMainActivity : AppCompatActivity() { AppCompatResources.getDrawable(this@BaseMainActivity, logo.logoId)!!.toBitmap().asImageBitmap(), null, ) - } + }, + modifier = Modifier.verticalScroll(rememberScrollState()) ) { NavigationRailItem( imageVector = Icons.Default.History, @@ -378,6 +394,15 @@ abstract class BaseMainActivity : AppCompatActivity() { customRoute = "_home" ) + NavigationRailItem( + imageVector = Icons.Default.Extension, + label = stringResource(R.string.extensions), + screen = Screen.ExtensionListScreen, + currentDestination = currentDestination, + navController = navController, + customRoute = "_home" + ) + NavigationRailItem( icon = { BadgedBox( @@ -572,6 +597,12 @@ abstract class BaseMainActivity : AppCompatActivity() { ) } + composable( + Screen.ExtensionListScreen.route, + enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start) }, + exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End) }, + ) { ExtensionList() } + additionalSettings() if (BuildConfig.DEBUG) { @@ -611,6 +642,12 @@ abstract class BaseMainActivity : AppCompatActivity() { enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) }, exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Down) } ) { FavoriteUi(logo) } + + composable( + Screen.ExtensionListScreen.route + "_home", + enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) }, + exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Down) } + ) { ExtensionList() } } @OptIn(ExperimentalPermissionsApi::class) @@ -633,7 +670,13 @@ abstract class BaseMainActivity : AppCompatActivity() { private fun setup() { lifecycleScope.launch { - genericInfo.toSource(currentService.orEmpty())?.let { sourceFlow.emit(it) } + if (currentService == null) { + val s = sourceRepository.list.randomOrNull()?.apiService + currentSourceRepository.emit(s) + currentService = s?.serviceName + } else { + sourceRepository.toSourceByApiServiceName(currentService.orEmpty())?.let { currentSourceRepository.emit(it.apiService) } + } } when (runBlocking { settingsHandling.systemThemeMode.firstOrNull() }) { diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/CurrentSourceRepository.kt b/UIViews/src/main/java/com/programmersbox/uiviews/CurrentSourceRepository.kt new file mode 100644 index 000000000..6777b0ec0 --- /dev/null +++ b/UIViews/src/main/java/com/programmersbox/uiviews/CurrentSourceRepository.kt @@ -0,0 +1,19 @@ +package com.programmersbox.uiviews + +import com.programmersbox.models.ApiService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class CurrentSourceRepository { + private val sourceFlow = MutableStateFlow(null) + + fun asFlow() = sourceFlow.asStateFlow() + + suspend fun emit(apiService: ApiService?) { + sourceFlow.emit(apiService) + } + + fun tryEmit(apiService: ApiService?) { + sourceFlow.tryEmit(apiService) + } +} \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/DebugFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/DebugFragment.kt index 17c0285bd..bd7899b11 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/DebugFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/DebugFragment.kt @@ -1,8 +1,11 @@ package com.programmersbox.uiviews import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -10,6 +13,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Deck +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.* import androidx.compose.runtime.* @@ -18,8 +22,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.programmersbox.uiviews.utils.* + @SuppressLint("ComposeContentEmitterReturningValues") @ExperimentalComposeUiApi @ExperimentalMaterialApi @@ -27,10 +34,15 @@ import com.programmersbox.uiviews.utils.* @Composable fun DebugView() { val context = LocalContext.current + val activity = LocalActivity.current val scope = rememberCoroutineScope() + val currentSourceRepository = LocalCurrentSource.current val genericInfo = LocalGenericInfo.current val navController = LocalNavController.current val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) + val sourceRepo = LocalSourcesRepository.current + + val sources by sourceRepo.sources.collectAsStateWithLifecycle(initialValue = emptyList()) Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -46,16 +58,37 @@ fun DebugView() { ) { p -> val moreSettings = remember { genericInfo.debugMenuItem(context) } LazyColumn(contentPadding = p) { - item { + sources.forEach { + PreferenceSetting( + settingTitle = { Text(it.name) }, + settingIcon = { Icon(rememberDrawablePainter(drawable = it.icon), null, modifier = Modifier.fillMaxSize()) }, + endIcon = { + IconButton( + onClick = { + /*context.packageManager.packageInstaller.uninstall( + it.packageName, + PendingIntent.getActivity(context, 0, activity.intent, PendingIntent.FLAG_IMMUTABLE).intentSender + )*/ + val uri = Uri.fromParts("package", it.packageName, null) + val uninstall = Intent(Intent.ACTION_DELETE, uri) + context.startActivity(uninstall) + } + ) { Icon(Icons.Default.Delete, null) } + }, + modifier = Modifier.clickable( + indication = rememberRipple(), + interactionSource = remember { MutableInteractionSource() } + ) { currentSourceRepository.tryEmit(it.apiService) } + ) + } + } + item { Surface( color = Color.Blue, modifier = Modifier.fillMaxWidth() - ) { - Text("Here!") - } - + ) { Text("Here!") } } item { @@ -80,7 +113,7 @@ fun DebugView() { ) { println("Hello") } ) - Divider() + HorizontalDivider() } item { @@ -113,7 +146,7 @@ fun DebugView() { ) } - Divider() + HorizontalDivider() } item { @@ -141,7 +174,7 @@ fun DebugView() { ) } - Divider() + HorizontalDivider() } item { @@ -170,7 +203,7 @@ fun DebugView() { updateValue = { value = it } ) - Divider() + HorizontalDivider() } item { @@ -200,12 +233,12 @@ fun DebugView() { updateValue = { value = it } ) - Divider() + HorizontalDivider() } itemsIndexed(moreSettings) { index, build -> build() - if (index < moreSettings.size - 1) Divider() + if (index < moreSettings.size - 1) HorizontalDivider() } } } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/GenericInfo.kt b/UIViews/src/main/java/com/programmersbox/uiviews/GenericInfo.kt index 283567206..5e52439f3 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/GenericInfo.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/GenericInfo.kt @@ -30,6 +30,8 @@ interface GenericInfo { val scrollBuffer: Int get() = 2 val deepLinkUri: String + val sourceType: String get() = "" + fun deepLinkDetails(context: Context, itemModel: ItemModel?): PendingIntent? fun deepLinkSettings(context: Context): PendingIntent? diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/OtakuApp.kt b/UIViews/src/main/java/com/programmersbox/uiviews/OtakuApp.kt index 8bc72dcc3..2a17c71e4 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/OtakuApp.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/OtakuApp.kt @@ -6,23 +6,35 @@ import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager import android.os.Build import androidx.annotation.RequiresApi -import androidx.work.* +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequest +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager import com.facebook.stetho.Stetho import com.google.android.material.color.DynamicColors +import com.programmersbox.extensionloader.SourceLoader +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.helpfulutils.NotificationChannelImportance import com.programmersbox.helpfulutils.createNotificationChannel import com.programmersbox.helpfulutils.createNotificationGroup import com.programmersbox.loggingutils.Loged import com.programmersbox.sharedutils.FirebaseUIStyle +import com.programmersbox.uiviews.checkers.AppCheckWorker +import com.programmersbox.uiviews.checkers.SourceUpdateChecker +import com.programmersbox.uiviews.checkers.UpdateFlowWorker import com.programmersbox.uiviews.utils.SettingsHandling import com.programmersbox.uiviews.utils.shouldCheckFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking +import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.loadKoinModules import org.koin.core.context.startKoin import org.koin.dsl.module +import java.util.Locale import java.util.concurrent.TimeUnit abstract class OtakuApp : Application() { @@ -43,6 +55,8 @@ abstract class OtakuApp : Application() { createNotificationGroup("otakuGroup") createNotificationChannel("updateCheckChannel", importance = NotificationChannelImportance.MIN) createNotificationChannel("appUpdate", importance = NotificationChannelImportance.HIGH) + createNotificationChannel("sourceUpdate", importance = NotificationChannelImportance.DEFAULT) + createNotificationGroup("sources") } startKoin { @@ -58,6 +72,22 @@ abstract class OtakuApp : Application() { onCreated() + loadKoinModules( + module { + single { SourceRepository() } + single { CurrentSourceRepository() } + single { SourceLoader(this@OtakuApp, get(), get().sourceType, get()) } + single { + OtakuWorldCatalog( + get().sourceType + .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + ) + } + } + ) + + get().load() + val work = WorkManager.getInstance(this) work.enqueueUniquePeriodicWork( @@ -77,6 +107,23 @@ abstract class OtakuApp : Application() { .build() ).state.observeForever { println(it) } + work.enqueueUniquePeriodicWork( + "sourceChecks", + ExistingPeriodicWorkPolicy.KEEP, + PeriodicWorkRequest.Builder(SourceUpdateChecker::class.java, 1, TimeUnit.DAYS) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(false) + .setRequiresCharging(false) + .setRequiresDeviceIdle(false) + .setRequiresStorageNotLow(false) + .build() + ) + .setInitialDelay(10, TimeUnit.SECONDS) + .build() + ).state.observeForever { println(it) } + updateSetup(this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) shortcutSetup() @@ -141,5 +188,4 @@ abstract class OtakuApp : Application() { } } } - } \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/OtakuWorldCatalog.kt b/UIViews/src/main/java/com/programmersbox/uiviews/OtakuWorldCatalog.kt new file mode 100644 index 000000000..64b7b9bd2 --- /dev/null +++ b/UIViews/src/main/java/com/programmersbox/uiviews/OtakuWorldCatalog.kt @@ -0,0 +1,108 @@ +package com.programmersbox.uiviews + +import android.app.Application +import com.programmersbox.models.ApiService +import com.programmersbox.models.ExternalApiServicesCatalog +import com.programmersbox.models.RemoteSources +import com.programmersbox.models.SourceInformation +import com.programmersbox.models.Sources +import io.ktor.client.HttpClient +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class OtakuWorldCatalog( + override val name: String, +) : ExternalApiServicesCatalog, KoinComponent { + + val genericInfo: GenericInfo by inject() + + private val json = Json { + isLenient = true + prettyPrint = true + ignoreUnknownKeys = true + coerceInputValues = true + } + + private val client by lazy { + HttpClient { + install(ContentNegotiation) { json(json) } + } + } + + private suspend fun remoteSources() = client.get("${REPO_URL_PREFIX}index.min.json") + .bodyAsText() + .let { json.decodeFromString>(it) } + + override suspend fun initialize(app: Application) {} + + override fun getSources(): List = listOf( + //TODO: Gotta think about this....Not a fan of it... + SourceInformation( + apiService = object : ApiService { + override val baseUrl: String get() = "https://github.com/jakepurple13/OtakuWorld" + override val serviceName: String get() = name + override val notWorking: Boolean get() = true + }, + name = name, + icon = null, + packageName = "", + catalog = this + ) + ) + + override suspend fun getRemoteSources(): List = remoteSources() + .filter { genericInfo.sourceType == it.feature } + .map { + RemoteSources( + name = it.name, + packageName = it.pkg, + version = it.version, + iconUrl = "${REPO_URL_PREFIX}icon/${it.pkg}.png", + downloadLink = "${REPO_URL_PREFIX}apk/${it.apk}", + sources = it.sources + ?.map { j -> + Sources( + name = j.name, + baseUrl = j.baseUrl, + version = it.version + ) + } + .orEmpty() + ) + } + + override val hasRemoteSources: Boolean = true +} + +private fun ExtensionJsonObject.extractLibVersion(): Double { + return version.substringBeforeLast('.').toDouble() +} + +private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/jakepurple13/OtakuWorldSources/repo/" + +@Serializable +private data class ExtensionJsonObject( + val name: String, + val pkg: String, + val apk: String, + val lang: String, + val code: Long, + val version: String, + val feature: String, + val sources: List?, +) + +@Serializable +private data class ExtensionSourceJsonObject( + val id: String, + val lang: String, + val name: String, + val baseUrl: String, + val versionId: Int, +) diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/all/AllFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/all/AllFragment.kt index 6a1b8b657..d1beccbec 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/all/AllFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/all/AllFragment.kt @@ -68,12 +68,13 @@ import com.programmersbox.favoritesdatabase.DbModel import com.programmersbox.favoritesdatabase.ItemDao import com.programmersbox.models.ApiService import com.programmersbox.models.ItemModel -import com.programmersbox.models.sourceFlow import com.programmersbox.sharedutils.MainLogo +import com.programmersbox.uiviews.CurrentSourceRepository import com.programmersbox.uiviews.R import com.programmersbox.uiviews.utils.ComponentState import com.programmersbox.uiviews.utils.InsetSmallTopAppBar import com.programmersbox.uiviews.utils.LightAndDarkPreviews +import com.programmersbox.uiviews.utils.LocalCurrentSource import com.programmersbox.uiviews.utils.LocalGenericInfo import com.programmersbox.uiviews.utils.LocalItemDao import com.programmersbox.uiviews.utils.LocalNavController @@ -94,10 +95,13 @@ fun AllView( logo: MainLogo, context: Context = LocalContext.current, dao: ItemDao = LocalItemDao.current, - allVm: AllViewModel = viewModel { AllViewModel(dao, context) }, + currentSourceRepository: CurrentSourceRepository = LocalCurrentSource.current, + allVm: AllViewModel = viewModel { AllViewModel(dao, context, currentSourceRepository) }, ) { val isConnected by allVm.observeNetwork.collectAsState(initial = true) - val source by sourceFlow.collectAsState(initial = null) + val source by currentSourceRepository + .asFlow() + .collectAsState(initial = null) LaunchedEffect(isConnected) { if (allVm.sourceList.isEmpty() && source != null && isConnected && allVm.count != 1) allVm.reset(context, source!!) @@ -231,7 +235,7 @@ fun AllScreen( showBanner: (Boolean) -> Unit ) { val info = LocalGenericInfo.current - val source by sourceFlow.collectAsState(initial = null) + val source by LocalCurrentSource.current.asFlow().collectAsState(initial = null) val navController = LocalNavController.current val context = LocalContext.current val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh = { source?.let { onReset(context, it) } }) @@ -287,7 +291,7 @@ fun SearchScreen( val info = LocalGenericInfo.current val focusManager = LocalFocusManager.current val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val source by sourceFlow.collectAsState(initial = null) + val source by LocalCurrentSource.current.asFlow().collectAsState(initial = null) val navController = LocalNavController.current OtakuScaffold( @@ -317,10 +321,12 @@ fun SearchScreen( .fillMaxWidth(), singleLine = true, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions(onSearch = { - focusManager.clearFocus() - search() - }) + keyboardActions = KeyboardActions( + onSearch = { + focusManager.clearFocus() + search() + } + ) ) } ) { p -> diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/all/AllViewModel.kt b/UIViews/src/main/java/com/programmersbox/uiviews/all/AllViewModel.kt index 17bf25e2f..799866c53 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/all/AllViewModel.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/all/AllViewModel.kt @@ -1,7 +1,11 @@ package com.programmersbox.uiviews.all import android.content.Context -import androidx.compose.runtime.* +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.util.fastMaxBy import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -9,15 +13,26 @@ import com.programmersbox.favoritesdatabase.DbModel import com.programmersbox.favoritesdatabase.ItemDao import com.programmersbox.models.ApiService import com.programmersbox.models.ItemModel -import com.programmersbox.models.sourceFlow import com.programmersbox.sharedutils.FirebaseDb +import com.programmersbox.uiviews.CurrentSourceRepository import com.programmersbox.uiviews.utils.dispatchIoAndCatchList import com.programmersbox.uiviews.utils.showErrorToast import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import ru.beryukhov.reactivenetwork.ReactiveNetwork -class AllViewModel(dao: ItemDao, context: Context? = null) : ViewModel() { +class AllViewModel( + dao: ItemDao, + context: Context? = null, + private val currentSourceRepository: CurrentSourceRepository, +) : ViewModel() { val observeNetwork = ReactiveNetwork() .observeInternetConnectivity() @@ -44,7 +59,7 @@ class AllViewModel(dao: ItemDao, context: Context? = null) : ViewModel() { .onEach { favoriteList = it.toMutableStateList() } .launchIn(viewModelScope) - sourceFlow + currentSourceRepository.asFlow() .filterNotNull() .onEach { count = 1 @@ -76,7 +91,7 @@ class AllViewModel(dao: ItemDao, context: Context? = null) : ViewModel() { } fun search() { - sourceFlow + currentSourceRepository.asFlow() .filterNotNull() .flatMapMerge { it.searchSourceList(searchText, 1, sourceList) } .onStart { isSearching = true } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/checkers/SourceUpdateChecker.kt b/UIViews/src/main/java/com/programmersbox/uiviews/checkers/SourceUpdateChecker.kt new file mode 100644 index 000000000..c9207741a --- /dev/null +++ b/UIViews/src/main/java/com/programmersbox/uiviews/checkers/SourceUpdateChecker.kt @@ -0,0 +1,92 @@ +package com.programmersbox.uiviews.checkers + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.programmersbox.extensionloader.SourceLoader +import com.programmersbox.extensionloader.SourceRepository +import com.programmersbox.helpfulutils.GroupBehavior +import com.programmersbox.helpfulutils.NotificationDslBuilder +import com.programmersbox.helpfulutils.notificationManager +import com.programmersbox.models.ExternalApiServicesCatalog +import com.programmersbox.sharedutils.AppUpdate +import com.programmersbox.uiviews.OtakuWorldCatalog +import com.programmersbox.uiviews.utils.NotificationLogo +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class SourceUpdateChecker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams), KoinComponent { + + private val logo: NotificationLogo by inject() + private val sourceRepository: SourceRepository by inject() + private val sourceLoader: SourceLoader by inject() + private val otakuWorldCatalog: OtakuWorldCatalog by inject() + + override suspend fun doWork(): Result { + return try { + val notificationManager = applicationContext.notificationManager + val packageManager = applicationContext.packageManager + if (sourceRepository.list.isEmpty()) { + sourceLoader.blockingLoad() + } + val remoteSources = otakuWorldCatalog.getRemoteSources() + sourceRepository.list + .filter { it.catalog is ExternalApiServicesCatalog } + .flatMap { (it.catalog as? ExternalApiServicesCatalog)?.getRemoteSources().orEmpty() } + + val updateList = sourceRepository.list + .filter { l -> remoteSources.any { it.packageName == l.packageName } } + .mapNotNull { + val localVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getPackageInfo(it.packageName, PackageManager.PackageInfoFlags.of(0L)) + } else { + packageManager.getPackageInfo(it.packageName, 0) + } + ?.versionName + .orEmpty() + + remoteSources + .find { r -> r.packageName == it.packageName } + ?.let { r -> AppUpdate.checkForUpdate(localVersion, r.version) } + ?.let { r -> if (r) it else null } + } + + updateList.forEach { + val r = remoteSources.find { r -> r.packageName == it.packageName }!! + val n = NotificationDslBuilder.builder( + applicationContext, + "sourceUpdate", + logo.notificationId + ) { + title = "${it.name} has an update!" + subText = "${r.version} is available." + groupId = "sources" + } + notificationManager.notify(it.hashCode(), n) + } + + if (updateList.isNotEmpty()) { + notificationManager.notify( + 15, + NotificationDslBuilder.builder( + applicationContext, + "sourceUpdate", + logo.notificationId + ) { + title = "Sources have updates!" + subText = "Sources have updates!" + showWhen = true + groupSummary = true + groupAlertBehavior = GroupBehavior.ALL + groupId = "sources" + } + ) + } + Result.success() + } catch (e: Exception) { + e.printStackTrace() + Result.success() + } + } +} \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/UpdateChecker.kt b/UIViews/src/main/java/com/programmersbox/uiviews/checkers/UpdateChecker.kt similarity index 73% rename from UIViews/src/main/java/com/programmersbox/uiviews/UpdateChecker.kt rename to UIViews/src/main/java/com/programmersbox/uiviews/checkers/UpdateChecker.kt index 6741df561..bfb22d292 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/UpdateChecker.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/checkers/UpdateChecker.kt @@ -1,4 +1,6 @@ -package com.programmersbox.uiviews +@file:OptIn(DelicateCoroutinesApi::class) + +package com.programmersbox.uiviews.checkers import android.app.Notification import android.app.PendingIntent @@ -12,18 +14,34 @@ import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMaxBy -import androidx.navigation.NavDeepLinkBuilder import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.programmersbox.favoritesdatabase.* +import com.programmersbox.extensionloader.SourceLoader +import com.programmersbox.extensionloader.SourceRepository +import com.programmersbox.favoritesdatabase.DbModel +import com.programmersbox.favoritesdatabase.ItemDao +import com.programmersbox.favoritesdatabase.ItemDatabase +import com.programmersbox.favoritesdatabase.NotificationItem +import com.programmersbox.favoritesdatabase.toItemModel import com.programmersbox.gsonutils.fromJson -import com.programmersbox.helpfulutils.* +import com.programmersbox.helpfulutils.GroupBehavior +import com.programmersbox.helpfulutils.NotificationDslBuilder +import com.programmersbox.helpfulutils.SemanticActions +import com.programmersbox.helpfulutils.intersect +import com.programmersbox.helpfulutils.notificationManager import com.programmersbox.loggingutils.Loged import com.programmersbox.loggingutils.fd import com.programmersbox.models.InfoModel import com.programmersbox.sharedutils.AppUpdate import com.programmersbox.sharedutils.FirebaseDb -import com.programmersbox.uiviews.utils.* +import com.programmersbox.uiviews.GenericInfo +import com.programmersbox.uiviews.R +import com.programmersbox.uiviews.utils.NotificationLogo +import com.programmersbox.uiviews.utils.UPDATE_CHECKING_END +import com.programmersbox.uiviews.utils.UPDATE_CHECKING_START +import com.programmersbox.uiviews.utils.appVersion +import com.programmersbox.uiviews.utils.updatePref +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect @@ -33,7 +51,6 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import java.io.IOException import java.net.HttpURLConnection import java.net.URL @@ -54,11 +71,6 @@ class AppCheckWorker(context: Context, workerParams: WorkerParameters) : Corouti ) { title = applicationContext.getString(R.string.theresAnUpdate) subText = applicationContext.getString(R.string.versionAvailable, f) - pendingIntent { context -> - NavDeepLinkBuilder(context) - .setDestination(Screen.SettingsScreen.route) - .createPendingIntent() - } } applicationContext.notificationManager.notify(12, n) } @@ -75,76 +87,78 @@ class UpdateFlowWorker(context: Context, workerParams: WorkerParameters) : Corou private val dao by lazy { ItemDatabase.getInstance(this@UpdateFlowWorker.applicationContext).itemDao() } private val genericInfo: GenericInfo by inject() + private val sourceRepository: SourceRepository by inject() + private val sourceLoader: SourceLoader by inject() - override suspend fun doWork(): Result { - return try { - update.sendRunningNotification(100, 0, applicationContext.getString(R.string.startingCheck)) - Loged.fd("Starting check here") - applicationContext.updatePref(UPDATE_CHECKING_START, System.currentTimeMillis()) - - Loged.fd("Start") - val list = listOf( - dao.getAllFavoritesSync(), - FirebaseDb.getAllShows().requireNoNulls() - ).flatten().groupBy(DbModel::url).map { it.value.fastMaxBy(DbModel::numChapters)!! } - - // Getting all recent updates - val newList = list.intersect( - genericInfo.sourceList() - .filter { s -> list.fastAny { m -> m.source == s.serviceName } } - .mapNotNull { m -> - try { - withTimeoutOrNull(10000) { - m.getRecentFlow() - .catch { emit(emptyList()) } - .firstOrNull() - } - } catch (e: Exception) { - e.printStackTrace() - null + override suspend fun doWork(): Result = try { + update.sendRunningNotification(100, 0, applicationContext.getString(R.string.startingCheck)) + Loged.fd("Starting check here") + applicationContext.updatePref(UPDATE_CHECKING_START, System.currentTimeMillis()) + + Loged.fd("Start") + + //Getting all favorites + val list = listOf( + dao.getAllFavoritesSync(), + FirebaseDb.getAllShows().requireNoNulls() + ).flatten().groupBy(DbModel::url).map { it.value.fastMaxBy(DbModel::numChapters)!! } + + //Making sure we have our sources + if (sourceRepository.list.isEmpty()) { + sourceLoader.blockingLoad() + } + + // Getting all recent updates + val newList = list.intersect( + sourceRepository.apiServiceList + .filter { s -> list.fastAny { m -> m.source == s.serviceName } } + .mapNotNull { m -> + runCatching { + withTimeoutOrNull(10000) { + m.getRecentFlow() + .catch { emit(emptyList()) } + .firstOrNull() } - }.flatten() - ) { o, n -> o.url == n.url } - .distinctBy { it.url } - - // Checking if any have updates - Loged.fd("Checking for updates") - val items = newList.mapIndexedNotNull { index, model -> - update.sendRunningNotification(newList.size, index, model.title) - try { - val newData = genericInfo.toSource(model.source) - ?.let { - withTimeoutOrNull(10000) { - model.toItemModel(it).toInfoModel() - .firstOrNull() - ?.getOrNull() - } + } + .onFailure { it.printStackTrace() } + .getOrNull() + }.flatten() + ) { o, n -> o.url == n.url } + .distinctBy { it.url } + + // Checking if any have updates + println("Checking for updates") + val items = newList.mapIndexedNotNull { index, model -> + update.sendRunningNotification(newList.size, index, model.title) + runCatching { + val newData = sourceRepository.toSourceByApiServiceName(model.source) + ?.apiService + ?.let { + withTimeoutOrNull(10000) { + model.toItemModel(it).toInfoModel() + .firstOrNull() + ?.getOrNull() } - println("Old: ${model.numChapters} New: ${newData?.chapters?.size}") - // To test notifications, comment these out but leave the Pair - if (model.numChapters >= (newData?.chapters?.size ?: -1)) null - else Pair(newData, model) - } catch (e: Exception) { - e.printStackTrace() - null - } - }.also { - try { - } catch (ignored: Exception) { - } + } + println("Old: ${model.numChapters} New: ${newData?.chapters?.size}") + // To test notifications, comment the takeUnless out + Pair(newData, model) + .takeUnless { it.second.numChapters >= (it.first?.chapters?.size ?: -1) } } + .onFailure { it.printStackTrace() } + .getOrNull() + } - // Saving updates - update.updateManga(dao, items) - update.onEnd(update.mapDbModel(dao, items, genericInfo), info = genericInfo) - Loged.fd("Finished!") + // Saving updates + update.updateManga(dao, items) + update.onEnd(update.mapDbModel(dao, items, genericInfo), info = genericInfo) + Loged.fd("Finished!") - Result.success() - } finally { - applicationContext.updatePref(UPDATE_CHECKING_END, System.currentTimeMillis()) - update.sendFinishedNotification() - Result.success() - } + Result.success() + } finally { + applicationContext.updatePref(UPDATE_CHECKING_END, System.currentTimeMillis()) + update.sendFinishedNotification() + Result.success() } } @@ -245,19 +259,6 @@ class UpdateNotification(private val context: Context) : KoinComponent { ) } - private fun getBitmapFromURL(strURL: String?, headers: Map = emptyMap()): Bitmap? = try { - val url = URL(strURL) - val connection: HttpURLConnection = url.openConnection() as HttpURLConnection - headers.forEach { connection.setRequestProperty(it.key, it.value.toString()) } - connection.doInput = true - connection.connect() - BitmapFactory.decodeStream(connection.inputStream) - } catch (e: IOException) { - //e.printStackTrace() - Loged.e(e.localizedMessage, showPretty = false) - null - } - fun sendRunningNotification(max: Int, progress: Int, contextText: CharSequence = "") { val notification = NotificationDslBuilder.builder(context, "updateCheckChannel", icon.notificationId) { onlyAlertOnce = true @@ -306,10 +307,12 @@ class BootReceived : BroadcastReceiver(), KoinComponent { private val logo: NotificationLogo by inject() private val info: GenericInfo by inject() + private val sourceRepository: SourceRepository by inject() override fun onReceive(context: Context?, intent: Intent?) { Loged.d("BootReceived") - context?.let { SavedNotifications.viewNotificationsFromDb(it, logo, info) } + println(intent?.action) + context?.let { SavedNotifications.viewNotificationsFromDb(it, logo, info, sourceRepository) } } } @@ -317,18 +320,25 @@ class NotifySingleWorker(context: Context, workerParams: WorkerParameters) : Cor private val logo: NotificationLogo by inject() private val genericInfo: GenericInfo by inject() + private val sourceRepository: SourceRepository by inject() override suspend fun doWork(): Result { inputData.getString("notiData") ?.fromJson() - ?.let { d -> SavedNotifications.viewNotificationFromDb(applicationContext, d, logo, genericInfo) } + ?.let { d -> SavedNotifications.viewNotificationFromDb(applicationContext, d, logo, genericInfo, sourceRepository) } return Result.success() } } object SavedNotifications { - fun viewNotificationFromDb(context: Context, n: NotificationItem, notificationLogo: NotificationLogo, info: GenericInfo) { + fun viewNotificationFromDb( + context: Context, + n: NotificationItem, + notificationLogo: NotificationLogo, + info: GenericInfo, + sourceRepository: SourceRepository, + ) { val icon = notificationLogo.notificationId val update = UpdateNotification(context) (n.id to NotificationDslBuilder.builder( @@ -372,7 +382,8 @@ object SavedNotifications { }*/ pendingIntent { context -> runBlocking { - val itemModel = info.toSource(n.source) + val itemModel = sourceRepository.toSourceByApiServiceName(n.source) + ?.apiService ?.getSourceByUrlFlow(n.url) ?.firstOrNull() @@ -383,7 +394,12 @@ object SavedNotifications { .let { update.onEnd(listOf(it), info = info) } } - fun viewNotificationsFromDb(context: Context, logo: NotificationLogo, info: GenericInfo) { + fun viewNotificationsFromDb( + context: Context, + logo: NotificationLogo, + info: GenericInfo, + sourceRepository: SourceRepository, + ) { val dao by lazy { ItemDatabase.getInstance(context).itemDao() } val icon = logo.notificationId val update = UpdateNotification(context) @@ -432,7 +448,8 @@ object SavedNotifications { }*/ pendingIntent { context -> runBlocking { - val itemModel = info.toSource(n.source)//UpdateWorker.sourceFromString(n.source) + val itemModel = sourceRepository.toSourceByApiServiceName(n.source)//UpdateWorker.sourceFromString(n.source) + ?.apiService ?.getSourceByUrlFlow(n.url) ?.firstOrNull() @@ -444,15 +461,15 @@ object SavedNotifications { .let { update.onEnd(it, info = info) } } } +} - private fun getBitmapFromURL(strURL: String?, headers: Map = emptyMap()): Bitmap? = try { - val url = URL(strURL) - val connection: HttpURLConnection = url.openConnection() as HttpURLConnection - headers.forEach { connection.setRequestProperty(it.key, it.value.toString()) } - connection.doInput = true - connection.connect() - BitmapFactory.decodeStream(connection.inputStream) - } catch (e: IOException) { - null - } -} \ No newline at end of file +private fun getBitmapFromURL(strURL: String?, headers: Map = emptyMap()): Bitmap? = runCatching { + val url = URL(strURL) + val connection: HttpURLConnection = url.openConnection() as HttpURLConnection + headers.forEach { connection.setRequestProperty(it.key, it.value.toString()) } + connection.doInput = true + connection.connect() + BitmapFactory.decodeStream(connection.inputStream) +} + .onFailure { it.printStackTrace() } + .getOrNull() \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteFragment.kt index 0c2e7fc7b..195556790 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteFragment.kt @@ -1,5 +1,6 @@ package com.programmersbox.uiviews.favorite +import android.widget.Toast import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable @@ -19,7 +20,6 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Circle @@ -29,13 +29,16 @@ import androidx.compose.material.icons.filled.Sort import androidx.compose.material.icons.filled.SortByAlpha import androidx.compose.material.icons.filled.Star import androidx.compose.material3.Button -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.SearchBar +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -44,6 +47,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -56,18 +60,18 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.compose.viewModel +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.ItemDao import com.programmersbox.favoritesdatabase.toItemModel import com.programmersbox.sharedutils.MainLogo -import com.programmersbox.uiviews.GenericInfo import com.programmersbox.uiviews.R import com.programmersbox.uiviews.utils.BackButton import com.programmersbox.uiviews.utils.ComponentState import com.programmersbox.uiviews.utils.InsetSmallTopAppBar import com.programmersbox.uiviews.utils.LightAndDarkPreviews -import com.programmersbox.uiviews.utils.LocalGenericInfo import com.programmersbox.uiviews.utils.LocalItemDao import com.programmersbox.uiviews.utils.LocalNavController +import com.programmersbox.uiviews.utils.LocalSourcesRepository import com.programmersbox.uiviews.utils.M3CoverCard import com.programmersbox.uiviews.utils.MockAppIcon import com.programmersbox.uiviews.utils.OtakuBannerBox @@ -80,17 +84,17 @@ import com.programmersbox.uiviews.utils.components.GroupButtonModel import com.programmersbox.uiviews.utils.components.ListBottomScreen import com.programmersbox.uiviews.utils.components.ListBottomSheetItemModel import com.programmersbox.uiviews.utils.navigateToDetails +import kotlinx.coroutines.launch import androidx.compose.material3.MaterialTheme as M3MaterialTheme @ExperimentalMaterial3Api -@ExperimentalMaterialApi @ExperimentalFoundationApi @Composable fun FavoriteUi( logo: MainLogo, - genericInfo: GenericInfo = LocalGenericInfo.current, dao: ItemDao = LocalItemDao.current, - viewModel: FavoriteViewModel = viewModel { FavoriteViewModel(dao, genericInfo) } + sourceRepository: SourceRepository = LocalSourcesRepository.current, + viewModel: FavoriteViewModel = viewModel { FavoriteViewModel(dao, sourceRepository) }, ) { val navController = LocalNavController.current val context = LocalContext.current @@ -100,6 +104,9 @@ fun FavoriteUi( val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) var showBanner by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + val snackbarHostState = remember { SnackbarHostState() } OtakuBannerBox( showBanner = showBanner, @@ -108,6 +115,7 @@ fun FavoriteUi( ) { OtakuScaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { Surface { Column( @@ -202,7 +210,7 @@ fun FavoriteUi( } ) if (index != 3) { - Divider() + HorizontalDivider() } } } @@ -293,8 +301,12 @@ fun FavoriteUi( onLongPress = { c -> newItemModel( if (c == ComponentState.Pressed) { - info.value.randomOrNull() - ?.let { genericInfo.toSource(it.source)?.let { it1 -> it.toItemModel(it1) } } + info.value.randomOrNull()?.let { + sourceRepository + .toSourceByApiServiceName(it.source) + ?.apiService + ?.let { it1 -> it.toItemModel(it1) } + } } else null ) showBanner = c == ComponentState.Pressed @@ -328,8 +340,19 @@ fun FavoriteUi( if (info.value.size == 1) { info.value .firstOrNull() - ?.let { genericInfo.toSource(it.source)?.let { it1 -> it.toItemModel(it1) } } - ?.let { navController.navigateToDetails(it) } + ?.let { + sourceRepository + .toSourceByApiServiceName(it.source) + ?.apiService + ?.let { it1 -> it.toItemModel(it1) } + } + ?.let(navController::navigateToDetails) ?: scope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + snackbarHostState.showSnackbar( + "Something went wrong. Source might not be installed", + duration = SnackbarDuration.Short + ) + } } else { Screen.FavoriteChoiceScreen.navigate(navController, info.value) } @@ -343,16 +366,26 @@ fun FavoriteUi( @Composable fun FavoriteChoiceScreen(vm: FavoriteChoiceViewModel = viewModel { FavoriteChoiceViewModel(createSavedStateHandle()) }) { - val genericInfo = LocalGenericInfo.current + val sourceRepository = LocalSourcesRepository.current val navController = LocalNavController.current + val context = LocalContext.current ListBottomScreen( includeInsetPadding = false, title = stringResource(R.string.chooseASource), list = vm.items, onClick = { item -> item - .let { genericInfo.toSource(it.source)?.let { it1 -> it.toItemModel(it1) } } - ?.let { navController.navigateToDetails(it) } + .let { + sourceRepository + .toSourceByApiServiceName(it.source) + ?.apiService + ?.let { it1 -> it.toItemModel(it1) } + } + ?.let(navController::navigateToDetails) ?: Toast.makeText( + context, + "Something went wrong. Source might not be installed", + Toast.LENGTH_SHORT + ).show() } ) { ListBottomSheetItemModel( @@ -363,7 +396,6 @@ fun FavoriteChoiceScreen(vm: FavoriteChoiceViewModel = viewModel { FavoriteChoic } @ExperimentalMaterial3Api -@ExperimentalMaterialApi @ExperimentalFoundationApi @LightAndDarkPreviews @Composable diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteViewModel.kt b/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteViewModel.kt index 0d6bea7a7..cf38e797a 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteViewModel.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteViewModel.kt @@ -10,35 +10,47 @@ import androidx.compose.ui.util.fastMaxBy import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.DbModel import com.programmersbox.favoritesdatabase.ItemDao import com.programmersbox.gsonutils.fromJson -import com.programmersbox.models.ApiService import com.programmersbox.sharedutils.FirebaseDb -import com.programmersbox.uiviews.GenericInfo import com.programmersbox.uiviews.utils.Screen import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -class FavoriteViewModel(dao: ItemDao, private val genericInfo: GenericInfo) : ViewModel() { +class FavoriteViewModel( + dao: ItemDao, + private val sourceRepository: SourceRepository, +) : ViewModel() { private val fireListener = FirebaseDb.FirebaseListener() - var favoriteList by mutableStateOf>(emptyList()) - private set + + private val favoriteList = mutableStateListOf() + + private var sourceList = sourceRepository.list.map { it.apiService.serviceName } + + private val fullSourceList get() = (sourceList + favoriteList.map { it.source }).distinct() init { combine( fireListener.getAllShowsFlow(), dao.getAllFavorites() ) { f, d -> (f + d).groupBy(DbModel::url).map { it.value.fastMaxBy(DbModel::numChapters)!! } } - .onEach { favoriteList = it } + .onEach { + favoriteList.clear() + favoriteList.addAll(it) + selectedSources.addAll( + sourceRepository.list.map { l -> l.apiService.serviceName } + + it.map { f -> f.source } + ) + } .launchIn(viewModelScope) - } - override fun onCleared() { - super.onCleared() - fireListener.unregister() + sourceRepository.sources + .onEach { sourceList = it.map { s -> s.apiService.serviceName } } + .launchIn(viewModelScope) } var searchText by mutableStateOf("") @@ -46,7 +58,7 @@ class FavoriteViewModel(dao: ItemDao, private val genericInfo: GenericInfo) : Vi var sortedBy by mutableStateOf>(SortFavoritesBy.TITLE) var reverse by mutableStateOf(false) - val selectedSources = mutableStateListOf(*genericInfo.sourceList().fastMap(ApiService::serviceName).toTypedArray()) + val selectedSources = mutableStateListOf() val listSources by derivedStateOf { favoriteList.filter { it.title.contains(searchText, true) && it.source in selectedSources } @@ -68,7 +80,7 @@ class FavoriteViewModel(dao: ItemDao, private val genericInfo: GenericInfo) : Vi } val allSources by derivedStateOf { - (genericInfo.sourceList().fastMap(ApiService::serviceName) + listSources.fastMap(DbModel::source)) + (fullSourceList + listSources.fastMap(DbModel::source)) .groupBy { it } .toList() .sortedBy { it.first } @@ -85,7 +97,7 @@ class FavoriteViewModel(dao: ItemDao, private val genericInfo: GenericInfo) : Vi fun resetSources() { selectedSources.clear() - selectedSources.addAll(genericInfo.sourceList().fastMap(ApiService::serviceName)) + selectedSources.addAll(fullSourceList) } private fun clearAllSources() { @@ -93,19 +105,23 @@ class FavoriteViewModel(dao: ItemDao, private val genericInfo: GenericInfo) : Vi } fun allClick() { - if (selectedSources.size == genericInfo.sourceList().size) { + if (selectedSources.size == fullSourceList.size) { clearAllSources() } else { resetSources() } } + override fun onCleared() { + super.onCleared() + fireListener.unregister() + } } sealed class SortFavoritesBy(val sort: (Map.Entry>) -> K) { - object TITLE : SortFavoritesBy(Map.Entry>::key) - object COUNT : SortFavoritesBy({ it.value.size }) - object CHAPTERS : SortFavoritesBy({ it.value.maxOf(DbModel::numChapters) }) + data object TITLE : SortFavoritesBy(Map.Entry>::key) + data object COUNT : SortFavoritesBy({ it.value.size }) + data object CHAPTERS : SortFavoritesBy({ it.value.maxOf(DbModel::numChapters) }) } class FavoriteChoiceViewModel( diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/globalsearch/GlobalSearchFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/globalsearch/GlobalSearchFragment.kt index 2a1005d5f..379083f16 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/globalsearch/GlobalSearchFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/globalsearch/GlobalSearchFragment.kt @@ -36,9 +36,9 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Divider import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -79,28 +79,26 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.placeholder.material.placeholder +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.HistoryDao import com.programmersbox.favoritesdatabase.HistoryItem import com.programmersbox.models.ItemModel import com.programmersbox.sharedutils.MainLogo -import com.programmersbox.uiviews.GenericInfo import com.programmersbox.uiviews.R import com.programmersbox.uiviews.utils.BackButton import com.programmersbox.uiviews.utils.ComponentState import com.programmersbox.uiviews.utils.ComposableUtils import com.programmersbox.uiviews.utils.InsetSmallTopAppBar import com.programmersbox.uiviews.utils.LightAndDarkPreviews -import com.programmersbox.uiviews.utils.LocalGenericInfo import com.programmersbox.uiviews.utils.LocalHistoryDao import com.programmersbox.uiviews.utils.LocalNavController +import com.programmersbox.uiviews.utils.LocalSourcesRepository import com.programmersbox.uiviews.utils.M3PlaceHolderCoverCard import com.programmersbox.uiviews.utils.MockApiService import com.programmersbox.uiviews.utils.MockAppIcon -import com.programmersbox.uiviews.utils.MockInfo import com.programmersbox.uiviews.utils.NotificationLogo import com.programmersbox.uiviews.utils.OtakuBannerBox import com.programmersbox.uiviews.utils.PreviewTheme -import com.programmersbox.uiviews.utils.Screen import com.programmersbox.uiviews.utils.adaptiveGridCell import com.programmersbox.uiviews.utils.combineClickableWithIndication import com.programmersbox.uiviews.utils.navigateToDetails @@ -116,11 +114,11 @@ import androidx.compose.material3.contentColorFor as m3ContentColorFor fun GlobalSearchView( mainLogo: MainLogo, notificationLogo: NotificationLogo, - info: GenericInfo = LocalGenericInfo.current, + sourceRepository: SourceRepository = LocalSourcesRepository.current, dao: HistoryDao = LocalHistoryDao.current, viewModel: GlobalSearchViewModel = viewModel { GlobalSearchViewModel( - info = info, + sourceRepository = sourceRepository, initialSearch = createSavedStateHandle().get("searchFor") ?: "", dao = dao, ) @@ -233,7 +231,7 @@ fun GlobalSearchView( } ) if (index != history.lastIndex) { - Divider() + HorizontalDivider() } } } @@ -268,7 +266,7 @@ fun GlobalSearchView( newItemModel(if (c == ComponentState.Pressed) m else null) showBanner = c == ComponentState.Pressed } - ) { Screen.GlobalSearchScreen.navigate(navController, m.title) } + ) { navController.navigateToDetails(m) } } } } @@ -322,7 +320,7 @@ fun GlobalSearchView( Surface( modifier = Modifier.placeholder(true, color = placeholderColor), tonalElevation = 4.dp, - shape = androidx.compose.material3.MaterialTheme.shapes.medium + shape = M3MaterialTheme.shapes.medium ) { Column { Box(modifier = Modifier.fillMaxWidth()) { @@ -344,22 +342,15 @@ fun GlobalSearchView( } else if (viewModel.searchListPublisher.isNotEmpty()) { items(viewModel.searchListPublisher) { i -> Surface( - modifier = Modifier.clickable { + tonalElevation = 4.dp, + shape = M3MaterialTheme.shapes.medium, + onClick = { searchModelBottom = i scope.launch { bottomScaffold.bottomSheetState.expand() } - }, - tonalElevation = 4.dp, - shape = M3MaterialTheme.shapes.medium + } ) { Column { - Box( - modifier = Modifier - .fillMaxWidth() - .clickable { - searchModelBottom = i - scope.launch { bottomScaffold.bottomSheetState.expand() } - } - ) { + Box(modifier = Modifier.fillMaxWidth()) { Text( i.apiName, modifier = Modifier @@ -496,7 +487,7 @@ private fun GlobalScreenPreview() { dao = dao, viewModel = viewModel { GlobalSearchViewModel( - info = MockInfo, + sourceRepository = SourceRepository(), initialSearch = "", dao = dao, ) diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/globalsearch/GlobalSearchViewModel.kt b/UIViews/src/main/java/com/programmersbox/uiviews/globalsearch/GlobalSearchViewModel.kt index 0589309be..b3d00bab1 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/globalsearch/GlobalSearchViewModel.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/globalsearch/GlobalSearchViewModel.kt @@ -5,19 +5,24 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.HistoryDao import com.programmersbox.models.ItemModel -import com.programmersbox.uiviews.GenericInfo import com.programmersbox.uiviews.utils.dispatchIoAndCatchList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import ru.beryukhov.reactivenetwork.ReactiveNetwork class GlobalSearchViewModel( - val info: GenericInfo, + val sourceRepository: SourceRepository, val dao: HistoryDao, initialSearch: String ) : ViewModel() { @@ -44,12 +49,13 @@ class GlobalSearchViewModel( isSearching = true searchListPublisher = emptyList() async { - info.searchList() + sourceRepository.list .apmap { a -> a + .apiService .searchSourceList(searchText, list = emptyList()) .dispatchIoAndCatchList() - .map { SearchModel(a.serviceName, it) } + .map { SearchModel(a.apiService.serviceName, it) } .filter { it.data.isNotEmpty() } .onEach { searchListPublisher = searchListPublisher + it } .onCompletion { isRefreshing = false } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/history/HistoryFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/history/HistoryFragment.kt index aa72b658f..ac289231b 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/history/HistoryFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/history/HistoryFragment.kt @@ -23,6 +23,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text @@ -63,9 +66,9 @@ import com.programmersbox.uiviews.utils.ComposableUtils import com.programmersbox.uiviews.utils.InsetMediumTopAppBar import com.programmersbox.uiviews.utils.LightAndDarkPreviews import com.programmersbox.uiviews.utils.LoadingDialog -import com.programmersbox.uiviews.utils.LocalGenericInfo import com.programmersbox.uiviews.utils.LocalHistoryDao import com.programmersbox.uiviews.utils.LocalNavController +import com.programmersbox.uiviews.utils.LocalSourcesRepository import com.programmersbox.uiviews.utils.LocalSystemDateTimeFormat import com.programmersbox.uiviews.utils.MockAppIcon import com.programmersbox.uiviews.utils.OtakuScaffold @@ -92,6 +95,7 @@ fun HistoryUi( val recentItems = hm.historyItems.collectAsLazyPagingItems() val recentSize by hm.historyCount.collectAsState(initial = 0) val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } var clearAllDialog by remember { mutableStateOf(false) } @@ -117,6 +121,7 @@ fun HistoryUi( OtakuScaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { InsetMediumTopAppBar( scrollBehavior = scrollBehavior, @@ -129,16 +134,6 @@ fun HistoryUi( ) } ) { p -> - - /*AnimatedLazyColumn( - contentPadding = p, - verticalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier.padding(vertical = 4.dp), - items = recentItems.itemSnapshotList.fastMap { item -> - AnimatedLazyListItem(key = item!!.url, value = item) { HistoryItem(item, scope) } - } - )*/ - LazyColumn( contentPadding = p, verticalArrangement = Arrangement.spacedBy(4.dp) @@ -150,7 +145,21 @@ fun HistoryUi( ) { val item = recentItems[it] if (item != null) { - HistoryItem(item, dao, logo, scope) + HistoryItem( + item = item, + dao = dao, + logo = logo, + scope = scope, + onError = { + scope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + snackbarHostState.showSnackbar( + "Something went wrong. Source might not be installed", + duration = SnackbarDuration.Short + ) + } + } + ) } else { HistoryItemPlaceholder() } @@ -161,7 +170,7 @@ fun HistoryUi( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun HistoryItem(item: RecentModel, dao: HistoryDao, logo: MainLogo, scope: CoroutineScope) { +private fun HistoryItem(item: RecentModel, dao: HistoryDao, logo: MainLogo, scope: CoroutineScope, onError: () -> Unit) { var showPopup by remember { mutableStateOf(false) } if (showPopup) { @@ -237,7 +246,7 @@ private fun HistoryItem(item: RecentModel, dao: HistoryDao, logo: MainLogo, scop val context = LocalContext.current val logoDrawable = remember { AppCompatResources.getDrawable(context, logo.logoId) } - val info = LocalGenericInfo.current + val info = LocalSourcesRepository.current val navController = LocalNavController.current Surface( @@ -245,7 +254,8 @@ private fun HistoryItem(item: RecentModel, dao: HistoryDao, logo: MainLogo, scop shape = MaterialTheme.shapes.medium, onClick = { scope.launch { - info.toSource(item.source) + info.toSourceByApiServiceName(item.source) + ?.apiService ?.getSourceByUrlFlow(item.url) ?.dispatchIo() ?.onStart { showLoadingDialog = true } @@ -257,7 +267,7 @@ private fun HistoryItem(item: RecentModel, dao: HistoryDao, logo: MainLogo, scop showLoadingDialog = false navController.navigateToDetails(m) } - ?.collect() + ?.collect() ?: onError() } } ) { @@ -282,7 +292,8 @@ private fun HistoryItem(item: RecentModel, dao: HistoryDao, logo: MainLogo, scop IconButton( onClick = { scope.launch { - info.toSource(item.source) + info.toSourceByApiServiceName(item.source) + ?.apiService ?.getSourceByUrlFlow(item.url) ?.dispatchIo() ?.onStart { showLoadingDialog = true } @@ -294,7 +305,7 @@ private fun HistoryItem(item: RecentModel, dao: HistoryDao, logo: MainLogo, scop showLoadingDialog = false navController.navigateToDetails(m) } - ?.collect() + ?.collect() ?: onError() } } ) { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) } @@ -364,7 +375,8 @@ private fun HistoryItemPreview() { ), dao = LocalHistoryDao.current, logo = MockAppIcon, - scope = rememberCoroutineScope() + scope = rememberCoroutineScope(), + onError = {} ) } } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/lists/ImportListViewModel.kt b/UIViews/src/main/java/com/programmersbox/uiviews/lists/ImportListViewModel.kt index 8e4a78182..17ee009fe 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/lists/ImportListViewModel.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/lists/ImportListViewModel.kt @@ -54,7 +54,7 @@ class ImportListViewModel( } sealed class ImportListStatus { - object Loading : ImportListStatus() + data object Loading : ImportListStatus() class Error(val throwable: Throwable) : ImportListStatus() class Success(val customList: CustomList?) : ImportListStatus() } \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuCustomListScreen.kt b/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuCustomListScreen.kt index 5fff446a1..ea5248542 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuCustomListScreen.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuCustomListScreen.kt @@ -32,11 +32,11 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.DismissDirection import androidx.compose.material3.DismissValue -import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -44,12 +44,14 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuDefaults import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SearchBar +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.Surface import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberDismissState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -84,8 +86,8 @@ import com.programmersbox.uiviews.utils.InsetSmallTopAppBar import com.programmersbox.uiviews.utils.LightAndDarkPreviews import com.programmersbox.uiviews.utils.LoadingDialog import com.programmersbox.uiviews.utils.LocalCustomListDao -import com.programmersbox.uiviews.utils.LocalGenericInfo import com.programmersbox.uiviews.utils.LocalNavController +import com.programmersbox.uiviews.utils.LocalSourcesRepository import com.programmersbox.uiviews.utils.MockAppIcon import com.programmersbox.uiviews.utils.PreviewTheme import com.programmersbox.uiviews.utils.Screen @@ -116,6 +118,7 @@ fun OtakuCustomListScreen( val navController = LocalNavController.current val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val customItem = vm.customItem + val state = rememberBottomSheetScaffoldState() val logoDrawable = remember { AppCompatResources.getDrawable(context, logo.logoId) } @@ -208,6 +211,7 @@ fun OtakuCustomListScreen( onRemove = { vm.removeItem(it) }, onMultipleRemove = { it.forEach { i -> vm.removeItem(i) } }, bottomScrollBehavior = scrollBehavior, + state = state, topBar = { Surface { Column { @@ -303,7 +307,7 @@ fun OtakuCustomListScreen( } ) if (index != vm.items.lastIndex) { - Divider() + HorizontalDivider() } } } @@ -346,6 +350,15 @@ fun OtakuCustomListScreen( logo = logoDrawable, showLoadingDialog = { showLoadingDialog = it }, onDelete = { vm.removeItem(it) }, + onError = { + scope.launch { + state.snackbarHostState.currentSnackbarData?.dismiss() + state.snackbarHostState.showSnackbar( + "Something went wrong. Source might not be installed", + duration = SnackbarDuration.Short + ) + } + }, modifier = Modifier.animateItemPlacement() ) } @@ -360,10 +373,11 @@ private fun CustomItem( logo: Drawable?, onDelete: (CustomListInfo) -> Unit, showLoadingDialog: (Boolean) -> Unit, - modifier: Modifier = Modifier + onError: () -> Unit, + modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() - val genericInfo = LocalGenericInfo.current + val sourceRepository = LocalSourcesRepository.current val navController = LocalNavController.current var showPopup by remember { mutableStateOf(false) } @@ -426,8 +440,9 @@ private fun CustomItem( dismissContent = { ElevatedCard( onClick = { - genericInfo - .toSource(item.source) + sourceRepository + .toSourceByApiServiceName(item.source) + ?.apiService ?.let { source -> Cached.cache[item.url]?.let { flow { @@ -446,7 +461,7 @@ private fun CustomItem( navController.navigateToDetails(it) } ?.onCompletion { showLoadingDialog(false) } - ?.launchIn(scope) + ?.launchIn(scope) ?: onError() }, modifier = Modifier .height(ComposableUtils.IMAGE_HEIGHT) @@ -489,7 +504,7 @@ private fun CustomItem( } ) - Divider() + HorizontalDivider() DropdownMenuItem( text = { Text(stringResource(R.string.remove)) }, @@ -539,7 +554,8 @@ private fun CustomItemPreview() { ), logo = null, onDelete = {}, - showLoadingDialog = {} + showLoadingDialog = {}, + onError = {} ) } } \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuListScreen.kt b/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuListScreen.kt index e4270a57b..17155ac89 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuListScreen.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/lists/OtakuListScreen.kt @@ -14,9 +14,9 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.FileDownload import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Divider import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -144,7 +144,7 @@ fun OtakuListScreen( } ) } - Divider(Modifier.padding(top = 4.dp)) + HorizontalDivider(Modifier.padding(top = 4.dp)) } } } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/notifications/NotificationFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/notifications/NotificationFragment.kt index 6989033d3..bdb78224d 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/notifications/NotificationFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/notifications/NotificationFragment.kt @@ -34,17 +34,18 @@ import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.DismissDirection import androidx.compose.material3.DismissValue -import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconToggleButton import androidx.compose.material3.ListItem import androidx.compose.material3.SelectableDates import androidx.compose.material3.SheetValue +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.Surface import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text @@ -82,17 +83,19 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.android.material.datepicker.DateValidatorPointForward +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.ItemDao import com.programmersbox.favoritesdatabase.NotificationItem import com.programmersbox.favoritesdatabase.toDbModel import com.programmersbox.favoritesdatabase.toItemModel import com.programmersbox.gsonutils.toJson +import com.programmersbox.models.ApiService import com.programmersbox.sharedutils.MainLogo import com.programmersbox.uiviews.GenericInfo import com.programmersbox.uiviews.NotificationSortBy -import com.programmersbox.uiviews.NotifySingleWorker import com.programmersbox.uiviews.R -import com.programmersbox.uiviews.SavedNotifications +import com.programmersbox.uiviews.checkers.NotifySingleWorker +import com.programmersbox.uiviews.checkers.SavedNotifications import com.programmersbox.uiviews.utils.BackButton import com.programmersbox.uiviews.utils.Cached import com.programmersbox.uiviews.utils.ComposableUtils @@ -103,6 +106,7 @@ import com.programmersbox.uiviews.utils.LocalGenericInfo import com.programmersbox.uiviews.utils.LocalItemDao import com.programmersbox.uiviews.utils.LocalNavController import com.programmersbox.uiviews.utils.LocalSettingsHandling +import com.programmersbox.uiviews.utils.LocalSourcesRepository import com.programmersbox.uiviews.utils.LocalSystemDateTimeFormat import com.programmersbox.uiviews.utils.MockAppIcon import com.programmersbox.uiviews.utils.MockInfo @@ -144,9 +148,10 @@ fun NotificationsScreen( notificationLogo: NotificationLogo, navController: NavController = LocalNavController.current, genericInfo: GenericInfo = LocalGenericInfo.current, + sourceRepository: SourceRepository = LocalSourcesRepository.current, db: ItemDao = LocalItemDao.current, settingsHandling: SettingsHandling = LocalSettingsHandling.current, - vm: NotificationScreenViewModel = viewModel { NotificationScreenViewModel(db, settingsHandling, genericInfo) }, + vm: NotificationScreenViewModel = viewModel { NotificationScreenViewModel(db, settingsHandling, sourceRepository) }, cancelNotificationById: (Int) -> Unit, cancelNotification: (NotificationItem) -> Unit, ) { @@ -275,6 +280,17 @@ fun NotificationsScreen( genericInfo = genericInfo, logoDrawable = logoDrawable, notificationLogo = notificationLogo, + toSource = { s -> sourceRepository.toSourceByApiServiceName(s)?.apiService }, + sourceRepository = sourceRepository, + onError = { + scope.launch { + state.snackbarHostState.currentSnackbarData?.dismiss() + state.snackbarHostState.showSnackbar( + "Something went wrong. Source might not be installed", + duration = SnackbarDuration.Short + ) + } + }, modifier = Modifier.animateItemPlacement(), ) } @@ -295,8 +311,8 @@ fun NotificationsScreen( Surface( shape = M3MaterialTheme.shapes.medium, tonalElevation = 4.dp, - modifier = Modifier.fillMaxWidth(), - onClick = { vm.toggleGroupedState(item.first) } + onClick = { vm.toggleGroupedState(item.first) }, + modifier = Modifier.fillMaxWidth() ) { ListItem( modifier = Modifier.padding(4.dp), @@ -320,7 +336,7 @@ fun NotificationsScreen( exit = shrinkVertically() ) { Column( - verticalArrangement = Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.spacedBy(4.dp), ) { item.second.forEach { NotificationItem( @@ -331,7 +347,18 @@ fun NotificationsScreen( db = db, genericInfo = genericInfo, logoDrawable = logoDrawable, - notificationLogo = notificationLogo + notificationLogo = notificationLogo, + toSource = { s -> sourceRepository.toSourceByApiServiceName(s)?.apiService }, + sourceRepository = sourceRepository, + onError = { + scope.launch { + state.snackbarHostState.currentSnackbarData?.dismiss() + state.snackbarHostState.showSnackbar( + "Something went wrong. Source might not be installed", + duration = SnackbarDuration.Short + ) + } + } ) } } @@ -383,8 +410,11 @@ private fun NotificationItem( cancelNotification: (NotificationItem) -> Unit, db: ItemDao, genericInfo: GenericInfo, + toSource: (String) -> ApiService?, logoDrawable: Drawable?, notificationLogo: NotificationLogo, + onError: () -> Unit, + sourceRepository: SourceRepository, modifier: Modifier = Modifier, ) { @@ -466,8 +496,7 @@ private fun NotificationItem( dismissContent = { ElevatedCard( onClick = { - genericInfo - .toSource(item.source) + toSource(item.source) ?.let { source -> Cached.cache[item.url]?.let { flow { @@ -485,7 +514,7 @@ private fun NotificationItem( showLoadingDialog = false navController.navigateToDetails(it) } - ?.launchIn(scope) + ?.launchIn(scope) ?: onError() }, modifier = Modifier.padding(horizontal = 4.dp) ) { @@ -516,19 +545,25 @@ private fun NotificationItem( onClick = { dropDownDismiss() scope.launch(Dispatchers.IO) { - SavedNotifications.viewNotificationFromDb(context, item, notificationLogo, genericInfo) + SavedNotifications.viewNotificationFromDb( + context = context, + n = item, + notificationLogo = notificationLogo, + info = genericInfo, + sourceRepository = sourceRepository + ) } } ) - Divider() + HorizontalDivider() NotifyAt( item = item, onDropDownDismiss = dropDownDismiss ) - Divider() + HorizontalDivider() DropdownMenuItem( onClick = { @@ -552,7 +587,7 @@ private fun NotificationItem( @Composable private fun NotifyAt( item: NotificationItem, - onDropDownDismiss: () -> Unit + onDropDownDismiss: () -> Unit, ) { val context = LocalContext.current var showDatePicker by remember { mutableStateOf(false) } @@ -653,7 +688,7 @@ private fun NotifyAt( private fun NotificationDeleteItem( item: NotificationItem, logoDrawable: Drawable?, - onRemoveAllWithSameName: () -> Unit + onRemoveAllWithSameName: () -> Unit, ) { ImageFlushListItem( leadingContent = { @@ -714,7 +749,10 @@ private fun NotificationItemPreview() { genericInfo = MockInfo, cancelNotification = {}, logoDrawable = null, - notificationLogo = NotificationLogo(R.drawable.ic_site_settings) + notificationLogo = NotificationLogo(R.drawable.ic_site_settings), + toSource = { null }, + onError = {}, + sourceRepository = SourceRepository() ) } } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/notifications/NotificationScreenViewModel.kt b/UIViews/src/main/java/com/programmersbox/uiviews/notifications/NotificationScreenViewModel.kt index ec8a7afb1..0b8acb0f3 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/notifications/NotificationScreenViewModel.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/notifications/NotificationScreenViewModel.kt @@ -1,11 +1,16 @@ package com.programmersbox.uiviews.notifications -import androidx.compose.runtime.* +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.ItemDao import com.programmersbox.favoritesdatabase.NotificationItem -import com.programmersbox.uiviews.GenericInfo import com.programmersbox.uiviews.NotificationSortBy import com.programmersbox.uiviews.utils.SettingsHandling import kotlinx.coroutines.Dispatchers @@ -17,7 +22,7 @@ import kotlinx.coroutines.withContext class NotificationScreenViewModel( db: ItemDao, private val settingsHandling: SettingsHandling, - genericInfo: GenericInfo, + sourceRepository: SourceRepository, ) : ViewModel() { val items = mutableStateListOf() @@ -29,7 +34,7 @@ class NotificationScreenViewModel( } val groupedListState = mutableStateMapOf( - *genericInfo.sourceList().map { it.serviceName to mutableStateOf(false) }.toTypedArray() + *sourceRepository.list.map { it.apiService.serviceName to mutableStateOf(false) }.toTypedArray() ) init { @@ -37,6 +42,13 @@ class NotificationScreenViewModel( .onEach { items.clear() items.addAll(it) + val l = groupedListState.toMap() + groupedListState.clear() + groupedListState.putAll( + it + .groupBy { n -> n.source } + .mapValues { n -> l[n.key] ?: mutableStateOf(false) } + ) } .launchIn(viewModelScope) diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentFragment.kt index e8afbe018..ab34ec85d 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentFragment.kt @@ -54,22 +54,26 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.ItemDao -import com.programmersbox.models.sourceFlow import com.programmersbox.sharedutils.MainLogo +import com.programmersbox.uiviews.CurrentSourceRepository import com.programmersbox.uiviews.R import com.programmersbox.uiviews.utils.ComponentState import com.programmersbox.uiviews.utils.InsetSmallTopAppBar import com.programmersbox.uiviews.utils.LightAndDarkPreviews +import com.programmersbox.uiviews.utils.LocalCurrentSource import com.programmersbox.uiviews.utils.LocalGenericInfo import com.programmersbox.uiviews.utils.LocalItemDao import com.programmersbox.uiviews.utils.LocalNavController +import com.programmersbox.uiviews.utils.LocalSourcesRepository import com.programmersbox.uiviews.utils.MockAppIcon import com.programmersbox.uiviews.utils.OtakuBannerBox import com.programmersbox.uiviews.utils.OtakuScaffold import com.programmersbox.uiviews.utils.PreviewTheme import com.programmersbox.uiviews.utils.Screen import com.programmersbox.uiviews.utils.components.InfiniteListHandler +import com.programmersbox.uiviews.utils.components.NoSourcesInstalled import com.programmersbox.uiviews.utils.currentService import com.programmersbox.uiviews.utils.navigateToDetails import kotlinx.coroutines.flow.filter @@ -85,7 +89,9 @@ fun RecentView( logo: MainLogo, dao: ItemDao = LocalItemDao.current, context: Context = LocalContext.current, - recentVm: RecentViewModel = viewModel { RecentViewModel(dao, context) }, + sourceRepository: SourceRepository = LocalSourcesRepository.current, + currentSourceRepository: CurrentSourceRepository = LocalCurrentSource.current, + recentVm: RecentViewModel = viewModel { RecentViewModel(dao, context, sourceRepository, currentSourceRepository) }, ) { val info = LocalGenericInfo.current val navController = LocalNavController.current @@ -103,20 +109,20 @@ fun RecentView( val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val showButton by remember { derivedStateOf { state.firstVisibleItemIndex > 0 } } - val sourceList = remember { info.sourceList() } - val initSource = remember(source) { sourceList.indexOf(source) } + val sourceList = recentVm.sources + val initSource = remember(source) { sourceList.indexOfFirst { it.apiService == source } } val pagerState = rememberPagerState( initialPage = initSource.coerceAtLeast(0), initialPageOffsetFraction = 0f - ) { info.sourceList().size } + ) { sourceList.size } LaunchedEffect(initSource) { if (initSource != -1) pagerState.scrollToPage(initSource) } LaunchedEffect(pagerState.currentPage, initSource) { if (initSource != -1) { sourceList.getOrNull(pagerState.currentPage)?.let { service -> - sourceFlow.emit(service) - context.currentService = service.serviceName + currentSourceRepository.emit(service.apiService) + context.currentService = service.apiService.serviceName } } } @@ -144,7 +150,7 @@ fun RecentView( }, label = "" ) { targetState -> - Text(stringResource(R.string.currentSource, sourceList.getOrNull(targetState)?.serviceName.orEmpty())) + Text(stringResource(R.string.currentSource, sourceList.getOrNull(targetState)?.apiService?.serviceName.orEmpty())) } }, actions = { @@ -203,6 +209,7 @@ fun RecentView( modifier = Modifier.pullRefresh(pull) ) { when { + sourceList.isEmpty() -> NoSourcesInstalled(Modifier.fillMaxSize()) recentVm.sourceList.isEmpty() -> info.ComposeShimmerItem() else -> { info.ItemListView( diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentViewModel.kt b/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentViewModel.kt index 0d4b50e07..231675a51 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentViewModel.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentViewModel.kt @@ -11,12 +11,14 @@ import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.util.fastMaxBy import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.DbModel import com.programmersbox.favoritesdatabase.ItemDao import com.programmersbox.models.ApiService import com.programmersbox.models.ItemModel -import com.programmersbox.models.sourceFlow +import com.programmersbox.models.SourceInformation import com.programmersbox.sharedutils.FirebaseDb +import com.programmersbox.uiviews.CurrentSourceRepository import com.programmersbox.uiviews.utils.dispatchIoAndCatchList import com.programmersbox.uiviews.utils.showErrorToast import kotlinx.coroutines.Dispatchers @@ -31,7 +33,12 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import ru.beryukhov.reactivenetwork.ReactiveNetwork -class RecentViewModel(dao: ItemDao, context: Context? = null) : ViewModel() { +class RecentViewModel( + dao: ItemDao, + context: Context? = null, + sourceRepository: SourceRepository, + currentSourceRepository: CurrentSourceRepository, +) : ViewModel() { var isRefreshing by mutableStateOf(false) val sourceList = mutableStateListOf() @@ -49,7 +56,16 @@ class RecentViewModel(dao: ItemDao, context: Context? = null) : ViewModel() { val gridState = LazyGridState(0, 0) + val sources = mutableStateListOf() + init { + sourceRepository.sources + .onEach { + sources.clear() + sources.addAll(it) + } + .launchIn(viewModelScope) + combine( itemListener.getAllShowsFlow(), dao.getAllFavorites() @@ -57,7 +73,7 @@ class RecentViewModel(dao: ItemDao, context: Context? = null) : ViewModel() { .onEach { favoriteList = it.toMutableStateList() } .launchIn(viewModelScope) - sourceFlow + currentSourceRepository.asFlow() .filterNotNull() .onEach { currentSource = it @@ -90,6 +106,7 @@ class RecentViewModel(dao: ItemDao, context: Context? = null) : ViewModel() { ?.getRecentFlow(count) ?.dispatchIoAndCatchList() ?.catch { + it.printStackTrace() context?.showErrorToast() emit(emptyList()) } @@ -103,5 +120,4 @@ class RecentViewModel(dao: ItemDao, context: Context? = null) : ViewModel() { super.onCleared() itemListener.unregister() } - } \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/settings/ExtensionListScreen.kt b/UIViews/src/main/java/com/programmersbox/uiviews/settings/ExtensionListScreen.kt new file mode 100644 index 000000000..fa57fb062 --- /dev/null +++ b/UIViews/src/main/java/com/programmersbox/uiviews/settings/ExtensionListScreen.kt @@ -0,0 +1,473 @@ +package com.programmersbox.uiviews.settings + +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Extension +import androidx.compose.material.icons.filled.InstallMobile +import androidx.compose.material.icons.filled.SendTimeExtension +import androidx.compose.material.icons.filled.Update +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LeadingIconTab +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import coil.compose.AsyncImage +import com.google.accompanist.drawablepainter.rememberDrawablePainter +import com.programmersbox.extensionloader.SourceRepository +import com.programmersbox.models.ApiServicesCatalog +import com.programmersbox.models.RemoteSources +import com.programmersbox.models.SourceInformation +import com.programmersbox.sharedutils.AppUpdate +import com.programmersbox.uiviews.OtakuWorldCatalog +import com.programmersbox.uiviews.R +import com.programmersbox.uiviews.all.pagerTabIndicatorOffset +import com.programmersbox.uiviews.checkers.SourceUpdateChecker +import com.programmersbox.uiviews.utils.BackButton +import com.programmersbox.uiviews.utils.DownloadAndInstaller +import com.programmersbox.uiviews.utils.InsetSmallTopAppBar +import com.programmersbox.uiviews.utils.LightAndDarkPreviews +import com.programmersbox.uiviews.utils.LocalCurrentSource +import com.programmersbox.uiviews.utils.LocalSourcesRepository +import com.programmersbox.uiviews.utils.OtakuScaffold +import com.programmersbox.uiviews.utils.PreviewTheme +import kotlinx.coroutines.launch +import org.koin.compose.koinInject + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +fun ExtensionList( + sourceRepository: SourceRepository = LocalSourcesRepository.current, + otakuWorldCatalog: OtakuWorldCatalog = koinInject(), + viewModel: ExtensionListViewModel = viewModel { ExtensionListViewModel(sourceRepository, otakuWorldCatalog) }, +) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState( + initialPage = 0, + initialPageOffsetFraction = 0f + ) { 2 } + + val context = LocalContext.current + val downloadAndInstall = remember { DownloadAndInstaller(context) } + + fun updateCheck() { + WorkManager.getInstance(context) + .enqueueUniqueWork( + "sourceCheck", + ExistingWorkPolicy.KEEP, + OneTimeWorkRequestBuilder() + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(false) + .setRequiresCharging(false) + .setRequiresDeviceIdle(false) + .setRequiresStorageNotLow(false) + .build() + ) + .build() + ) + } + + OtakuScaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + Column { + InsetSmallTopAppBar( + title = { Text(stringResource(R.string.extensions)) }, + navigationIcon = { BackButton() }, + actions = { IconButton(onClick = ::updateCheck) { Icon(Icons.Default.Update, null) } }, + scrollBehavior = scrollBehavior, + ) + TabRow( + // Our selected tab is our current page + selectedTabIndex = pagerState.currentPage, + // Override the indicator, using the provided pagerTabIndicatorOffset modifier + indicator = { tabPositions -> + TabRowDefaults.SecondaryIndicator( + Modifier.pagerTabIndicatorOffset(pagerState, tabPositions) + ) + } + ) { + // Add tabs for all of our pages + LeadingIconTab( + text = { Text(stringResource(R.string.installed)) }, + selected = pagerState.currentPage == 0, + onClick = { scope.launch { pagerState.animateScrollToPage(0) } }, + icon = { Icon(Icons.Default.Extension, null) } + ) + + LeadingIconTab( + text = { Text(stringResource(R.string.extensions)) }, + selected = pagerState.currentPage == 1, + onClick = { scope.launch { pagerState.animateScrollToPage(1) } }, + icon = { Icon(Icons.Default.SendTimeExtension, null) } + ) + } + } + }, + ) { paddingValues -> + Crossfade( + targetState = viewModel.installed.isEmpty(), + label = "", + ) { target -> + when (target) { + true -> { + Box( + Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + RemoteExtensionItems( + remoteSources = viewModel.remoteSources, + onDownloadAndInstall = { downloadLink, destinationPath -> + downloadAndInstall.downloadAndInstall(downloadLink, destinationPath) + } + ) + } + } + + false -> { + HorizontalPager( + state = pagerState, + contentPadding = paddingValues, + ) { page -> + when (page) { + 0 -> InstalledExtensionItems( + installedSources = viewModel.installed, + sourcesList = viewModel.remoteSourcesVersions, + onDownloadAndInstall = { downloadLink, destinationPath -> + downloadAndInstall.downloadAndInstall(downloadLink, destinationPath) + } + ) + + 1 -> RemoteExtensionItems( + remoteSources = viewModel.remoteSources, + onDownloadAndInstall = { downloadLink, destinationPath -> + downloadAndInstall.downloadAndInstall(downloadLink, destinationPath) + } + ) + } + } + } + } + } + + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun InstalledExtensionItems( + installedSources: Map, + sourcesList: List, + onDownloadAndInstall: (String, String) -> Unit, +) { + val currentSourceRepository = LocalCurrentSource.current + val context = LocalContext.current + fun uninstall(packageName: String) { + val uri = Uri.fromParts("package", packageName, null) + val uninstall = Intent(Intent.ACTION_DELETE, uri) + context.startActivity(uninstall) + } + Column { + ListItem( + headlineContent = { + val source by LocalCurrentSource.current.asFlow().collectAsState(initial = null) + Text(stringResource(R.string.currentSource, source?.serviceName.orEmpty())) + } + ) + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.fillMaxSize() + ) { + installedSources.forEach { (t, u) -> + stickyHeader { + Surface( + shape = MaterialTheme.shapes.medium, + tonalElevation = 4.dp, + onClick = { u.showItems = !u.showItems }, + modifier = Modifier.fillMaxWidth() + ) { + ListItem( + modifier = Modifier.padding(4.dp), + headlineContent = { + Text(t?.name ?: u.sourceInformation.firstOrNull()?.name?.takeIf { t != null } ?: "Single Source") + }, + leadingContent = { Text("(${u.sourceInformation.size})") }, + trailingContent = t?.let { + { + IconButton( + onClick = { uninstall(u.sourceInformation.random().packageName) } + ) { Icon(Icons.Default.Delete, null) } + } + } + ) + } + } + + if (u.showItems) { + items( + u.sourceInformation, + key = { it.apiService.serviceName }, + contentType = { it } + ) { source -> + val version = remember(context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.getPackageInfo( + source.packageName, + PackageManager.PackageInfoFlags.of(0L) + ) + } else { + context.packageManager.getPackageInfo(source.packageName, 0) + } + ?.versionName + .orEmpty() + } + ExtensionItem( + sourceInformation = source, + version = version, + onClick = { currentSourceRepository.tryEmit(source.apiService) }, + trailingIcon = { + Row { + sourcesList.find { + it.sources.any { s -> s.baseUrl == source.apiService.baseUrl } + } + ?.let { r -> + if (AppUpdate.checkForUpdate(version, r.version)) { + IconButton( + onClick = { + onDownloadAndInstall( + r.downloadLink, + r.downloadLink.toUri().lastPathSegment ?: "${r.name}.apk" + ) + } + ) { Icon(Icons.Default.Update, null) } + } + } + IconButton( + onClick = { uninstall(source.packageName) } + ) { Icon(Icons.Default.Delete, null) } + } + } + ) + } + } + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@Composable +private fun RemoteExtensionItems( + remoteSources: Map, + onDownloadAndInstall: (String, String) -> Unit, +) { + Column { + var search by remember { mutableStateOf("") } + OutlinedTextField( + value = search, + onValueChange = { search = it }, + label = { Text("Search Remote Extensions") }, + trailingIcon = { + IconButton(onClick = { search = "" }) { Icon(Icons.Default.Clear, null) } + }, + modifier = Modifier.fillMaxWidth() + ) + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.fillMaxSize() + ) { + remoteSources.forEach { (t, u) -> + stickyHeader { + InsetSmallTopAppBar( + title = { Text(t) }, + insetPadding = WindowInsets(0.dp), + navigationIcon = { Text("(${u.sources.size})") }, + actions = { + IconButton( + onClick = { u.showItems = !u.showItems } + ) { + Icon( + Icons.Default.ArrowDropDown, + null, + modifier = Modifier.rotateWithBoolean(u.showItems) + ) + } + } + ) + } + + if (u.showItems) { + items(u.sources.filter { it.name.contains(search, true) }) { + RemoteItem( + remoteSource = it, + onDownloadAndInstall = { + onDownloadAndInstall(it.downloadLink, it.downloadLink.toUri().lastPathSegment ?: "${it.name}.apk") + }, + modifier = Modifier.animateItemPlacement() + ) + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ExtensionItem( + sourceInformation: SourceInformation, + version: String, + onClick: () -> Unit, + trailingIcon: (@Composable () -> Unit)?, + modifier: Modifier = Modifier, +) { + OutlinedCard( + onClick = onClick, + modifier = modifier + ) { + ListItem( + overlineContent = { Text("Version: $version") }, + headlineContent = { Text(sourceInformation.apiService.serviceName) }, + leadingContent = { Image(rememberDrawablePainter(drawable = sourceInformation.icon), null) }, + trailingContent = trailingIcon + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RemoteItem( + remoteSource: RemoteSources, + onDownloadAndInstall: () -> Unit, + modifier: Modifier = Modifier, +) { + var showDialog by remember { mutableStateOf(false) } + if (showDialog) { + AlertDialog( + onDismissRequest = { showDialog = false }, + title = { Text("Download and Install ${remoteSource.name}?") }, + //icon = { AsyncImage(model = remoteSource.iconUrl, contentDescription = null) }, + text = { Text("Are you sure?") }, + confirmButton = { + TextButton( + onClick = { + onDownloadAndInstall() + showDialog = false + } + ) { Text("Yes") } + }, + dismissButton = { + TextButton( + onClick = { showDialog = false } + ) { Text("No") } + } + ) + } + var showSources by remember { mutableStateOf(false) } + OutlinedCard( + onClick = { showSources = !showSources }, + modifier = modifier.animateContentSize() + ) { + ListItem( + headlineContent = { Text(remoteSource.name) }, + leadingContent = { AsyncImage(model = remoteSource.iconUrl, contentDescription = null) }, + overlineContent = { Text("Version: ${remoteSource.version}") }, + supportingContent = { + AnimatedVisibility(visible = showSources) { + Column { + remoteSource.sources.forEach { + ListItem( + headlineContent = { Text(it.name) }, + overlineContent = { Text("Version: ${it.version}") } + ) + HorizontalDivider() + } + } + } + }, + trailingContent = { + Row { + IconButton( + onClick = { showSources = !showSources } + ) { Icon(Icons.Default.ArrowDropDown, null, modifier = Modifier.rotateWithBoolean(showSources)) } + + IconButton( + onClick = { showDialog = true } + ) { Icon(Icons.Default.InstallMobile, null) } + } + }, + modifier = Modifier.animateContentSize() + ) + } +} + +private fun Modifier.rotateWithBoolean(shouldRotate: Boolean) = composed { + rotate(animateFloatAsState(targetValue = if (shouldRotate) 180f else 0f, label = "").value) +} + +@LightAndDarkPreviews +@Composable +private fun ExtensionListPreview() { + PreviewTheme { + ExtensionList() + } +} \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/settings/ExtensionListViewModel.kt b/UIViews/src/main/java/com/programmersbox/uiviews/settings/ExtensionListViewModel.kt new file mode 100644 index 000000000..b46703255 --- /dev/null +++ b/UIViews/src/main/java/com/programmersbox/uiviews/settings/ExtensionListViewModel.kt @@ -0,0 +1,69 @@ +package com.programmersbox.uiviews.settings + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.programmersbox.extensionloader.SourceRepository +import com.programmersbox.models.ExternalApiServicesCatalog +import com.programmersbox.models.RemoteSources +import com.programmersbox.models.SourceInformation +import com.programmersbox.uiviews.OtakuWorldCatalog +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class ExtensionListViewModel( + sourceRepository: SourceRepository, + otakuWorldCatalog: OtakuWorldCatalog +) : ViewModel() { + private val installedSources = mutableStateListOf() + val remoteSources = mutableStateMapOf() + + val installed by derivedStateOf { + installedSources + .groupBy { it.catalog } + .mapValues { InstalledViewState(it.value) } + } + + val remoteSourcesVersions by derivedStateOf { + remoteSources.values.flatMap { it.sources } + } + + init { + sourceRepository.sources + .onEach { + installedSources.clear() + installedSources.addAll(it) + } + .onEach { sources -> + remoteSources.clear() + remoteSources["${otakuWorldCatalog.name}World"] = RemoteViewState(otakuWorldCatalog.getRemoteSources()) + remoteSources.putAll( + sources.asSequence() + .map { it.catalog } + .filterIsInstance() + .filter { it.hasRemoteSources } + .distinct() + .toList() + .associate { it.name to RemoteViewState(it.getRemoteSources()) } + ) + } + .launchIn(viewModelScope) + } +} + +class InstalledViewState( + val sourceInformation: List +) { + var showItems by mutableStateOf(false) +} + +class RemoteViewState( + val sources: List +) { + var showItems by mutableStateOf(false) +} \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/settings/InfoSettings.kt b/UIViews/src/main/java/com/programmersbox/uiviews/settings/InfoSettings.kt index 3427458f7..a6848ecb2 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/settings/InfoSettings.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/settings/InfoSettings.kt @@ -13,6 +13,7 @@ import androidx.compose.material.icons.filled.LibraryBooks import androidx.compose.material.icons.filled.SystemUpdateAlt import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -35,7 +36,7 @@ import com.programmersbox.sharedutils.AppUpdate import com.programmersbox.sharedutils.MainLogo import com.programmersbox.sharedutils.updateAppCheck import com.programmersbox.uiviews.R -import com.programmersbox.uiviews.utils.DownloadUpdate +import com.programmersbox.uiviews.utils.DownloadAndInstaller import com.programmersbox.uiviews.utils.LightAndDarkPreviews import com.programmersbox.uiviews.utils.LocalActivity import com.programmersbox.uiviews.utils.LocalGenericInfo @@ -52,6 +53,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.io.File +@OptIn(ExperimentalMaterial3Api::class) @Composable fun InfoSettings( infoViewModel: MoreInfoViewModel = viewModel(), @@ -117,6 +119,8 @@ fun InfoSettings( modifier = Modifier.clickable { scope.launch(Dispatchers.IO) { infoViewModel.updateChecker(context) } } ) + val downloadInstaller = remember { DownloadAndInstaller(context) } + ShowWhen( visibility = AppUpdate.checkForUpdate(appVersion(), appUpdate?.update_real_version.orEmpty()) ) { @@ -142,7 +146,11 @@ fun InfoSettings( a.let(genericInfo.apkString).toString() ) if (isApkAlreadyThere.exists()) isApkAlreadyThere.delete() - DownloadUpdate(context, context.packageName).downloadUpdate(a) + val url = a.downloadUrl(genericInfo.apkString) + downloadInstaller.downloadAndInstall( + url = url, + destinationPath = url.split("/").lastOrNull() ?: "update_apk" + ) } } } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/settings/NotificationSettings.kt b/UIViews/src/main/java/com/programmersbox/uiviews/settings/NotificationSettings.kt index f115739e3..4f2888f79 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/settings/NotificationSettings.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/settings/NotificationSettings.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.padding import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -30,7 +31,7 @@ import com.programmersbox.favoritesdatabase.ItemDao import com.programmersbox.helpfulutils.notificationManager import com.programmersbox.uiviews.OtakuApp import com.programmersbox.uiviews.R -import com.programmersbox.uiviews.UpdateFlowWorker +import com.programmersbox.uiviews.checkers.UpdateFlowWorker import com.programmersbox.uiviews.utils.LightAndDarkPreviews import com.programmersbox.uiviews.utils.LocalItemDao import com.programmersbox.uiviews.utils.PreferenceSetting @@ -42,6 +43,7 @@ import com.programmersbox.uiviews.utils.updatePref import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @Composable fun NotificationSettings( dao: ItemDao = LocalItemDao.current, diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/settings/PlaySettings.kt b/UIViews/src/main/java/com/programmersbox/uiviews/settings/PlaySettings.kt index b9f639460..a3cc78158 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/settings/PlaySettings.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/settings/PlaySettings.kt @@ -2,6 +2,7 @@ package com.programmersbox.uiviews.settings import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BatteryAlert +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -21,6 +22,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +@OptIn(ExperimentalMaterial3Api::class) @Composable fun PlaySettings(customSettings: (@Composable () -> Unit)?) { SettingsScaffold(stringResource(R.string.playSettings)) { diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/settings/SettingsFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/settings/SettingsFragment.kt index 3772a192e..7430aa3d6 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/settings/SettingsFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/settings/SettingsFragment.kt @@ -16,6 +16,7 @@ import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Android import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Extension import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Language @@ -29,14 +30,15 @@ import androidx.compose.material.icons.filled.Source import androidx.compose.material.icons.filled.Star import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -62,7 +64,6 @@ import coil.compose.SubcomposeAsyncImageContent import coil.request.ImageRequest import coil.transform.CircleCropTransformation import com.programmersbox.favoritesdatabase.ItemDao -import com.programmersbox.models.sourceFlow import com.programmersbox.sharedutils.MainLogo import com.programmersbox.uiviews.BuildConfig import com.programmersbox.uiviews.R @@ -72,10 +73,11 @@ import com.programmersbox.uiviews.utils.InsetSmallTopAppBar import com.programmersbox.uiviews.utils.LifecycleHandle import com.programmersbox.uiviews.utils.LightAndDarkPreviews import com.programmersbox.uiviews.utils.LocalActivity -import com.programmersbox.uiviews.utils.LocalGenericInfo +import com.programmersbox.uiviews.utils.LocalCurrentSource import com.programmersbox.uiviews.utils.LocalHistoryDao import com.programmersbox.uiviews.utils.LocalItemDao import com.programmersbox.uiviews.utils.LocalNavController +import com.programmersbox.uiviews.utils.LocalSourcesRepository import com.programmersbox.uiviews.utils.MockAppIcon import com.programmersbox.uiviews.utils.OtakuScaffold import com.programmersbox.uiviews.utils.PreferenceSetting @@ -172,7 +174,7 @@ private fun SettingsScreen( ) { val context = LocalContext.current val navController = LocalNavController.current - val source by sourceFlow.collectAsState(initial = null) + val source by LocalCurrentSource.current.asFlow().collectAsState(initial = null) if (BuildConfig.DEBUG) { PreferenceSetting( @@ -244,7 +246,7 @@ private fun SettingsScreen( composeSettingsDsl.viewSettings?.invoke() - Divider() + HorizontalDivider() CategorySetting { Text(stringResource(R.string.general_menu_title)) } @@ -257,6 +259,15 @@ private fun SettingsScreen( ) { navController.navigate(Screen.SourceChooserScreen.route) } ) + PreferenceSetting( + settingTitle = { Text(stringResource(R.string.view_extensions)) }, + settingIcon = { Icon(Icons.Default.Extension, null, modifier = Modifier.fillMaxSize()) }, + modifier = Modifier.clickable( + indication = rememberRipple(), + interactionSource = remember { MutableInteractionSource() } + ) { navController.navigate(Screen.ExtensionListScreen.route) } + ) + ShowWhen(visibility = source != null) { PreferenceSetting( settingTitle = { Text(stringResource(R.string.view_source_in_browser)) }, @@ -293,7 +304,7 @@ private fun SettingsScreen( ) ) - Divider() + HorizontalDivider() CategorySetting { Text(stringResource(R.string.additional_settings)) } @@ -333,6 +344,7 @@ private fun SettingsScreen( ) } +@OptIn(ExperimentalMaterial3Api::class) @LightAndDarkPreviews @Composable private fun SettingsPreview() { @@ -422,29 +434,29 @@ private fun AccountSettings( @Composable fun SourceChooserScreen() { - val source by sourceFlow.collectAsState(initial = null) val scope = rememberCoroutineScope() val context = LocalContext.current val navController = LocalNavController.current - val genericInfo = LocalGenericInfo.current + val sourceRepository = LocalSourcesRepository.current + val currentSourceRepository = LocalCurrentSource.current ListBottomScreen( includeInsetPadding = true, title = stringResource(R.string.chooseASource), - list = genericInfo.sourceList(), + list = sourceRepository.list.filterNot { it.apiService.notWorking }, onClick = { service -> navController.popBackStack() scope.launch { service.let { - sourceFlow.emit(it) - context.currentService = it.serviceName + currentSourceRepository.emit(it.apiService) + context.currentService = it.apiService.serviceName } } } ) { ListBottomSheetItemModel( - primaryText = it.serviceName, - icon = if (it == source) Icons.Default.Check else null + primaryText = it.apiService.serviceName, + icon = if (it.apiService.serviceName == context.currentService) Icons.Default.Check else null ) } } @@ -492,27 +504,27 @@ private fun Modifier.click(action: () -> Unit): Modifier = composed { @Composable internal fun SettingsScaffold( title: String, + scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), + topBar: @Composable (TopAppBarScrollBehavior) -> Unit = { + InsetSmallTopAppBar( + title = { Text(title) }, + navigationIcon = { BackButton() }, + scrollBehavior = it, + ) + }, content: @Composable ColumnScope.() -> Unit ) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - OtakuScaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - InsetSmallTopAppBar( - title = { Text(title) }, - navigationIcon = { BackButton() }, - scrollBehavior = scrollBehavior, - ) - }, + topBar = { topBar(scrollBehavior) }, contentWindowInsets = ScaffoldDefaults.contentWindowInsets ) { p -> Column( + content = content, modifier = Modifier .padding(p) .fillMaxSize() .verticalScroll(rememberScrollState()), - content = content ) } } \ No newline at end of file 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 5ded81441..9ed44f266 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/utils/ContextUtils.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/ContextUtils.kt @@ -1,18 +1,20 @@ package com.programmersbox.uiviews.utils import android.Manifest -import android.annotation.SuppressLint import android.app.Dialog -import android.app.DownloadManager -import android.content.* +import android.content.Context +import android.content.ContextWrapper import android.content.pm.PackageManager -import android.graphics.* +import android.graphics.Bitmap +import android.graphics.BlurMaskFilter +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint import android.net.ConnectivityManager import android.net.Network import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Environment import android.text.format.DateFormat import android.view.View import android.widget.FrameLayout @@ -22,14 +24,27 @@ import androidx.annotation.RequiresPermission import androidx.annotation.StringRes import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.filled.BatteryAlert +import androidx.compose.material.icons.filled.BatteryChargingFull +import androidx.compose.material.icons.filled.BatteryFull +import androidx.compose.material.icons.filled.BatteryStd +import androidx.compose.material.icons.filled.BatteryUnknown +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode -import androidx.core.content.FileProvider import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.preferencesDataStore import androidx.fragment.app.FragmentActivity import androidx.lifecycle.SavedStateHandle @@ -41,25 +56,38 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.google.gson.* +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.sizePx +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.gsonutils.sharedPrefObjectDelegate -import com.programmersbox.helpfulutils.* +import com.programmersbox.helpfulutils.Battery +import com.programmersbox.helpfulutils.BatteryHealth +import com.programmersbox.helpfulutils.connectivityManager +import com.programmersbox.helpfulutils.runOnUIThread import com.programmersbox.models.ApiService import com.programmersbox.models.ChapterModel import com.programmersbox.models.InfoModel -import com.programmersbox.sharedutils.AppUpdate import com.programmersbox.uiviews.GenericInfo import com.programmersbox.uiviews.R -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import java.io.File import java.lang.reflect.Type import java.text.SimpleDateFormat -import java.util.* +import java.util.Locale import java.util.concurrent.TimeUnit import kotlin.collections.set import kotlin.properties.Delegates @@ -131,18 +159,21 @@ class ChapterModelSerializer : JsonSerializer { } } -class ChapterModelDeserializer(private val genericInfo: GenericInfo) : JsonDeserializer { +class ChapterModelDeserializer : JsonDeserializer, KoinComponent { + private val sourceRepository: SourceRepository by inject() override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): ChapterModel? { return json.asJsonObject.let { - genericInfo.toSource(it["source"].asString)?.let { it1 -> - ChapterModel( - name = it["name"].asString, - uploaded = it["uploaded"].asString, - source = it1, - sourceUrl = it["sourceUrl"].asString, - url = it["url"].asString - ) - } + sourceRepository.toSourceByApiServiceName(it["source"].asString) + ?.apiService + ?.let { it1 -> + ChapterModel( + name = it["name"].asString, + uploaded = it["uploaded"].asString, + source = it1, + sourceUrl = it["sourceUrl"].asString, + url = it["url"].asString + ) + } } } } @@ -153,9 +184,10 @@ class ApiServiceSerializer : JsonSerializer { } } -class ApiServiceDeserializer(private val genericInfo: GenericInfo) : JsonDeserializer { +class ApiServiceDeserializer(private val genericInfo: GenericInfo) : JsonDeserializer, KoinComponent { + private val sourceRepository: SourceRepository by inject() override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): ApiService? { - return genericInfo.toSource(json.asString) + return sourceRepository.toSourceByApiServiceName(json.asString)?.apiService ?: genericInfo.toSource(json.asString) } } @@ -278,87 +310,6 @@ inline fun factoryCreate(crossinline build: () -> V) = o } } -class DownloadUpdate(private val context: Context, private val packageName: String) : KoinComponent { - - val genericInfo: GenericInfo by inject() - - fun downloadUpdate(update: AppUpdate.AppUpdates): Boolean { - val downloadManager = context.downloadManager - val request = DownloadManager.Request(Uri.parse(update.downloadUrl(genericInfo.apkString))) - .setMimeType("application/vnd.android.package-archive") - .setTitle(context.getString(R.string.app_name)) - .setDestinationInExternalPublicDir( - Environment.DIRECTORY_DOWNLOADS, - update.downloadUrl(genericInfo.apkString).split("/").lastOrNull() ?: "update_apk" - ) - .setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE) - .setAllowedOverRoaming(true) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - - val id = try { - downloadManager.enqueue(request) - } catch (e: Exception) { - -1 - } - if (id == -1L) return false - - val receiver = object : BroadcastReceiver() { - @SuppressLint("Range") - override fun onReceive(context: Context?, intent: Intent?) { - try { - val downloadId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, id) ?: id - - val query = DownloadManager.Query() - query.setFilterById(downloadId) - val c = downloadManager.query(query) - - if (c.moveToFirst()) { - val columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS) - if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) { - c.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI) - val uri = Uri.parse(c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))) - openApk(this@DownloadUpdate.context, uri) - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver( - receiver, - IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), - Context.RECEIVER_NOT_EXPORTED - ) - } else { - context.registerReceiver( - receiver, - IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), - ) - } - return true - } - - private fun openApk(context: Context, uri: Uri) { - uri.path?.let { - val contentUri = FileProvider.getUriForFile( - context, - "$packageName.provider", - File(it) - ) - val installIntent = Intent(Intent.ACTION_VIEW).apply { - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) - data = contentUri - } - context.startActivity(installIntent) - } - } -} - object Cached { private val map = mutableMapOf() diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/DownloadAndInstaller.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/DownloadAndInstaller.kt new file mode 100644 index 000000000..5474b927a --- /dev/null +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/DownloadAndInstaller.kt @@ -0,0 +1,184 @@ +package com.programmersbox.uiviews.utils + +import android.app.DownloadManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.os.Build +import android.os.Environment +import androidx.core.content.FileProvider +import androidx.core.content.getSystemService +import androidx.core.net.toUri +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.transformWhile +import java.io.File +import kotlin.time.Duration.Companion.seconds + +internal class DownloadAndInstaller(private val context: Context) { + private val downloadManager by lazy { context.getSystemService()!! } + private val downloadReceiver = DownloadCompletionReceiver() + private val activeDownloads = hashMapOf() + private val downloadsStateFlows = hashMapOf>() + + fun downloadAndInstall( + url: String, + destinationPath: String + ): Flow { + val oldDownload = activeDownloads[url] + if (oldDownload != null) { + deleteDownload(url) + } + + downloadReceiver.register() + + val downloadUri = url.toUri() + val request = DownloadManager.Request(downloadUri) + .setTitle(destinationPath) + .setMimeType(APK_MIME) + .setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment) + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + + val id = downloadManager.enqueue(request) + activeDownloads[url] = id + + val downloadStateFlow = MutableStateFlow(InstallType.Pending) + downloadsStateFlows[id] = downloadStateFlow + + val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus -> + when (downloadStatus) { + DownloadManager.STATUS_PENDING -> InstallType.Pending + DownloadManager.STATUS_RUNNING -> InstallType.Downloading + else -> null + } + } + + return merge(downloadStateFlow, pollStatusFlow).transformWhile { + emit(it) + !it.isCompleted() + }.onCompletion { + deleteDownload(url) + } + } + + private fun downloadStatusFlow(id: Long): Flow = flow { + val query = DownloadManager.Query().setFilterById(id) + while (true) { + val downloadStatus = downloadManager.query(query).use { cursor -> + if (!cursor.moveToFirst()) return@flow + cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) + } + + emit(downloadStatus) + + if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) { + return@flow + } + + delay(1.seconds) + } + } + .distinctUntilChanged() + + fun installApk(downloadId: Long, uri: Uri) { + val installIntent = Intent(Intent.ACTION_VIEW).apply { + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) + setDataAndType(uri, APK_MIME) + putExtra(EXTRA_DOWNLOAD_ID, downloadId) + } + context.startActivity(installIntent) + } + + fun updateInstallStep(downloadId: Long, step: InstallType) { + downloadsStateFlows[downloadId]?.let { it.value = step } + } + + private fun deleteDownload(pkgName: String) { + val downloadId = activeDownloads.remove(pkgName) + if (downloadId != null) { + downloadManager.remove(downloadId) + downloadsStateFlows.remove(downloadId) + } + if (activeDownloads.isEmpty()) { + downloadReceiver.unregister() + } + } + + private inner class DownloadCompletionReceiver : BroadcastReceiver() { + private var isRegistered = false + + fun register() { + if (isRegistered) return + isRegistered = true + + val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) + runCatching { context.registerReceiver(this, filter) } + .onFailure { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver(this, filter, Context.RECEIVER_EXPORTED) + } + } + } + + fun unregister() { + if (!isRegistered) return + isRegistered = false + + context.unregisterReceiver(this) + } + + + override fun onReceive(context: Context, intent: Intent?) { + val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return + if (id !in activeDownloads.values) return + val uri = downloadManager.getUriForDownloadedFile(id) + if (uri == null) { + updateInstallStep(id, InstallType.Error) + return + } + + val query = DownloadManager.Query().setFilterById(id) + downloadManager.query(query).use { cursor -> + if (cursor.moveToFirst()) { + val localUri = cursor.getString( + cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI), + ).removePrefix(FILE_SCHEME) + + installApk(id, File(localUri).getUriCompat(context)) + } + } + } + } + + companion object { + const val APK_MIME = "application/vnd.android.package-archive" + const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID" + const val FILE_SCHEME = "file://" + } +} + +enum class InstallType { + Idle, Pending, Downloading, Installing, Installed, Error; + + fun isCompleted(): Boolean { + return this == Installed || this == Error || this == Idle + } +} + +fun File.getUriCompat(context: Context): Uri { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + FileProvider.getUriForFile(context, "${context.packageName}.provider", this) + } else { + this.toUri() + } +} \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/MockData.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/MockData.kt index 312b2623b..309a6b786 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/utils/MockData.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/MockData.kt @@ -39,6 +39,7 @@ import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.DbModel import com.programmersbox.favoritesdatabase.HistoryDatabase import com.programmersbox.favoritesdatabase.ItemDatabase @@ -52,6 +53,8 @@ import com.programmersbox.sharedutils.MainLogo import com.programmersbox.uiviews.BaseMainActivity import com.programmersbox.uiviews.GenericInfo import com.programmersbox.uiviews.R +import java.text.SimpleDateFormat +import java.util.Locale import androidx.compose.material.MaterialTheme as M2MaterialTheme val MockInfo = object : GenericInfo { @@ -210,6 +213,8 @@ fun PreviewTheme( LocalItemDao provides remember { ItemDatabase.getInstance(context).itemDao() }, LocalHistoryDao provides remember { HistoryDatabase.getInstance(context).historyDao() }, LocalCustomListDao provides remember { ListDatabase.getInstance(context).listDao() }, + LocalSourcesRepository provides SourceRepository(), + LocalSystemDateTimeFormat provides remember { SimpleDateFormat("", Locale.getDefault()) } ) { Surface { content() } } } } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/Screen.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/Screen.kt index 39634319d..03e572223 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/utils/Screen.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/Screen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavController import androidx.navigation.NavHostController import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.DbModel import com.programmersbox.favoritesdatabase.HistoryDao import com.programmersbox.favoritesdatabase.HistoryDatabase @@ -26,6 +27,7 @@ import com.programmersbox.favoritesdatabase.ListDatabase import com.programmersbox.gsonutils.toJson import com.programmersbox.models.ApiService import com.programmersbox.models.ItemModel +import com.programmersbox.uiviews.CurrentSourceRepository import com.programmersbox.uiviews.GenericInfo import org.koin.compose.koinInject import java.util.UUID @@ -74,6 +76,7 @@ sealed class Screen(val route: String) { } object SourceChooserScreen : Screen("source_chooser") + object ExtensionListScreen : Screen("extension_list") } fun NavController.navigateToDetails(model: ItemModel) = navigate( @@ -123,7 +126,9 @@ fun OtakuMaterialTheme( LocalItemDao provides remember { ItemDatabase.getInstance(context).itemDao() }, LocalHistoryDao provides remember { HistoryDatabase.getInstance(context).historyDao() }, LocalCustomListDao provides remember { ListDatabase.getInstance(context).listDao() }, - LocalSystemDateTimeFormat provides remember { context.getSystemDateTimeFormat() } + LocalSystemDateTimeFormat provides remember { context.getSystemDateTimeFormat() }, + LocalSourcesRepository provides koinInject(), + LocalCurrentSource provides koinInject() ) { content() } } } @@ -132,3 +137,5 @@ fun OtakuMaterialTheme( val LocalItemDao = staticCompositionLocalOf { error("nothing here") } val LocalHistoryDao = staticCompositionLocalOf { error("nothing here") } val LocalCustomListDao = staticCompositionLocalOf { error("nothing here") } +val LocalSourcesRepository = staticCompositionLocalOf { error("nothing here") } +val LocalCurrentSource = staticCompositionLocalOf { CurrentSourceRepository() } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/components/AutoComplete.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/components/AutoComplete.kt index 4504a8a5d..339447c51 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/utils/components/AutoComplete.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/components/AutoComplete.kt @@ -3,13 +3,23 @@ package com.programmersbox.uiviews.utils.components import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed @@ -112,7 +122,7 @@ fun AutoCompleteBox( trailingIcon = trailingIcon?.let { { it.invoke(item) } }, leadingIcon = leadingIcon?.let { { it.invoke(item) } } ) - if (i < items.size - 1) androidx.compose.material3.Divider() + if (i < items.size - 1) HorizontalDivider() } } } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/components/ListBottomSheet.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/components/ListBottomSheet.kt index 33f9640e1..07daa8885 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/utils/components/ListBottomSheet.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/components/ListBottomSheet.kt @@ -11,8 +11,8 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -56,7 +56,7 @@ fun ListBottomScreen( navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.Close, null) } }, actions = { if (list.isNotEmpty()) Text("(${list.size})") } ) - Divider() + HorizontalDivider() } lazyListContent() itemsIndexed(list) { index, it -> @@ -69,7 +69,7 @@ fun ListBottomScreen( overlineContent = c.overlineText?.let { i -> { Text(i) } }, trailingContent = c.trailingText?.let { i -> { Text(i) } } ) - if (index < list.size - 1) Divider() + if (index < list.size - 1) HorizontalDivider() } } } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/components/NoSourcesInstalled.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/components/NoSourcesInstalled.kt new file mode 100644 index 000000000..68b4d255e --- /dev/null +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/components/NoSourcesInstalled.kt @@ -0,0 +1,55 @@ +package com.programmersbox.uiviews.utils.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExtensionOff +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.programmersbox.uiviews.utils.LightAndDarkPreviews +import com.programmersbox.uiviews.utils.LocalNavController +import com.programmersbox.uiviews.utils.PreviewTheme +import com.programmersbox.uiviews.utils.Screen + +@Composable +fun NoSourcesInstalled(modifier: Modifier = Modifier) { + val navController = LocalNavController.current + Box( + contentAlignment = Alignment.Center, + modifier = modifier + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + Icons.Default.ExtensionOff, + contentDescription = null, + modifier = Modifier.size(120.dp) + ) + Button( + onClick = { + navController.navigate(Screen.ExtensionListScreen.route) { + popUpTo(Screen.SettingsScreen.route) + launchSingleTop = true + } + } + ) { + Text("Cannot find any extensions. Please install some!") + } + } + } +} + +@LightAndDarkPreviews +@Composable +private fun NoSourcesInstalledPreview() { + PreviewTheme { + NoSourcesInstalled() + } +} \ 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 8f881a545..24a6697d5 100644 --- a/UIViews/src/main/res/values/strings.xml +++ b/UIViews/src/main/res/values/strings.xml @@ -202,4 +202,7 @@ Additional Settings Notification Settings Notifications + Extensions + View Extensions + Installed \ No newline at end of file diff --git a/anime_sources/defaultanimesources/.gitignore b/anime_sources/defaultanimesources/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/anime_sources/defaultanimesources/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/anime_sources/defaultanimesources/build.gradle.kts b/anime_sources/defaultanimesources/build.gradle.kts new file mode 100644 index 000000000..bab98a9e5 --- /dev/null +++ b/anime_sources/defaultanimesources/build.gradle.kts @@ -0,0 +1,43 @@ +import plugins.SourceType + +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + id("otaku-source-application") +} + +android { + namespace = "com.programmersbox.defaultanimesources" + + defaultConfig { + applicationId = "com.programmersbox.defaultanimesources" + } +} + +otakuSourceInformation { + name = "Default Anime Sources" + classInfo = ".AnimeSources" + sourceType = SourceType.Anime +} + +dependencies { + testImplementation(TestDeps.junit) + androidTestImplementation(TestDeps.androidJunit) + androidTestImplementation(TestDeps.androidEspresso) + implementation(libs.bundles.okHttpLibs) + + implementation(libs.coroutinesCore) + + implementation(Deps.gsonutils) + implementation(Deps.helpfulutils) + debugImplementation(Deps.loggingutils) + implementation(libs.gson) + + implementation(libs.jsoup) + + implementation(projects.models) + implementation(projects.animeSources) + api(projects.sourceUtilities) + implementation(libs.bundles.ktorLibs) + + implementation(libs.bundles.koinLibs) +} \ No newline at end of file diff --git a/anime_sources/defaultanimesources/proguard-rules.pro b/anime_sources/defaultanimesources/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/anime_sources/defaultanimesources/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/anime_sources/defaultanimesources/src/main/AndroidManifest.xml b/anime_sources/defaultanimesources/src/main/AndroidManifest.xml new file mode 100644 index 000000000..418151c5b --- /dev/null +++ b/anime_sources/defaultanimesources/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/anime_sources/defaultanimesources/src/main/java/com/programmersbox/defaultanimesources/AnimeSources.kt b/anime_sources/defaultanimesources/src/main/java/com/programmersbox/defaultanimesources/AnimeSources.kt new file mode 100644 index 000000000..1045a1ae8 --- /dev/null +++ b/anime_sources/defaultanimesources/src/main/java/com/programmersbox/defaultanimesources/AnimeSources.kt @@ -0,0 +1,11 @@ +package com.programmersbox.defaultanimesources + +import com.programmersbox.anime_sources.Sources +import com.programmersbox.models.ApiService +import com.programmersbox.models.ApiServicesCatalog + +object AnimeSources : ApiServicesCatalog { + override fun createSources(): List = Sources.entries.filterNot { it.notWorking }.toList() + + override val name: String get() = "Default Anime Sources" +} \ No newline at end of file diff --git a/anime_sources/defaultanimesources/src/main/res/drawable/ic_launcher_background.xml b/anime_sources/defaultanimesources/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/anime_sources/defaultanimesources/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/anime_sources/defaultanimesources/src/main/res/drawable/ic_launcher_foreground.xml b/anime_sources/defaultanimesources/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/anime_sources/defaultanimesources/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/anime_sources/defaultanimesources/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/anime_sources/defaultanimesources/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/anime_sources/defaultanimesources/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/anime_sources/defaultanimesources/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-hdpi/ic_launcher.webp b/anime_sources/defaultanimesources/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/anime_sources/defaultanimesources/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/anime_sources/defaultanimesources/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/anime_sources/defaultanimesources/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-mdpi/ic_launcher.webp b/anime_sources/defaultanimesources/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/anime_sources/defaultanimesources/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/anime_sources/defaultanimesources/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/anime_sources/defaultanimesources/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-xhdpi/ic_launcher.webp b/anime_sources/defaultanimesources/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/anime_sources/defaultanimesources/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/anime_sources/defaultanimesources/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/anime_sources/defaultanimesources/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/anime_sources/defaultanimesources/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/anime_sources/defaultanimesources/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/anime_sources/defaultanimesources/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/anime_sources/defaultanimesources/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/anime_sources/defaultanimesources/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/anime_sources/defaultanimesources/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/anime_sources/defaultanimesources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/anime_sources/defaultanimesources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/anime_sources/defaultanimesources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/anime_sources/defaultanimesources/src/main/res/values/colors.xml b/anime_sources/defaultanimesources/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/anime_sources/defaultanimesources/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/anime_sources/defaultanimesources/src/main/res/values/strings.xml b/anime_sources/defaultanimesources/src/main/res/values/strings.xml new file mode 100644 index 000000000..e2dba5032 --- /dev/null +++ b/anime_sources/defaultanimesources/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + DefaultAnimeSources + \ No newline at end of file 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 6bf8bb793..549ff004f 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 @@ -1,6 +1,23 @@ package com.programmersbox.anime_sources -import com.programmersbox.anime_sources.anime.* +import com.programmersbox.anime_sources.anime.AllAnime +import com.programmersbox.anime_sources.anime.AnimeFlick +import com.programmersbox.anime_sources.anime.AnimeKisaDubbed +import com.programmersbox.anime_sources.anime.AnimeKisaMovies +import com.programmersbox.anime_sources.anime.AnimeKisaSubbed +import com.programmersbox.anime_sources.anime.CrunchyRoll +import com.programmersbox.anime_sources.anime.Dopebox +import com.programmersbox.anime_sources.anime.GogoAnimeVC +import com.programmersbox.anime_sources.anime.Hdm +import com.programmersbox.anime_sources.anime.PutlockerAnime +import com.programmersbox.anime_sources.anime.PutlockerCartoons +import com.programmersbox.anime_sources.anime.PutlockerMovies +import com.programmersbox.anime_sources.anime.PutlockerTV +import com.programmersbox.anime_sources.anime.SflixS +import com.programmersbox.anime_sources.anime.VidEmbed +import com.programmersbox.anime_sources.anime.Vidstreaming +import com.programmersbox.anime_sources.anime.WcoStream +import com.programmersbox.anime_sources.anime.WcoStreamCC import com.programmersbox.models.ApiService import com.programmersbox.models.ItemModel import org.jsoup.Jsoup @@ -11,7 +28,6 @@ enum class Sources(private val api: ApiService, override val notWorking: Boolean ALLANIME(AllAnime), GOGOANIME_VC(GogoAnimeVC), - KAWAIIFU(Kawaiifu), HDM(Hdm, true), //ANIMESIMPLE_SUBBED(AnimeSimpleSubbed), ANIMESIMPLE_DUBBED(AnimeSimpleDubbed), @@ -48,7 +64,6 @@ enum class Sources(private val api: ApiService, override val notWorking: Boolean SFLIX, WCOSTREAM, GOGOANIME_VC, - KAWAIIFU, ANIMEFLICK, ANIMEKISA_SUBBED, //HDM, diff --git a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/AllAnime.kt b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/AllAnime.kt index 719d5265d..737435752 100644 --- a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/AllAnime.kt +++ b/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/AllAnime.kt @@ -8,10 +8,16 @@ import com.programmersbox.anime_sources.utilities.extractors import com.programmersbox.anime_sources.utilities.getQualityFromName import com.programmersbox.gsonutils.fromJson import com.programmersbox.gsonutils.getApi -import com.programmersbox.models.* -import io.ktor.client.call.* -import io.ktor.client.request.* +import com.programmersbox.models.ChapterModel +import com.programmersbox.models.InfoModel +import com.programmersbox.models.ItemModel +import com.programmersbox.models.Storage +import com.programmersbox.models.createHttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json import org.jsoup.Jsoup import org.mozilla.javascript.Context import org.mozilla.javascript.Scriptable @@ -23,6 +29,14 @@ object AllAnime : ShowApi( allPath = "", recentPath = "" ) { + + private val json = Json { + isLenient = true + prettyPrint = true + ignoreUnknownKeys = true + coerceInputValues = true + } + override val canDownload: Boolean get() = false private val client by lazy { createHttpClient() } @@ -30,7 +44,9 @@ object AllAnime : ShowApi( override suspend fun recent(page: Int): List { val url = """$baseUrl/graphql?variables={"search":{"allowAdult":true,"allowUnknown":false},"limit":30,"page":1,"translationType":"dub","countryOrigin":"ALL"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"d2670e3e27ee109630991152c8484fce5ff5e280c523378001f9a23dc1839068"}}""" - return client.get(url).body() + return client.get(url).bodyAsText() + .also { println(it) } + .let { json.decodeFromString(it) } .data.shows.edges .map { ItemModel( 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 91addd9b3..6593dc133 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 @@ -161,7 +161,8 @@ class CrunchyrollGeoBypasser { object CrunchyRoll : ShowApi( baseUrl = "http://www.crunchyroll.com", - recentPath = "videos/anime/updated", allPath = "" + recentPath = "videos/anime/updated", + allPath = "" ) { override val serviceName: String get() = "CRUNCHYROLL" 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 deleted file mode 100644 index c17aaaa28..000000000 --- a/anime_sources/src/main/java/com/programmersbox/anime_sources/anime/Kawaiifu.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.programmersbox.anime_sources.anime - -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.getQualityFromName -import com.programmersbox.models.ChapterModel -import com.programmersbox.models.InfoModel -import com.programmersbox.models.ItemModel -import com.programmersbox.models.Storage -import org.jsoup.Jsoup -import org.jsoup.nodes.Element - -object Kawaiifu : ShowApi( - baseUrl = "https://kawaiifu.com", - recentPath = "page/", allPath = "" -) { - override val serviceName: String get() = "KAWAIIFU" - override val canScroll: Boolean get() = true - override val canScrollAll: Boolean get() = false - override val canDownload: Boolean get() = false - override fun recentPage(page: Int): String = page.toString() - - 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 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 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 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 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 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/test/java/com/programmersbox/anime_sources/ExampleUnitTest.kt b/anime_sources/src/test/java/com/programmersbox/anime_sources/ExampleUnitTest.kt index dda01e342..65e0ff69f 100644 --- a/anime_sources/src/test/java/com/programmersbox/anime_sources/ExampleUnitTest.kt +++ b/anime_sources/src/test/java/com/programmersbox/anime_sources/ExampleUnitTest.kt @@ -1,11 +1,15 @@ package com.programmersbox.anime_sources import androidx.annotation.WorkerThread -import androidx.compose.ui.util.fastMap -import com.programmersbox.anime_sources.anime.* +import com.programmersbox.anime_sources.anime.AllAnime +import com.programmersbox.anime_sources.anime.AnimeKisaSubbed +import com.programmersbox.anime_sources.anime.AnimeSimpleSubbed +import com.programmersbox.anime_sources.anime.AnimeToonDubbed +import com.programmersbox.anime_sources.anime.GogoAnimeVC +import com.programmersbox.anime_sources.anime.PutlockerTV +import com.programmersbox.anime_sources.anime.Vidstreaming import com.programmersbox.gsonutils.getApi import com.programmersbox.gsonutils.header -import com.programmersbox.models.ItemModel import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import okhttp3.MediaType @@ -41,27 +45,6 @@ class ExampleUnitTest { println(f2) } - @Test - fun kawaiifuTest() { - val url = "https://kawaiifu.com" - val f = Jsoup.connect(url) - .sslSocketFactory(socketFactory()) - .get() - - val recent = f.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 = Kawaiifu - ) - } - - println(recent.joinToString("\n")) - - } - @Test fun allmoviesforyouTest() { val url = "https://allmoviesforyou.co" diff --git a/animeworld/build.gradle.kts b/animeworld/build.gradle.kts index f80d622ff..7e5fdc7d3 100644 --- a/animeworld/build.gradle.kts +++ b/animeworld/build.gradle.kts @@ -49,7 +49,6 @@ dependencies { implementation(projects.uiViews) implementation(projects.models) implementation(projects.favoritesdatabase) - implementation(projects.animeSources) implementation(projects.sharedutils) implementation(libs.bundles.roomLibs) diff --git a/animeworld/src/main/AndroidManifest.xml b/animeworld/src/main/AndroidManifest.xml index efe055c59..f56894b03 100644 --- a/animeworld/src/main/AndroidManifest.xml +++ b/animeworld/src/main/AndroidManifest.xml @@ -1,16 +1,12 @@ - - - - Qualities.P360 + "480" -> Qualities.P480 + "720" -> Qualities.P720 + "1080" -> Qualities.P1080 + "1440" -> Qualities.P1440 + "2160" -> Qualities.P2160 + "4k" -> Qualities.P2160 + "4K" -> Qualities.P2160 + else -> Qualities.Unknown + } +} \ No newline at end of file diff --git a/animeworld/src/main/java/com/programmersbox/animeworld/GenericAnime.kt b/animeworld/src/main/java/com/programmersbox/animeworld/GenericAnime.kt index 1c2d2ef37..af438e6be 100644 --- a/animeworld/src/main/java/com/programmersbox/animeworld/GenericAnime.kt +++ b/animeworld/src/main/java/com/programmersbox/animeworld/GenericAnime.kt @@ -45,6 +45,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -76,12 +77,10 @@ import androidx.navigation.compose.composable import androidx.navigation.navDeepLink import com.google.accompanist.placeholder.PlaceholderHighlight import com.google.accompanist.placeholder.material.placeholder -import com.google.accompanist.placeholder.material.shimmer +import com.google.accompanist.placeholder.shimmer import com.google.android.gms.cast.framework.CastContext import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.obsez.android.lib.filechooser.ChooserDialog -import com.programmersbox.anime_sources.ShowApi -import com.programmersbox.anime_sources.Sources import com.programmersbox.animeworld.cast.ExpandedControlsActivity import com.programmersbox.animeworld.videochoice.VideoChoiceScreen import com.programmersbox.animeworld.videochoice.VideoSourceViewModel @@ -120,28 +119,38 @@ import kotlinx.coroutines.launch import org.koin.dsl.module val appModule = module { - single { GenericAnime(get()) } + single { GenericAnime(get(), get()) } single { MainLogo(R.mipmap.ic_launcher) } single { NotificationLogo(R.mipmap.ic_launcher_foreground) } + single { StorageHolder() } } -class GenericAnime(val context: Context) : GenericInfo { +class StorageHolder { + var storageModel: Storage? = null +} + +class GenericAnime( + val context: Context, + val storageHolder: StorageHolder, +) : GenericInfo { override val apkString: AppUpdate.AppUpdates.() -> String? get() = { if (BuildConfig.FLAVOR == "noFirebase") anime_no_firebase_file else anime_file } override val deepLinkUri: String get() = "animeworld://" + override val sourceType: String get() = "anime" + override fun chapterOnClick( model: ChapterModel, allChapters: List, infoModel: InfoModel, context: Context, activity: FragmentActivity, - navController: NavController + navController: NavController, ) { - if ((model.source as? ShowApi)?.canPlay == false) { + /*if ((model.source as? ShowApi)?.canPlay == false) { Toast.makeText(context, context.getString(R.string.source_no_stream, model.source.serviceName), Toast.LENGTH_SHORT).show() return - } + }*/ getEpisodes( R.string.source_no_stream, model, @@ -160,6 +169,7 @@ class GenericAnime(val context: Context) : GenericInfo { it.headers ) } else { + storageHolder.storageModel = it context.navigateToVideoPlayer( navController, it.link, @@ -179,14 +189,14 @@ class GenericAnime(val context: Context) : GenericInfo { activity: FragmentActivity, navController: NavController ) { - if ((model.source as? ShowApi)?.canDownload == false) { - Toast.makeText( - context, - context.getString(R.string.source_no_download, model.source.serviceName), - Toast.LENGTH_SHORT - ).show() - return - } + /* if ((model.source as? ShowApi)?.canDownload == false) { + Toast.makeText( + context, + context.getString(R.string.source_no_download, model.source.serviceName), + Toast.LENGTH_SHORT + ).show() + return + }*/ activity.requestPermissions( *if (Build.VERSION.SDK_INT >= 33) arrayOf(Manifest.permission.READ_MEDIA_VIDEO) @@ -287,18 +297,19 @@ class GenericAnime(val context: Context) : GenericInfo { } } - context.downloadManager.enqueue(d) + runCatching { context.downloadManager.enqueue(d) } + .onSuccess { Toast.makeText(context, "Downloading...", Toast.LENGTH_SHORT).show() } + .onFailure { + it.printStackTrace() + Toast.makeText(context, "Something went wrong...", Toast.LENGTH_SHORT).show() + } } - override fun sourceList(): List = Sources.values().filterNot(Sources::notWorking).toList() + override fun sourceList(): List = emptyList() - override fun searchList(): List = Sources.searchSources + override fun searchList(): List = emptyList() - override fun toSource(s: String): ApiService? = try { - Sources.valueOf(s) - } catch (e: IllegalArgumentException) { - null - } + override fun toSource(s: String): ApiService? = null @Composable override fun DetailActions(infoModel: InfoModel, tint: Color) { @@ -323,6 +334,10 @@ class GenericAnime(val context: Context) : GenericInfo { @Composable override fun ComposeShimmerItem() { + val placeholderColor = contentColorFor(backgroundColor = MaterialTheme.colorScheme.surface) + .copy(0.1f) + .compositeOver(MaterialTheme.colorScheme.surface) + LazyColumn { items(10) { Card( @@ -335,11 +350,8 @@ class GenericAnime(val context: Context) : GenericInfo { .fillMaxWidth() .placeholder( true, - highlight = PlaceholderHighlight.shimmer(), - color = androidx.compose.material3 - .contentColorFor(backgroundColor = MaterialTheme.colorScheme.surface) - .copy(0.1f) - .compositeOver(MaterialTheme.colorScheme.surface) + color = placeholderColor, + highlight = PlaceholderHighlight.shimmer(MaterialTheme.colorScheme.surface.copy(alpha = .75f)) ) ) { Icon( diff --git a/animeworld/src/main/java/com/programmersbox/animeworld/MainActivity.kt b/animeworld/src/main/java/com/programmersbox/animeworld/MainActivity.kt index 5aebacf9f..b9aecb79c 100644 --- a/animeworld/src/main/java/com/programmersbox/animeworld/MainActivity.kt +++ b/animeworld/src/main/java/com/programmersbox/animeworld/MainActivity.kt @@ -2,15 +2,10 @@ package com.programmersbox.animeworld import androidx.compose.runtime.Composable import androidx.compose.ui.viewinterop.AndroidViewBinding -import androidx.lifecycle.lifecycleScope -import com.programmersbox.anime_sources.Sources import com.programmersbox.animeworld.cast.CastHelper import com.programmersbox.animeworld.databinding.MiniControllerBinding import com.programmersbox.animeworld.videos.ViewVideoViewModel -import com.programmersbox.models.sourceFlow import com.programmersbox.uiviews.BaseMainActivity -import com.programmersbox.uiviews.utils.currentService -import kotlinx.coroutines.launch class MainActivity : BaseMainActivity() { @@ -25,15 +20,6 @@ class MainActivity : BaseMainActivity() { } catch (e: Exception) { } - - lifecycleScope.launch { - if (currentService == null) { - val s = Sources.values().filterNot(Sources::notWorking).random() - sourceFlow.emit(s) - currentService = s.serviceName - } - } - } @Composable diff --git a/animeworld/src/main/java/com/programmersbox/animeworld/cast/CastOptions.kt b/animeworld/src/main/java/com/programmersbox/animeworld/cast/CastOptions.kt index 6346ca81d..75b1f6546 100644 --- a/animeworld/src/main/java/com/programmersbox/animeworld/cast/CastOptions.kt +++ b/animeworld/src/main/java/com/programmersbox/animeworld/cast/CastOptions.kt @@ -7,7 +7,6 @@ import android.content.Context import android.content.Intent import android.content.res.Configuration import android.net.Uri -import android.net.wifi.WifiManager import android.os.Environment import android.util.Log import android.view.Menu @@ -25,6 +24,7 @@ import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.images.WebImage import com.programmersbox.animeworld.R +import com.programmersbox.helpfulutils.connectivityManager import io.github.dkbai.tinyhttpd.nanohttpd.webserver.SimpleWebServer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -34,9 +34,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import java.lang.ref.WeakReference -import java.net.InetAddress -import java.nio.ByteBuffer -import java.nio.ByteOrder typealias SessionCallback = (Int) -> Unit typealias SimpleCallback = () -> Unit @@ -381,18 +378,9 @@ class CastHelper { class Utils { companion object { fun findIPAddress(context: Context): String? { - val wifiManager = - context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wifiManager = context.connectivityManager try { - return if (wifiManager.connectionInfo != null) { - val wifiInfo = wifiManager.connectionInfo - InetAddress.getByAddress( - ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN) - .putInt(wifiInfo.ipAddress) - .array() - ).hostAddress - } else - null + return wifiManager.getLinkProperties(wifiManager.activeNetwork)?.linkAddresses?.get(1)?.address?.hostAddress } catch (e: Exception) { Log.e(Utils::class.java.name, "Error finding IpAddress: ${e.message}", e) } diff --git a/animeworld/src/main/java/com/programmersbox/animeworld/videochoice/VideoChoiceScreen.kt b/animeworld/src/main/java/com/programmersbox/animeworld/videochoice/VideoChoiceScreen.kt index dbf16cf26..8c6aacadc 100644 --- a/animeworld/src/main/java/com/programmersbox/animeworld/videochoice/VideoChoiceScreen.kt +++ b/animeworld/src/main/java/com/programmersbox/animeworld/videochoice/VideoChoiceScreen.kt @@ -14,11 +14,11 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.compose.viewModel -import com.programmersbox.anime_sources.utilities.Qualities -import com.programmersbox.anime_sources.utilities.getQualityFromName import com.programmersbox.animeworld.GenericAnime import com.programmersbox.animeworld.MainActivity +import com.programmersbox.animeworld.Qualities import com.programmersbox.animeworld.R +import com.programmersbox.animeworld.getQualityFromName import com.programmersbox.animeworld.navigateToVideoPlayer import com.programmersbox.models.Storage import com.programmersbox.uiviews.GenericInfo diff --git a/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoPlayerActivity.kt b/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoPlayerActivity.kt index 2536dc001..705cf4df7 100644 --- a/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoPlayerActivity.kt +++ b/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoPlayerActivity.kt @@ -158,7 +158,7 @@ class VideoPlayerActivity : AppCompatActivity() { private val chapterModel: ChapterModel? by lazy { intent.getStringExtra("chapterModel") - ?.fromJson(ChapterModel::class.java to ChapterModelDeserializer(genericInfo)) + ?.fromJson(ChapterModel::class.java to ChapterModelDeserializer()) } private lateinit var videoBinding: ActivityVideoPlayerBinding diff --git a/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoPlayerCompose.kt b/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoPlayerCompose.kt index bde4e0f9f..fce0dc5ca 100644 --- a/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoPlayerCompose.kt +++ b/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoPlayerCompose.kt @@ -55,6 +55,7 @@ import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView +import com.programmersbox.animeworld.StorageHolder import com.programmersbox.animeworld.ignoreSsl import com.programmersbox.helpfulutils.audioManager import com.programmersbox.uiviews.BaseMainActivity @@ -63,6 +64,7 @@ import com.programmersbox.uiviews.utils.* import com.programmersbox.uiviews.utils.components.AirBar import kotlinx.coroutines.* import kotlinx.coroutines.flow.first +import org.koin.compose.koinInject import java.security.SecureRandom import java.util.* import java.util.concurrent.TimeUnit @@ -77,7 +79,8 @@ import kotlin.math.abs fun VideoPlayerUi( context: Context = LocalContext.current, genericInfo: GenericInfo = LocalGenericInfo.current, - viewModel: VideoViewModel = viewModel { VideoViewModel(createSavedStateHandle(), genericInfo, context) } + storageHolder: StorageHolder = koinInject(), + viewModel: VideoViewModel = viewModel { VideoViewModel(createSavedStateHandle(), genericInfo, context, storageHolder) }, ) { val activity = LocalActivity.current diff --git a/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoViewModel.kt b/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoViewModel.kt index 9a6d118e0..a78da26b0 100644 --- a/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoViewModel.kt +++ b/animeworld/src/main/java/com/programmersbox/animeworld/videoplayer/VideoViewModel.kt @@ -23,6 +23,7 @@ import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.upstream.BandwidthMeter import androidx.navigation.NavController +import com.programmersbox.animeworld.StorageHolder import com.programmersbox.gsonutils.fromJson import com.programmersbox.helpfulutils.battery import com.programmersbox.models.ChapterModel @@ -42,7 +43,8 @@ import javax.net.ssl.X509TrustManager class VideoViewModel( handle: SavedStateHandle, genericInfo: GenericInfo, - context: Context + context: Context, + private val storageHolder: StorageHolder, ) : ViewModel() { companion object { @@ -63,8 +65,8 @@ class VideoViewModel( } val chapterModel: ChapterModel? = handle.get("chapterModel") - ?.fromJson(ChapterModel::class.java to ChapterModelDeserializer(genericInfo)) - val showPath = handle.get("showPath").orEmpty() + ?.fromJson(ChapterModel::class.java to ChapterModelDeserializer()) + val showPath = storageHolder.storageModel?.link ?: handle.get("showPath").orEmpty() val showName = handle.get("showName") val downloadOrStream = handle.get("downloadOrStream")?.toBoolean() ?: true val headers = handle.get("referer") ?: chapterModel?.url ?: "" @@ -135,6 +137,11 @@ class VideoViewModel( mFormatter.format("%02d:%02d", minute, second).toString() } } + + override fun onCleared() { + super.onCleared() + storageHolder.storageModel = null + } } @Composable diff --git a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/MainActivity.kt b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/MainActivity.kt index 9052ff141..13669d05c 100644 --- a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/MainActivity.kt +++ b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/MainActivity.kt @@ -6,7 +6,6 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope import com.programmersbox.anime_sources.Sources import com.programmersbox.animeworldtv.compose.HomeScreen -import com.programmersbox.models.sourceFlow import com.programmersbox.sharedutils.AppUpdate import com.programmersbox.sharedutils.updateAppCheck import kotlinx.coroutines.Dispatchers @@ -28,14 +27,14 @@ class MainActivity : FragmentActivity() { lifecycleScope.launch { if (currentService == null) { val s = Sources.values().filterNot(Sources::notWorking).random() - sourceFlow.emit(s) + //sourceFlow.emit(s) currentService = s.serviceName } else if (currentService != null) { - try { + /*try { Sources.valueOf(currentService!!) } catch (e: IllegalArgumentException) { null - }?.let { sourceFlow.emit(it) } + }?.let { sourceFlow.emit(it) }*/ } } diff --git a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/MainFragment.kt b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/MainFragment.kt index 7cdf41c01..e5b018b32 100644 --- a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/MainFragment.kt +++ b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/MainFragment.kt @@ -16,19 +16,29 @@ import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat import androidx.leanback.app.BackgroundManager import androidx.leanback.app.BrowseSupportFragment -import androidx.leanback.widget.* +import androidx.leanback.widget.ArrayObjectAdapter +import androidx.leanback.widget.HeaderItem +import androidx.leanback.widget.ListRow +import androidx.leanback.widget.ListRowPresenter +import androidx.leanback.widget.OnItemViewClickedListener +import androidx.leanback.widget.OnItemViewSelectedListener +import androidx.leanback.widget.Presenter +import androidx.leanback.widget.Row +import androidx.leanback.widget.RowPresenter import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import com.programmersbox.models.ItemModel -import com.programmersbox.models.sourceFlow import com.programmersbox.sharedutils.AppUpdate import com.programmersbox.sharedutils.updateAppCheck -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import java.util.* +import java.util.Timer +import java.util.TimerTask /** * Loads a grid of cards with movies to browse. @@ -126,7 +136,7 @@ class MainFragment : BrowseSupportFragment() { val gridRowAdapter = ArrayObjectAdapter(mGridPresenter) lifecycleScope.launch { - sourceFlow + /*sourceFlow .filterNotNull() .flowOn(Dispatchers.IO) .flatMapMerge { it.getListFlow() } @@ -137,12 +147,12 @@ class MainFragment : BrowseSupportFragment() { val gridHeader = HeaderItem(NUM_ROWS.toLong(), "PREFERENCES") - /*val mGridPresenter = GridItemPresenter() + *//*val mGridPresenter = GridItemPresenter() val gridRowAdapter = ArrayObjectAdapter(mGridPresenter) gridRowAdapter.add(resources.getString(R.string.favorites)) //gridRowAdapter.add(resources.getString(R.string.grid_view)) //gridRowAdapter.add(getString(R.string.error_fragment)) - gridRowAdapter.add(resources.getString(R.string.personal_settings))*/ + gridRowAdapter.add(resources.getString(R.string.personal_settings))*//* rowsAdapter.add(ListRow(gridHeader, gridRowAdapter)) it.entries.forEach { item -> @@ -155,7 +165,7 @@ class MainFragment : BrowseSupportFragment() { rowsAdapter.add(ListRow(header, listRowAdapter)) } - /*for (i in 0 until NUM_ROWS) { + *//*for (i in 0 until NUM_ROWS) { if (i != 0) { Collections.shuffle(it) } @@ -165,9 +175,9 @@ class MainFragment : BrowseSupportFragment() { } val header = HeaderItem(i.toLong(), MovieList.MOVIE_CATEGORY[i]) rowsAdapter.add(ListRow(header, listRowAdapter)) - }*/ + }*//* } - .collect() + .collect()*/ } val gridHeader = HeaderItem(NUM_ROWS.toLong(), "PREFERENCES") diff --git a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/SettingsActivity.kt b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/SettingsActivity.kt index 523db0c5a..f0c04a385 100644 --- a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/SettingsActivity.kt +++ b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/SettingsActivity.kt @@ -1,6 +1,5 @@ package com.programmersbox.animeworldtv -import android.graphics.drawable.Drawable import android.os.Bundle import android.widget.Toast import androidx.fragment.app.FragmentActivity @@ -11,16 +10,18 @@ import androidx.preference.DialogPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceScreen -import com.bumptech.glide.Glide import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.programmersbox.anime_sources.Sources -import com.programmersbox.models.sourceFlow import com.programmersbox.sharedutils.AppUpdate import com.programmersbox.sharedutils.CustomFirebaseUser import com.programmersbox.sharedutils.FirebaseAuthentication import com.programmersbox.sharedutils.updateAppCheck import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean @@ -85,7 +86,7 @@ class SettingsFragment : LeanbackSettingsFragmentCompat(), DialogPreference.Targ findPreference("user_account")?.let { p -> fun accountChanges(user: CustomFirebaseUser?) { - activity?.let { + /*activity?.let { Glide.with(this@PrefFragment) .load(user?.photoUrl ?: R.mipmap.ic_launcher) .placeholder(R.mipmap.ic_launcher) @@ -93,7 +94,7 @@ class SettingsFragment : LeanbackSettingsFragmentCompat(), DialogPreference.Targ .fallback(R.mipmap.ic_launcher) .circleCrop() .into { resourceReady { image, _ -> p.icon = image } } - } + }*/ p.title = user?.displayName ?: "User" } @@ -154,7 +155,7 @@ class SettingsFragment : LeanbackSettingsFragmentCompat(), DialogPreference.Targ list.map { it.serviceName }.toTypedArray(), list.indexOfFirst { it.serviceName == service } ) { d, i -> - sourceFlow.tryEmit(list[i]) + //sourceFlow.tryEmit(list[i]) requireContext().currentService = list[i].serviceName d.dismiss() } @@ -163,11 +164,11 @@ class SettingsFragment : LeanbackSettingsFragmentCompat(), DialogPreference.Targ true } lifecycleScope.launch { - sourceFlow + /*sourceFlow .filterNotNull() .flowOn(Dispatchers.Main) .onEach { p.title = getString(R.string.currentSource, it.serviceName) } - .collect() + .collect()*/ } } } diff --git a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/TvUtils.kt b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/TvUtils.kt index 4d61f0f0a..3d6f7da4d 100644 --- a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/TvUtils.kt +++ b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/TvUtils.kt @@ -1,16 +1,11 @@ package com.programmersbox.animeworldtv import android.content.Context -import android.graphics.drawable.Drawable import android.util.Log import com.bumptech.glide.GlideBuilder -import com.bumptech.glide.RequestBuilder import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition import com.programmersbox.gsonutils.sharedPrefObjectDelegate -import kotlin.properties.Delegates var Context.currentService: String? by sharedPrefObjectDelegate(null) @@ -25,6 +20,7 @@ class AnimeWorldTvGlideModule : AppGlideModule() { @DslMarker annotation class GlideMarker +/* fun RequestBuilder.into(target: CustomTargetBuilder.() -> Unit) = into(CustomTargetBuilder().apply(target).build()) class CustomTargetBuilder internal constructor() { @@ -43,5 +39,4 @@ class CustomTargetBuilder internal constructor() { override fun onLoadCleared(placeholder: Drawable?) = loadCleared(placeholder) override fun onResourceReady(resource: T, transition: Transition?) = resourceReady(resource, transition) } - -} \ No newline at end of file +}*/ diff --git a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/DetailView.kt b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/DetailView.kt index 256046e56..0a7ddac54 100644 --- a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/DetailView.kt +++ b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/DetailView.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalTvMaterial3Api::class) + package com.programmersbox.animeworldtv.compose import androidx.activity.compose.BackHandler diff --git a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/MainView.kt b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/MainView.kt index 231f0dbe6..ea2181de5 100644 --- a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/MainView.kt +++ b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/MainView.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalTvMaterial3Api::class) + package com.programmersbox.animeworldtv.compose import androidx.compose.animation.AnimatedContent @@ -82,9 +84,11 @@ import androidx.tv.material3.StandardCardLayout import androidx.tv.material3.Text import coil.compose.AsyncImage import coil.request.ImageRequest +import com.programmersbox.models.ApiService import com.programmersbox.models.ItemModel -import com.programmersbox.models.sourceFlow import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flowOn @@ -95,7 +99,8 @@ import kotlinx.coroutines.flow.onEach @OptIn(ExperimentalTvMaterial3Api::class, ExperimentalTvFoundationApi::class) @Composable fun MainView() { - val vm = viewModel() + val currentSourceRepository = LocalCurrentSource.current + val vm = viewModel { MainViewModel(currentSourceRepository) } val navController = LocalNavController.current TvLazyColumn { @@ -552,13 +557,15 @@ private fun MoviesRowItemText( } } -class MainViewModel : ViewModel() { +class MainViewModel( + currentSourceRepository: CurrentSourceRepository, +) : ViewModel() { val randomShows = mutableStateListOf() val shows = mutableStateMapOf>() init { - sourceFlow + currentSourceRepository.asFlow() .filterNotNull() .flowOn(Dispatchers.IO) .flatMapMerge { it.getListFlow() } @@ -576,4 +583,19 @@ class MainViewModel : ViewModel() { } -fun Collection.randomN(n: Int): List = buildList { repeat(n) { add(this@randomN.random()) } } \ No newline at end of file +fun Collection.randomN(n: Int): List = buildList { repeat(n) { add(this@randomN.random()) } } + +class CurrentSourceRepository { + private val sourceFlow = MutableStateFlow(null) + + suspend fun emit(apiService: ApiService?) { + sourceFlow.emit(apiService) + } + + fun tryEmit(apiService: ApiService?) { + sourceFlow.tryEmit(apiService) + } + + fun asFlow() = sourceFlow.asStateFlow() + +} \ No newline at end of file diff --git a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/Utils.kt b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/Utils.kt index 93c8b5f18..ca8cc6861 100644 --- a/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/Utils.kt +++ b/animeworldtv/src/main/java/com/programmersbox/animeworldtv/compose/Utils.kt @@ -59,12 +59,13 @@ fun OtakuMaterialTheme( CompositionLocalProvider( LocalActivity provides remember { context.findActivity() }, LocalNavController provides navController, + LocalCurrentSource provides remember { CurrentSourceRepository() } ) { content() } } } val LocalNavController = staticCompositionLocalOf { error("No NavController Found!") } - +val LocalCurrentSource = staticCompositionLocalOf { CurrentSourceRepository() } val LocalActivity = staticCompositionLocalOf { error("Context is not an Activity.") } fun Context.findActivity(): FragmentActivity { @@ -99,9 +100,7 @@ val currentColorScheme: ColorScheme private val DPadEventsKeyCodes = listOf( KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT, KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 924cd13af..4dd426186 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("otaku-application") kotlin("android") id("com.mikepenz.aboutlibraries.plugin") + id("kotlinx-serialization") alias(libs.plugins.ksp) } @@ -19,11 +20,18 @@ android { } namespace = "com.programmersbox.otakuworld" + + configurations.all { + resolutionStrategy { + force(libs.preference) + } + } } dependencies { implementation(libs.material) implementation(libs.constraintlayout) + implementation(libs.androidxWebkit) testImplementation(TestDeps.junit) androidTestImplementation(TestDeps.androidJunit) androidTestImplementation(TestDeps.androidEspresso) @@ -40,6 +48,13 @@ dependencies { implementation(libs.bundles.roomLibs) ksp(libs.roomCompiler) + implementation(libs.kotlinxSerialization) + implementation(libs.jsoup) + implementation(libs.preference) { + isTransitive = true + } + implementation(libs.bundles.koinLibs) + //Custom Libraries implementation(Deps.jakepurple13Libs) val composeBom = platform(libs.composePlatform) @@ -47,5 +62,4 @@ dependencies { implementation(libs.bundles.compose) implementation(libs.androidxWindow) - } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9c8e835b0..0421d0417 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,13 +26,13 @@ + android:name="com.programmersbox.uiviews.checkers.BootReceived"> diff --git a/app/src/main/java/com/programmersbox/otakuworld/DialogScreens.kt b/app/src/main/java/com/programmersbox/otakuworld/DialogScreens.kt index e24546e15..f443add23 100644 --- a/app/src/main/java/com/programmersbox/otakuworld/DialogScreens.kt +++ b/app/src/main/java/com/programmersbox/otakuworld/DialogScreens.kt @@ -13,7 +13,15 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items @@ -23,18 +31,57 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.BottomSheetScaffold import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowRight +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.ChevronLeft +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.MotionPhotosAuto +import androidx.compose.material.icons.filled.SettingsBrightness +import androidx.compose.material.icons.filled.SwapHoriz +import androidx.compose.material.icons.filled.Update +import androidx.compose.material.icons.filled.ViewArray import androidx.compose.material.rememberBottomSheetScaffoldState import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.contentColorFor +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.* +import androidx.compose.ui.layout.LayoutModifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalViewConfiguration @@ -46,14 +93,27 @@ import androidx.constraintlayout.compose.ExperimentalMotionApi import androidx.constraintlayout.compose.MotionLayout import androidx.constraintlayout.compose.MotionLayoutDebugFlags import androidx.constraintlayout.compose.MotionScene -import com.programmersbox.uiviews.utils.* +import com.programmersbox.uiviews.utils.BaseBottomSheetDialogFragment +import com.programmersbox.uiviews.utils.CheckBoxSetting +import com.programmersbox.uiviews.utils.PreferenceSetting +import com.programmersbox.uiviews.utils.SliderSetting +import com.programmersbox.uiviews.utils.SwitchSetting import com.programmersbox.uiviews.utils.components.FullDynamicThemePrimaryColorsFromImage import com.programmersbox.uiviews.utils.components.rememberDynamicColorState +import com.programmersbox.uiviews.utils.currentColorScheme import kotlinx.coroutines.channels.ticker import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch -import java.util.* +import java.util.EnumSet import kotlin.math.roundToInt import kotlin.random.Random import kotlin.random.nextInt @@ -119,7 +179,7 @@ fun TestView(closeClick: () -> Unit) { actions = { IconButton(onClick = { closeClick() }) { Icon(imageVector = Icons.Default.Close, contentDescription = null) } }, scrollBehavior = scrollBehavior ) - androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) } }, sheetPeekHeight = 0.dp, @@ -236,7 +296,7 @@ fun SwitchView(closeClick: () -> Unit) { }, scrollBehavior = scrollBehavior ) - androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) } }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) @@ -274,7 +334,7 @@ fun CheckView(closeClick: () -> Unit) { }, scrollBehavior = scrollBehavior ) - androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) } }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) @@ -317,7 +377,7 @@ fun PagerView(closeClick: () -> Unit) { actions = { IconButton(onClick = { closeClick() }) { Icon(imageVector = Icons.Default.Close, contentDescription = null) } }, scrollBehavior = scrollBehavior ) - androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) } }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) @@ -472,7 +532,7 @@ fun PagerView(closeClick: () -> Unit) { text = { androidx.compose.material3.Text(settingLocation.toString()) } ) if (index < optionsList.value.size - 1) - androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) } } } @@ -623,7 +683,7 @@ fun OptimisticView(closeClick: () -> Unit) { actions = { IconButton(onClick = { closeClick() }) { Icon(imageVector = Icons.Default.Close, contentDescription = null) } }, scrollBehavior = scrollBehavior ) - androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) } }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) @@ -674,7 +734,7 @@ fun OptimisticView(closeClick: () -> Unit) { onClick = { scope.launch { count2.emit(++count) } } ) { Text("Hi: $timer") } - androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) } item { @@ -741,7 +801,7 @@ fun MotionLayoutView(closeClick: () -> Unit) { actions = { IconButton(onClick = { closeClick() }) { Icon(imageVector = Icons.Default.Close, contentDescription = null) } }, scrollBehavior = scrollBehavior ) - androidx.compose.material3.Divider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) } }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) @@ -830,7 +890,7 @@ fun ThemeingView(closeClick: () -> Unit) { actions = { IconButton(onClick = { closeClick() }) { Icon(imageVector = Icons.Default.Close, contentDescription = null) } }, scrollBehavior = scrollBehavior ) - androidx.compose.material3.Divider() + HorizontalDivider() } }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) diff --git a/app/src/main/java/com/programmersbox/otakuworld/MainActivity.kt b/app/src/main/java/com/programmersbox/otakuworld/MainActivity.kt index 4c7fa5970..3a73a426e 100644 --- a/app/src/main/java/com/programmersbox/otakuworld/MainActivity.kt +++ b/app/src/main/java/com/programmersbox/otakuworld/MainActivity.kt @@ -129,7 +129,6 @@ class MainActivity : AppCompatActivity() { private val sourceList = mutableStateListOf() private val favorites = mutableStateListOf() - @OptIn( ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class, @@ -173,6 +172,8 @@ class MainActivity : AppCompatActivity() { androidx.compose.material3.MaterialTheme(colorScheme = currentScheme) { + OtherStuffTrying() + /*var list by remember { mutableStateOf(listOf("A", "B", "C")) } LazyColumn { item { diff --git a/app/src/main/java/com/programmersbox/otakuworld/OtherCheck.kt b/app/src/main/java/com/programmersbox/otakuworld/OtherCheck.kt new file mode 100644 index 000000000..b94cb5a02 --- /dev/null +++ b/app/src/main/java/com/programmersbox/otakuworld/OtherCheck.kt @@ -0,0 +1,43 @@ +package com.programmersbox.otakuworld + +import android.content.Context +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import com.programmersbox.extensionloader.SourceRepository +import com.programmersbox.models.SourceInformation +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@Composable +fun OtherStuffTrying() { + val context = LocalContext.current + val vm = viewModel { OtherViewModel(context) } + + Column { + vm.list.forEach { + ListItem(headlineContent = { Text(it.name) }) + } + } +} + +class OtherViewModel(context: Context) : ViewModel() { + val sourceRepository = SourceRepository() + + val list = mutableStateListOf() + + init { + sourceRepository.sources + .onEach { + list.clear() + list.addAll(it) + } + .launchIn(viewModelScope) + } +} diff --git a/app/src/main/java/com/programmersbox/otakuworld/PlaygroundApp.kt b/app/src/main/java/com/programmersbox/otakuworld/PlaygroundApp.kt index 684a31681..01a449539 100644 --- a/app/src/main/java/com/programmersbox/otakuworld/PlaygroundApp.kt +++ b/app/src/main/java/com/programmersbox/otakuworld/PlaygroundApp.kt @@ -2,11 +2,25 @@ package com.programmersbox.otakuworld import android.app.Application import com.google.android.material.color.DynamicColors +import com.programmersbox.source_utilities.NetworkHelper +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.loadKoinModules +import org.koin.core.context.startKoin +import org.koin.dsl.module class PlaygroundApp : Application() { override fun onCreate() { super.onCreate() //TODO: This acts funky if user enabled force dark mode from developer options DynamicColors.applyToActivitiesIfAvailable(this) + + startKoin { + androidContext(this@PlaygroundApp) + loadKoinModules( + module { + single { NetworkHelper(get()) } + } + ) + } } } \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 937e3c7db..38d4a069a 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -22,6 +22,11 @@ gradlePlugin { implementationClass = "plugins.AndroidLibraryPlugin" } + register("otaku-source-application") { + id = "otaku-source-application" + implementationClass = "plugins.AndroidSourcePlugin" + } + register("otaku-easylauncher") { id = "otaku-easylauncher" implementationClass = "plugins.EasyLauncherSetup" @@ -36,7 +41,7 @@ gradlePlugin { dependencies { implementation(gradleApi()) - implementation(kotlin("stdlib")) + implementation(libs.kotlinStLib) implementation(libs.gradle) implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}") implementation(libs.easylauncher) diff --git a/buildSrc/src/main/kotlin/AppInfo.kt b/buildSrc/src/main/kotlin/AppInfo.kt index 1475e35c4..458c2b77b 100644 --- a/buildSrc/src/main/kotlin/AppInfo.kt +++ b/buildSrc/src/main/kotlin/AppInfo.kt @@ -1,5 +1,5 @@ object AppInfo { - const val otakuVersionName = "30.0.5" + const val otakuVersionName = "31.0.0" const val compileVersion = 34 const val minimumSdk = 23 const val targetSdk = 34 diff --git a/buildSrc/src/main/kotlin/plugins/AndroidPluginBase.kt b/buildSrc/src/main/kotlin/plugins/AndroidPluginBase.kt index 265a50e80..003f2f756 100644 --- a/buildSrc/src/main/kotlin/plugins/AndroidPluginBase.kt +++ b/buildSrc/src/main/kotlin/plugins/AndroidPluginBase.kt @@ -40,7 +40,7 @@ abstract class AndroidPluginBase( } } - fun Project.configureAndroidBase() { + private fun Project.configureAndroidBase() { extensions.findByType(clazz)?.apply { androidConfig(this@configureAndroidBase) compileSdkVersion(AppInfo.compileVersion) diff --git a/buildSrc/src/main/kotlin/plugins/AndroidSourcePlugin.kt b/buildSrc/src/main/kotlin/plugins/AndroidSourcePlugin.kt new file mode 100644 index 000000000..38c93a2ad --- /dev/null +++ b/buildSrc/src/main/kotlin/plugins/AndroidSourcePlugin.kt @@ -0,0 +1,70 @@ +package plugins + +import com.android.build.gradle.internal.dsl.BaseAppModuleExtension +import org.gradle.api.Project +import javax.inject.Inject +import kotlin.properties.Delegates + +class AndroidSourcePlugin : AndroidPluginBase(BaseAppModuleExtension::class) { + + override fun apply(target: Project) { + super.apply(target) + + target.extensions.create( + "otakuSourceInformation", + SourceInformation::class.java, + target + ) + } + + override fun Project.projectSetup() { + pluginManager.apply("com.android.application") + } + + override fun BaseAppModuleExtension.androidConfig(project: Project) {} +} + +abstract class SourceInformation @Inject constructor(private val project: Project) { + internal val source = SourceInfo() + + var name: String + get() = source.metadataName + set(value) { + source.metadataName = value + setInfo("extName", value) + } + + var classInfo: String + get() = source.metadataClass + set(value) { + source.metadataClass = value + setInfo("extClass", value) + } + + var sourceType: SourceType + get() = SourceType.Nothing + set(value) { + source.sourceType = value + setInfo( + "extSuffix", + value + .takeIf { it != SourceType.Nothing } + ?.name + ?.lowercase() ?: error("SourceType not Set") + ) + } + + private fun setInfo(key: String, value: String) { + project.extensions + .findByType(BaseAppModuleExtension::class.java) + ?.apply { defaultConfig { manifestPlaceholders[key] = value } } + } +} + +internal class SourceInfo { + var metadataName by Delegates.notNull() + var metadataClass by Delegates.notNull() + var sourceType by Delegates.notNull() +} + +enum class SourceType { Anime, Manga, Novel, Nothing } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ac6f9777..d29115396 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,34 +1,34 @@ [versions] easylauncher = "6.2.0" -firebaseCrashlyticsGradle = "2.9.6" +firebaseCrashlyticsGradle = "2.9.7" googleServices = "4.3.15" gradle = "8.1.0-beta05" kamelImage = "0.6.1" -kotlin = "1.8.22" -latestAboutLibsRelease = "10.8.0" +kotlin = "1.9.0" +latestAboutLibsRelease = "10.8.3" coroutinesVersion = "1.7.2" glideVersion = "4.15.1" navigationSafeArgsGradlePlugin = "2.6.0" -pagingVersion = "3.1.1" +pagingVersion = "3.2.0" roomVersion = "2.5.2" -navVersion = "2.7.0-beta02" +navVersion = "2.7.0-rc01" koinVersion = "3.4.2" lottieVersion = "6.0.0" coil = "2.4.0" lifecycle = "2.6.1" -jetpack = "1.6.0-alpha01" -jetpackCompiler = "1.4.8" -jetbrainsCompiler = "1.4.1" +jetpack = "1.6.0-alpha02" +jetpackCompiler = "1.5.1" +jetbrainsCompiler = "1.4.3" accompanist = "0.31.4-beta" okhttpVersion = "4.11.0" ktorVersion = "2.3.2" -tvCompose = "1.0.0-alpha07" +tvCompose = "1.0.0-alpha08" workVersion = "2.8.1" -ziplineVersion = "0.9.12" +ziplineVersion = "1.1.0" landscapist = "2.2.2" protobufVersion = "3.23.4" sketchVersion = "3.2.3" -composeBomVersion = "2023.07.00-alpha01" +composeBomVersion = "2023.07.00-alpha02" ### MangaWorld piasy = "1.8.1" @@ -41,7 +41,7 @@ media3Version = "1.1.0" kotlinGradle = { id = "org.jetbrains.kotlin:kotlin.gradle.plugin", version.ref = "kotlin" } kotlinSerializationGradle = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } navSafeArgsGradle = { id = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navVersion" } -ksp = { id = "com.google.devtools.ksp", version = "1.8.22-1.0.11" } +ksp = { id = "com.google.devtools.ksp", version = "1.9.0-1.0.11" } [libraries] easylauncher = { module = "com.project.starter:easylauncher", version.ref = "easylauncher" } @@ -142,22 +142,22 @@ navUiKtx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "na navTesting = { module = "androidx.navigation:navigation-testing", version.ref = "navVersion" } reactiveNetwork = "ru.beryukhov:flowreactivenetwork:1.0.4" -fragmentKtx = "androidx.fragment:fragment-ktx:1.6.0" +fragmentKtx = "androidx.fragment:fragment-ktx:1.6.1" stetho = "com.facebook.stetho:stetho:1.6.0" -iconicsCore = "com.mikepenz:iconics-core:5.4.0" +iconicsCore = "com.mikepenz:iconics-core:5.5.0-compose01" androidxLegacySupport = "androidx.legacy:legacy-support-v4:1.0.0" -firebaseAuth = "com.google.firebase:firebase-auth:22.0.0" -googlePlayAds = "com.google.android.gms:play-services-ads:22.1.0" +firebaseAuth = "com.google.firebase:firebase-auth:22.1.0" +googlePlayAds = "com.google.android.gms:play-services-ads:22.2.0" -pagingCompose = "androidx.paging:paging-compose:3.2.0-rc01" +pagingCompose = "androidx.paging:paging-compose:3.2.0" fileChooser = "com.github.hedzr:android-file-chooser:1.2.0" storage = "com.anggrayudi:storage:1.5.2" -androidxWindow = "androidx.window:window:1.2.0-alpha03" +androidxWindow = "androidx.window:window:1.2.0-beta01" androidBrowserHelper = "com.google.androidbrowserhelper:androidbrowserhelper:2.5.0" androidxBrowser = "androidx.browser:browser:1.5.0" @@ -165,20 +165,20 @@ fastScroll = "me.zhanghai.android.fastscroll:library:1.1.8" showMoreLess = "com.github.noowenz:ShowMoreLess:1.0.3" toolbarCompose = "me.onebone:toolbar-compose:2.3.5" -lazyColumnScrollbar = "com.github.nanihadesuka:LazyColumnScrollbar:1.6.3" +lazyColumnScrollbar = "com.github.nanihadesuka:LazyColumnScrollbar:1.7.2" datastore = "androidx.datastore:datastore:1.0.0" datastorePref = "androidx.datastore:datastore-preferences:1.0.0" jsoup = "org.jsoup:jsoup:1.16.1" -crashlytics = "com.google.firebase:firebase-crashlytics:18.3.7" +crashlytics = "com.google.firebase:firebase-crashlytics:18.4.0" analytics = "com.google.firebase:firebase-analytics:21.3.0" -playServices = "com.google.android.gms:play-services-auth:20.5.0" +playServices = "com.google.android.gms:play-services-auth:20.6.0" preference = "androidx.preference:preference-ktx:1.2.0" -recyclerview = "androidx.recyclerview:recyclerview:1.3.0" +recyclerview = "androidx.recyclerview:recyclerview:1.3.1" constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" swiperefresh = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" @@ -262,7 +262,7 @@ androidxWebkit = "androidx.webkit:webkit:1.7.0" mlkitTranslate = "com.google.mlkit:translate:17.0.1" mlkitLanguage = "com.google.mlkit:language-id:17.0.4" firebaseDatabase = "com.google.firebase:firebase-database-ktx:20.2.2" -firebaseFirestore = "com.google.firebase:firebase-firestore-ktx:24.6.1" +firebaseFirestore = "com.google.firebase:firebase-firestore-ktx:24.7.0" firebaseUiAuth = "com.firebaseui:firebase-ui-auth:8.0.2" protobufJava = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobufVersion" } diff --git a/manga_sources/defaultmangasources/.gitignore b/manga_sources/defaultmangasources/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/manga_sources/defaultmangasources/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/manga_sources/defaultmangasources/build.gradle.kts b/manga_sources/defaultmangasources/build.gradle.kts new file mode 100644 index 000000000..f7306e5c4 --- /dev/null +++ b/manga_sources/defaultmangasources/build.gradle.kts @@ -0,0 +1,43 @@ +import plugins.SourceType + +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + id("otaku-source-application") +} + +android { + namespace = "com.programmersbox.defaultmangasources" + + defaultConfig { + applicationId = "com.programmersbox.defaultmangasources" + } +} + +otakuSourceInformation { + name = "Default Manga Sources" + classInfo = ".MangaSources" + sourceType = SourceType.Manga +} + +dependencies { + testImplementation(TestDeps.junit) + androidTestImplementation(TestDeps.androidJunit) + androidTestImplementation(TestDeps.androidEspresso) + implementation(libs.bundles.okHttpLibs) + + implementation(libs.coroutinesCore) + + implementation(Deps.gsonutils) + implementation(Deps.helpfulutils) + debugImplementation(Deps.loggingutils) + implementation(libs.gson) + + implementation(libs.jsoup) + + implementation(projects.models) + implementation(projects.mangaSources) + api(projects.sourceUtilities) + implementation(libs.bundles.ktorLibs) + + implementation(libs.bundles.koinLibs) +} \ No newline at end of file diff --git a/manga_sources/defaultmangasources/proguard-rules.pro b/manga_sources/defaultmangasources/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/manga_sources/defaultmangasources/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/manga_sources/defaultmangasources/src/main/AndroidManifest.xml b/manga_sources/defaultmangasources/src/main/AndroidManifest.xml new file mode 100644 index 000000000..418151c5b --- /dev/null +++ b/manga_sources/defaultmangasources/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/manga_sources/defaultmangasources/src/main/java/com/programmersbox/defaultmangasources/MangaSources.kt b/manga_sources/defaultmangasources/src/main/java/com/programmersbox/defaultmangasources/MangaSources.kt new file mode 100644 index 000000000..55ce36d8e --- /dev/null +++ b/manga_sources/defaultmangasources/src/main/java/com/programmersbox/defaultmangasources/MangaSources.kt @@ -0,0 +1,11 @@ +package com.programmersbox.defaultmangasources + +import com.programmersbox.manga_sources.Sources +import com.programmersbox.models.ApiService +import com.programmersbox.models.ApiServicesCatalog + +object MangaSources : ApiServicesCatalog { + override fun createSources(): List = Sources.entries.filterNot { it.notWorking } + + override val name: String get() = "Default Manga Sources" +} \ No newline at end of file diff --git a/manga_sources/defaultmangasources/src/main/res/drawable/ic_launcher_background.xml b/manga_sources/defaultmangasources/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/manga_sources/defaultmangasources/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/manga_sources/defaultmangasources/src/main/res/drawable/ic_launcher_foreground.xml b/manga_sources/defaultmangasources/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/manga_sources/defaultmangasources/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/manga_sources/defaultmangasources/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/manga_sources/defaultmangasources/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/manga_sources/defaultmangasources/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/manga_sources/defaultmangasources/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-hdpi/ic_launcher.webp b/manga_sources/defaultmangasources/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/manga_sources/defaultmangasources/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/manga_sources/defaultmangasources/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/manga_sources/defaultmangasources/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-mdpi/ic_launcher.webp b/manga_sources/defaultmangasources/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/manga_sources/defaultmangasources/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/manga_sources/defaultmangasources/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/manga_sources/defaultmangasources/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-xhdpi/ic_launcher.webp b/manga_sources/defaultmangasources/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/manga_sources/defaultmangasources/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/manga_sources/defaultmangasources/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/manga_sources/defaultmangasources/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/manga_sources/defaultmangasources/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/manga_sources/defaultmangasources/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/manga_sources/defaultmangasources/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/manga_sources/defaultmangasources/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/manga_sources/defaultmangasources/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/manga_sources/defaultmangasources/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/manga_sources/defaultmangasources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/manga_sources/defaultmangasources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/manga_sources/defaultmangasources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/manga_sources/defaultmangasources/src/main/res/values/colors.xml b/manga_sources/defaultmangasources/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/manga_sources/defaultmangasources/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/manga_sources/defaultmangasources/src/main/res/values/strings.xml b/manga_sources/defaultmangasources/src/main/res/values/strings.xml new file mode 100644 index 000000000..df51fb5be --- /dev/null +++ b/manga_sources/defaultmangasources/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Default Manga Sources + \ No newline at end of file diff --git a/manga_sources/src/main/java/com/programmersbox/manga_sources/Sources.kt b/manga_sources/src/main/java/com/programmersbox/manga_sources/Sources.kt index 3a4e97a00..50f07770c 100644 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/Sources.kt +++ b/manga_sources/src/main/java/com/programmersbox/manga_sources/Sources.kt @@ -1,13 +1,8 @@ package com.programmersbox.manga_sources import com.programmersbox.manga_sources.manga.AsuraScans -import com.programmersbox.manga_sources.manga.MangaFourLife import com.programmersbox.manga_sources.manga.MangaHere -import com.programmersbox.manga_sources.manga.MangaPark -import com.programmersbox.manga_sources.manga.MangaRead -import com.programmersbox.manga_sources.manga.MangaReadCo import com.programmersbox.manga_sources.manga.MangaTown -import com.programmersbox.manga_sources.manga.Mangamutiny import com.programmersbox.manga_sources.manga.NineAnime import com.programmersbox.manga_sources.manga.Tsumino import com.programmersbox.models.ApiService @@ -24,17 +19,9 @@ enum class Sources( MANGA_HERE(domain = "mangahere", source = MangaHere), ASURA_SCANS(domain = "asurascans", source = AsuraScans), - MANGA_4_LIFE(domain = "manga4life", source = MangaFourLife), - MANGA_PARK(domain = "mangapark", source = MangaPark), - MANGA_READ(domain = "mangaread", source = MangaRead), - MANGA_READ_CO(domain = "mangareadco", source = MangaReadCo), NINE_ANIME(domain = "nineanime", source = NineAnime), //MANGAKAKALOT(domain = "mangakakalot", source = Mangakakalot), - MANGAMUTINY(domain = "mangamutiny", source = Mangamutiny, filterOutOfUpdate = true) { - override val notWorking: Boolean get() = true - }, - //MANGA_DOG(domain = "mangadog", source = MangaDog), //INKR(domain = "mangarock", source = com.programmersbox.manga_sources.mangasources.manga.INKR), TSUMINO(domain = "tsumino", isAdult = true, source = Tsumino), diff --git a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaBox.kt b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaBox.kt index 1b56f24b1..964cb5a19 100644 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaBox.kt +++ b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaBox.kt @@ -368,7 +368,7 @@ object Manganelo : MangaBox("Manganato", "https://manganato.com") { object MangaTown : ApiService, KoinComponent { - override val notWorking: Boolean get() = true + override val notWorking: Boolean get() = false private val context: Context by inject() 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 deleted file mode 100644 index 2b69f01b9..000000000 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaFourLife.kt +++ /dev/null @@ -1,205 +0,0 @@ -package com.programmersbox.manga_sources.manga - -import android.annotation.SuppressLint -import androidx.compose.ui.util.fastMap -import com.github.salomonbrys.kotson.get -import com.github.salomonbrys.kotson.string -import com.google.gson.JsonElement -import com.programmersbox.gsonutils.fromJson -import com.programmersbox.gsonutils.getApi -import com.programmersbox.gsonutils.getJsonApi -import com.programmersbox.models.* -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.* - -object MangaFourLife : ApiService { - - override val baseUrl: String = "https://manga4life.com" - - override val serviceName: String get() = "MANGA_4_LIFE" - - private val headers: List> = listOf( - "User-Agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0" - ) - - private val mangaList = mutableListOf() - - private fun getManga(pageNumber: Int): List { - 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() - ) - } - val endRange = ((pageNumber * 24) - 1).let { if (it <= mangaList.lastIndex) it else mangaList.lastIndex } - return mangaList.subList((pageNumber - 1) * 24, endRange) - } - - 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 suspend fun recent(page: Int): List = getManga(page) - - override suspend fun allList(page: Int): List = getManga(page) - - private fun getApiVersion() = getJsonApi>("https://manga4life.com/_search.php")?.fastMap { - ItemModel( - title = it.s.toString(), - description = "", - url = "https://manga4life.com/manga/${it.i}", - imageUrl = "https://cover.nep.li/cover/${it.i}.jpg", - source = this - ) - } - - private val toMangaModel: (LifeBase) -> ItemModel = { - ItemModel( - title = it.s.toString(), - description = "Last updated: ${it.ls}", - url = "https://manga4life.com/manga/${it.i}", - imageUrl = "https://temp.compsci88.com/cover/${it.i}.jpg", - source = this - ) - } - - @SuppressLint("ConstantLocale") - private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - - - 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 = doc.select("div.BoxBody > div.row").select("img").attr("abs:src"), - 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 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() - if (1 != t) index = "-index-$t" - val n = e.substring(1, e.length - 1) - var suffix = "" - val path = e.substring(e.length - 1).toInt() - if (0 != path) suffix = ".$path" - return "-chapter-$n$index$suffix.html" - } - - private fun chapterImage(e: String): String { - val a = e.substring(1, e.length - 1) - val b = e.substring(e.length - 1).toInt() - return if (b == 0) a else "$a.$b" - } - - 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?) - - private data class LifeChapter(val Chapter: String?, val Type: String?, val Date: String?, val ChapterName: String?) - - private data class LifeBase( - val i: String?, - val s: String?, - val o: String?, - val ss: String?, - val ps: String?, - val t: String?, - val v: String?, - val vm: String?, - val y: String?, - val a: List?, - val al: List?, - val l: String?, - val lt: Number?, - val ls: String?, - val g: List?, - val h: Boolean? - ) - -} \ No newline at end of file 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 deleted file mode 100644 index d867dd736..000000000 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaPark.kt +++ /dev/null @@ -1,278 +0,0 @@ -package com.programmersbox.manga_sources.manga - -import android.annotation.SuppressLint -import androidx.compose.ui.util.fastMap -import app.cash.zipline.QuickJs -import com.programmersbox.manga_sources.Sources -import com.programmersbox.manga_sources.utilities.* -import com.programmersbox.models.* -import com.programmersbox.source_utilities.NetworkHelper -import com.programmersbox.source_utilities.asJsoup -import com.programmersbox.source_utilities.cloudflare -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonPrimitive -import okhttp3.Request -import okhttp3.Response -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import java.text.SimpleDateFormat -import java.util.* - -object MangaPark : ApiService, KoinComponent { - - override val baseUrl = "https://mangapark.net" - - override val serviceName: String get() = "MANGA_PARK" - - private val helper: NetworkHelper by inject() - - private fun String.v3Url() = baseUrl - - 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 suspend fun allList(page: Int): List { - return cloudflare(helper, "${baseUrl.v3Url()}/browse?sort=d007&page=$page").execute().asJsoup().browseToItemModel() - } - - 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( - title = it.select("a.fw-bold").text(), - description = it.select("div.limit-html").text(), - url = it.select("a.fw-bold").attr("abs:href"), - imageUrl = it.select("a.position-relative img").attr("abs:src"), - source = Sources.MANGA_PARK - ) - } - - 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) - } - - private fun chapterListParse(response: Response, mangaUrl: String): List { - val f = "div.p-2:not(:has(.px-3))" - return response.asJsoup() - .select("div.episode-list #chap-index") - .flatMap { it.select(f).fastMap { chapterFromElement(it) } } - .fastMap { - ChapterModel( - name = it.name, - url = it.url, - uploaded = it.originalDate, - sourceUrl = mangaUrl, - source = this - ).apply { uploadedTime = it.dateUploaded } - } - } - - private fun chapterListParse(response: Document, mangaUrl: String): List { - val f = "div.p-2:not(:has(.px-3))" - return response - .select("div.episode-list #chap-index") - .flatMap { it.select(f).fastMap { chapterFromElement(it) } } - .fastMap { - ChapterModel( - name = it.name, - url = it.url, - uploaded = it.originalDate, - sourceUrl = mangaUrl, - source = this - ).apply { uploadedTime = it.dateUploaded } - } - } - - private class SChapter { - var url: String = "" - var name: String = "" - var chapterNumber: Float = 0f - var dateUploaded: Long? = null - var originalDate: String = "" - } - - private fun chapterFromElement(element: Element): SChapter { - val urlElement = element.select("a.ms-3") - val time = element.select("div.extra > i.ps-2").text() - return SChapter().apply { - name = urlElement.text().removePrefix("Ch").trim()//urlElement.text() - chapterNumber = urlElement.attr("href").substringAfterLast("/").toFloatOrNull() ?: 0f - if (time != "") { - dateUploaded = parseDate(time) - } - originalDate = time - url = baseUrl.v3Url() + urlElement.attr("href") - } - } - - private fun chapterFromElement(element: Element, lastNum: Float): SChapter { - fun Float.incremented() = this + .00001F - fun Float?.orIncrementLastNum() = if (this == null || this < lastNum) lastNum.incremented() else this - - return SChapter().apply { - url = element.select(".tit > a").first()!!.attr("href").replaceAfterLast("/", "") - name = element.select(".tit > a").first()!!.text() - // Get the chapter number or create a unique one if it's not available - chapterNumber = Regex("""\b\d+\.?\d?\b""").findAll(name) - .toList() - .fastMap { it.value.toFloatOrNull() } - .let { nums -> - when { - nums.count() == 1 -> nums[0].orIncrementLastNum() - nums.count() >= 2 -> nums[1].orIncrementLastNum() - else -> lastNum.incremented() - } - } - dateUploaded = element.select(".time").firstOrNull()?.text()?.trim()?.let { parseDate(it) } - originalDate = element.select(".time").firstOrNull()?.text()?.trim().toString() - } - } - - private val cryptoJS by lazy { helper.client.newCall(GET(cryptoJSUrl, MangaUtils.headers)).execute().body!!.string() } - - private const val cryptoJSUrl = "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js" - - private val dateFormat = SimpleDateFormat("MMM d, yyyy, HH:mm a", Locale.ENGLISH) - private val dateFormatTimeOnly = SimpleDateFormat("HH:mm a", Locale.ENGLISH) - - @SuppressLint("DefaultLocale") - private fun parseDate(date: String): Long? { - val lcDate = date.lowercase() - if (lcDate.endsWith("ago")) return parseRelativeDate(lcDate) - - // Handle 'yesterday' and 'today' - var relativeDate: Calendar? = null - if (lcDate.startsWith("yesterday")) { - relativeDate = Calendar.getInstance() - relativeDate.add(Calendar.DAY_OF_MONTH, -1) // yesterday - } else if (lcDate.startsWith("today")) { - relativeDate = Calendar.getInstance() - } - - relativeDate?.let { - // Since the date is not specified, it defaults to 1970! - val time = dateFormatTimeOnly.parse(lcDate.substringAfter(' ')) - val cal = Calendar.getInstance() - cal.time = time!! - - // Copy time to relative date - it.set(Calendar.HOUR_OF_DAY, cal.get(Calendar.HOUR_OF_DAY)) - it.set(Calendar.MINUTE, cal.get(Calendar.MINUTE)) - return it.timeInMillis - } - - return dateFormat.parse(lcDate)?.time - } - - /** - * Parses dates in this form: - * `11 days ago` - */ - private fun parseRelativeDate(date: String): Long? { - val trimmedDate = date.split(" ") - - if (trimmedDate[2] != "ago") return null - - val number = when (trimmedDate[0]) { - "a" -> 1 - else -> trimmedDate[0].toIntOrNull() ?: return null - } - val unit = trimmedDate[1].removeSuffix("s") // Remove 's' suffix - - val now = Calendar.getInstance() - - // Map English unit to Java unit - val javaUnit = when (unit) { - "year" -> Calendar.YEAR - "month" -> Calendar.MONTH - "week" -> Calendar.WEEK_OF_MONTH - "day" -> Calendar.DAY_OF_MONTH - "hour" -> Calendar.HOUR - "minute" -> Calendar.MINUTE - "second" -> Calendar.SECOND - else -> return null - } - - now.add(javaUnit, -number) - - return now.timeInMillis - } - - 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 suspend fun chapterInfo(chapterModel: ChapterModel): List { - val script = cloudflare(helper, chapterModel.url).execute().asJsoup() - .select("script:containsData(imgHttpLis):containsData(amWord):containsData(amPass)").html() - val imgHttpLisString = script.substringAfter("const imgHttpLis =").substringBefore(";").trim() - val imgHttpLis = Json.parseToJsonElement(imgHttpLisString).jsonArray.map { it.jsonPrimitive.content } - val amWord = script.substringAfter("const amWord =").substringBefore(";").trim() - val amPass = script.substringAfter("const amPass =").substringBefore(";").trim() - - val decryptScript = cryptoJS + "CryptoJS.AES.decrypt($amWord, $amPass).toString(CryptoJS.enc.Utf8);" - - val imgAccListString = QuickJs.create().use { it.evaluate(decryptScript).toString() } - val imgAccList = Json.parseToJsonElement(imgAccListString).jsonArray.map { it.jsonPrimitive.content } - - return imgHttpLis.zip(imgAccList).mapIndexed { i, (imgUrl, imgAcc) -> "$imgUrl?$imgAcc" } - .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/MangaRead.kt b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaRead.kt deleted file mode 100644 index 65f2107da..000000000 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/MangaRead.kt +++ /dev/null @@ -1,337 +0,0 @@ -package com.programmersbox.manga_sources.manga - -import com.programmersbox.manga_sources.Sources -import com.programmersbox.manga_sources.utilities.GET -import com.programmersbox.manga_sources.utilities.POST -import com.programmersbox.models.ApiService -import com.programmersbox.models.ChapterModel -import com.programmersbox.models.InfoModel -import com.programmersbox.models.ItemModel -import com.programmersbox.models.Storage -import com.programmersbox.source_utilities.NetworkHelper -import com.programmersbox.source_utilities.asJsoup -import okhttp3.CacheControl -import okhttp3.FormBody -import okhttp3.Headers -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import org.jsoup.Jsoup -import org.jsoup.nodes.Element -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import java.util.Locale -import java.util.concurrent.TimeUnit -import kotlin.math.absoluteValue -import kotlin.random.Random - -object MangaRead : Madara( - "https://www.mangaread.org", - "MANGA_READ", -) { - override val sources: Sources - get() = Sources.MANGA_READ -} - -object MangaReadCo : Madara( - "https://mangaread.co", - "MANGA_READ_CO", -) { - override val sources: Sources - get() = Sources.MANGA_READ_CO -} - -abstract class Madara( - override val baseUrl: String, - override val serviceName: String, -) : ApiService, KoinComponent { - - abstract val sources: Sources - - override val canScroll: Boolean = true - - private val helper: NetworkHelper by inject() - - private val client: OkHttpClient by lazy { - helper.cloudflareClient.newBuilder() - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build() - } - - private val userAgentRandomizer = " ${Random.nextInt().absoluteValue}" - - private fun headersBuilder(): Headers.Builder = Headers.Builder() - .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/78.0$userAgentRandomizer") - .add("Referer", "$baseUrl/") - - private fun formBuilder(page: Int, popular: Boolean) = FormBody.Builder().apply { - add("action", "madara_load_more") - add("page", (page - 1).toString()) - add("template", "madara-core/content/content-archive") - add("vars[orderby]", "meta_value_num") - add("vars[paged]", "1") - add("vars[posts_per_page]", "20") - add("vars[post_type]", "wp-manga") - add("vars[post_status]", "publish") - add("vars[meta_key]", if (popular) "_wp_manga_views" else "_latest_update") - add("vars[order]", "desc") - add("vars[sidebar]", if (popular) "full" else "right") - add("vars[manga_archives_item_layout]", "big_thumbnail") - } - - protected open val filterNonMangaItems = true - - protected open val mangaEntrySelector: String by lazy { - if (filterNonMangaItems) ".manga" else "" - } - - override suspend fun recent(page: Int): List { - val request = client.newCall( - GET( - url = "$baseUrl/$mangaSubString/${searchPage(page)}?m_orderby=latest", - headers = headersBuilder().build(), - cache = CacheControl.FORCE_NETWORK, - ) - ).execute() - return request.asJsoup() - .select("div.page-item-detail:not(:has(a[href*='bilibilicomics.com']))$mangaEntrySelector") - .map { - val info = it.select("div.post-title a") - ItemModel( - url = info.attr("abs:href"), // intentionally not using setUrlWithoutDomain - title = info.text(), - imageUrl = it.select("img").first()?.let { imageFromElement(it) }.orEmpty(), - source = sources, - description = "" - ).also { it.extras["Referer"] = it.url } - } - } - - private fun imageFromElement(element: Element): String? { - return when { - element.hasAttr("data-src") -> element.attr("abs:data-src") - element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src") - element.hasAttr("srcset") -> element.attr("abs:srcset").substringBefore(" ") - else -> element.attr("abs:src") - } - } - - override suspend fun allList(page: Int): List { - val request = client.newCall( - GET( - url = "$baseUrl/$mangaSubString/${searchPage(page)}?m_orderby=views", - headers = headersBuilder().build(), - cache = CacheControl.FORCE_NETWORK, - ) - ).execute() - return request.asJsoup() - .select("div.page-item-detail:not(:has(a[href*='bilibilicomics.com']))$mangaEntrySelector") - .map { - val info = it.select("div.post-title a") - ItemModel( - url = info.attr("abs:href"), // intentionally not using setUrlWithoutDomain - title = info.text(), - imageUrl = it.select("img").first()?.let { imageFromElement(it) }.orEmpty(), - source = sources, - description = "" - ).also { it.extras["Referer"] = it.url } - } - } - - override suspend fun itemInfo(model: ItemModel): InfoModel { - val doc = Jsoup.connect(model.url).get() - - val title = doc.select("div.post-title h3, div.post-title h1") - val description = - doc.select("div.description-summary div.summary__content, div.summary_content div.post-content_item > h5 + div, div.summary_content div.manga-excerpt") - .let { - if (it.select("p").text().isNotEmpty()) { - it.select("p").joinToString(separator = "\n\n") { p -> - p.text().replace("
", "\n") - } - } else { - it.text() - } - } - - val genres = doc.select("div.genres-content a") - .map { element -> element.text().lowercase(Locale.ROOT) } - .toMutableSet() - - doc.select("div.tags-content a").forEach { element -> - if (genres.contains(element.text()).not()) { - genres.add(element.text().lowercase(Locale.ROOT)) - } - } - - return InfoModel( - title = title.text(), - genres = genres.toList(), - url = model.url, - alternativeNames = emptyList(), - chapters = chapterListParse(client.newCall(GET(model.url, headersBuilder().build())).execute(), model.url), - description = description, - imageUrl = doc.select("div.summary_image img").first()?.let { imageFromElement(it) }.orEmpty(), - source = sources - ) - } - - private fun oldXhrChaptersRequest(mangaId: String): Request { - val form = FormBody.Builder() - .add("action", "manga_get_chapters") - .add("manga", mangaId) - .build() - - val xhrHeaders = headersBuilder() - .add("Content-Length", form.contentLength().toString()) - .add("Content-Type", form.contentType().toString()) - .add("Referer", baseUrl) - .add("X-Requested-With", "XMLHttpRequest") - .build() - - return POST("$baseUrl/wp-admin/admin-ajax.php", xhrHeaders, form) - } - - private fun xhrChaptersRequest(mangaUrl: String): Request { - val xhrHeaders = headersBuilder() - .add("Referer", baseUrl) - .add("X-Requested-With", "XMLHttpRequest") - .build() - - return POST("$mangaUrl/ajax/chapters", xhrHeaders) - } - - protected val useNewChapterEndpoint: Boolean = false - - /** - * Internal attribute to control if it should always use the - * new chapter endpoint after a first check if useNewChapterEndpoint is - * set to false. Using a separate variable to still allow the other - * one to be overridable manually in each source. - */ - private var oldChapterEndpointDisabled: Boolean = false - - - private fun chapterListParse(response: Response, sourceUrl: String): List { - val document = response.asJsoup() - val chaptersWrapper = document.select("div[id^=manga-chapters-holder]") - - var chapterElements = document.select("li.wp-manga-chapter") - - if (chapterElements.isEmpty() && !chaptersWrapper.isNullOrEmpty()) { - val mangaUrl = document.location().removeSuffix("/") - val mangaId = chaptersWrapper.attr("data-id") - - var xhrRequest = if (useNewChapterEndpoint || oldChapterEndpointDisabled) - xhrChaptersRequest(mangaUrl) else oldXhrChaptersRequest(mangaId) - var xhrResponse = client.newCall(xhrRequest).execute() - - // Newer Madara versions throws HTTP 400 when using the old endpoint. - if (!useNewChapterEndpoint && xhrResponse.code == 400) { - xhrResponse.close() - // Set it to true so following calls will be made directly to the new endpoint. - oldChapterEndpointDisabled = true - - xhrRequest = xhrChaptersRequest(mangaUrl) - xhrResponse = client.newCall(xhrRequest).execute() - } - - chapterElements = xhrResponse.asJsoup().select("li.wp-manga-chapter") - xhrResponse.close() - } - - return chapterElements.map { chapterFromElement(it, sourceUrl) } - } - - private fun chapterFromElement(element: Element, sourceUrl: String): ChapterModel { - val info = element.select("a") - - return ChapterModel( - url = info.attr("abs:href").let { - it.substringBefore("?style=paged") + if (!it.endsWith("?style=list")) "?style=list" else "" - }, - name = info.text(), - source = sources, - sourceUrl = sourceUrl, - uploaded = element.select("img:not(.thumb)").firstOrNull()?.attr("alt") - ?: element.select("span a").firstOrNull()?.attr("title") ?: "" - ) - } - - override suspend fun chapterInfo(chapterModel: ChapterModel): List { - val doc = client.newCall(GET(chapterModel.url, headersBuilder().build())).execute().asJsoup() - - return doc.select("div.page-break, li.blocks-gallery-item, .reading-content .text-left:not(:has(.blocks-gallery-item)) img") - .mapIndexed { index, element -> - Storage( - sub = index.toString(), - filename = doc.location(), - link = element.select("img").first()?.let { - it.absUrl(if (it.hasAttr("data-src")) "data-src" else "src") - }, - source = doc.location() - ).also { it.headers["Referer"] = chapterModel.url } - } - } - - override suspend fun sourceByUrl(url: String): ItemModel { - val doc = Jsoup.connect(url).get() - - val title = doc.select("div.post-title h3, div.post-title h1") - val description = - doc.select("div.description-summary div.summary__content, div.summary_content div.post-content_item > h5 + div, div.summary_content div.manga-excerpt") - .let { - if (it.select("p").text().isNotEmpty()) { - it.select("p").joinToString(separator = "\n\n") { p -> - p.text().replace("
", "\n") - } - } else { - it.text() - } - } - - val genres = doc.select("div.genres-content a") - .map { element -> element.text().lowercase(Locale.ROOT) } - .toMutableSet() - - doc.select("div.tags-content a").forEach { element -> - if (genres.contains(element.text()).not()) { - genres.add(element.text().lowercase(Locale.ROOT)) - } - } - - return ItemModel( - title = title.text(), - url = url, - description = description, - imageUrl = doc.select("div.summary_image img").first()?.let { imageFromElement(it) }.orEmpty(), - source = sources - ) - } - - override suspend fun search(searchText: CharSequence, page: Int, list: List): List { - val url = "$baseUrl/page/$page/".toHttpUrlOrNull()!!.newBuilder() - url.addQueryParameter("s", searchText.toString()) - url.addQueryParameter("post_type", "wp-manga") - - return client.newCall(GET(url.toString(), headersBuilder().build())).execute().asJsoup() - .select("div.c-tabs-item__content") - .map { - val info = it.select("div.post-title a") - ItemModel( - title = info.text(), - url = info.attr("abs:href"), - description = "", - imageUrl = it.select("img").first()?.let { imageFromElement(it) }.orEmpty(), - source = sources - ) - } - } - - open val mangaSubString = "manga" - - protected open fun searchPage(page: Int): String = "page/$page/" - -} \ No newline at end of file diff --git a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/Mangamutiny.kt b/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/Mangamutiny.kt deleted file mode 100644 index 45126b2f5..000000000 --- a/manga_sources/src/main/java/com/programmersbox/manga_sources/manga/Mangamutiny.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.programmersbox.manga_sources.manga - -import androidx.compose.ui.util.fastMap -import com.programmersbox.gsonutils.getJsonApi -import com.programmersbox.gsonutils.header -import com.programmersbox.models.ApiService -import com.programmersbox.models.ItemModel -import okhttp3.Request - -object Mangamutiny : ApiService { - - override val baseUrl = "https://api.mangamutiny.org" - private const val mangaApiPath = "/v1/public/manga" - private const val chapterApiPath = "/v1/public/chapter" - - override val serviceName: String get() = "MANGAMUTINY" - - override val websiteUrl: String = "https://mangamutiny.org" - - private val headers: List> - get() = listOf( - "Accept" to "application/json", - "Origin" to "https://mangamutiny.org" - ) - - private val header: Request.Builder.() -> Unit = { header(*headers.toTypedArray()) } - - override suspend fun search(searchText: CharSequence, page: Int, list: List): List { - return getJsonApi( - "$baseUrl$mangaApiPath?sort=-titles&limit=20&text=$searchText${if (page != 1) "&skip=${page * 20}" else ""}", - header - ) - ?.items?.fastMap { - ItemModel( - title = it.title.orEmpty(), - description = "", - url = "$baseUrl$mangaApiPath/${it.slug}", - imageUrl = it.thumbnail.orEmpty(), - source = this - ) - } - .orEmpty() - } - - private fun chapterTitleBuilder(rootNode: MunityChapters): String { - val volume = rootNode.volume//volumegetNullable("volume")?.asInt - val chapter = rootNode.chapter?.toInt()//getNullable("chapter")?.asInt - val textTitle = rootNode.title//getNullable("title")?.asString - val chapterTitle = StringBuilder() - if (volume != null) chapterTitle.append("Vol. $volume") - if (chapter != null) { - if (volume != null) chapterTitle.append(" ") - chapterTitle.append("Chapter $chapter") - } - if (textTitle != null && textTitle != "") { - if (volume != null || chapter != null) chapterTitle.append(" ") - chapterTitle.append(textTitle) - } - return chapterTitle.toString() - } - - override val canScroll: Boolean get() = true - - private data class Munity(val items: List?, val total: Number?) - private data class MangaMunity(val title: String?, val slug: String?, val thumbnail: String?, val id: String?) - private data class MangaInfoMunity( - val status: String?, - val genres: List?, - val chapterCount: Number?, - val viewCount: Number?, - val rating: Number?, - val ratingCount: Number?, - val title: String?, - val summary: String?, - val authors: String?, - val artists: String?, - val slug: String?, - val updatedAt: String?, - val thumbnail: String?, - val lastReleasedAt: String?, - val category: String?, - val alias: String?, - val subCount: Number?, - val commentCount: Number?, - val chapters: List?, - val id: String? - ) - - private data class MunityChapters( - val viewCount: Number?, - val title: String?, - val volume: Int?, - val chapter: Number?, - val slug: String?, - val releasedAt: String?, - val id: String? - ) - - private data class MunityPage( - val images: List?, - val storage: String?, - val viewCount: Number?, - val title: String?, - val volume: Number?, - val chapter: Number?, - val manga: String?, - val slug: String?, - val releasedAt: String?, - val order: Number?, - val id: String? - ) -} \ No newline at end of file diff --git a/manga_sources/src/test/java/com/programmersbox/manga_sources/ExampleUnitTest.kt b/manga_sources/src/test/java/com/programmersbox/manga_sources/ExampleUnitTest.kt index 8961e1b00..398c65962 100644 --- a/manga_sources/src/test/java/com/programmersbox/manga_sources/ExampleUnitTest.kt +++ b/manga_sources/src/test/java/com/programmersbox/manga_sources/ExampleUnitTest.kt @@ -4,8 +4,6 @@ import android.content.Context import com.programmersbox.gsonutils.fromJson import com.programmersbox.gsonutils.getApi import com.programmersbox.gsonutils.getJsonApi -import com.programmersbox.manga_sources.manga.MangaFourLife -import com.programmersbox.manga_sources.manga.MangaPark import com.programmersbox.manga_sources.manga.MangaRead import com.programmersbox.manga_sources.manga.NineAnime import com.programmersbox.manga_sources.utilities.toJsoup diff --git a/mangaworld/build.gradle.kts b/mangaworld/build.gradle.kts index e67b837a8..bcf3255f8 100644 --- a/mangaworld/build.gradle.kts +++ b/mangaworld/build.gradle.kts @@ -40,9 +40,11 @@ dependencies { implementation(projects.uiViews) implementation(projects.models) implementation(projects.favoritesdatabase) - implementation(projects.mangaSources) implementation(projects.sharedutils) + implementation(projects.sourceUtilities) implementation(projects.imageloader) + implementation(libs.duktape) + implementation(libs.bundles.ziplineLibs) implementation(libs.glide) ksp(libs.glideCompiler) diff --git a/mangaworld/src/main/AndroidManifest.xml b/mangaworld/src/main/AndroidManifest.xml index 309b16e6e..7369d7073 100644 --- a/mangaworld/src/main/AndroidManifest.xml +++ b/mangaworld/src/main/AndroidManifest.xml @@ -1,13 +1,9 @@ - - - - { GenericManga(get()) } + single { GenericManga(get(), get()) } single { NetworkHelper(get()) } single { MainLogo(R.mipmap.ic_launcher) } single { NotificationLogo(R.drawable.manga_world_round_logo) } + single { ChapterHolder() } } -class ChapterList(private val context: Context, private val genericInfo: GenericInfo) { - fun set(item: List?) { - val i = item.toJson(ChapterModel::class.java to ChapterModelSerializer()) - context.defaultSharedPref.edit().putString("chapterList", i).commit() - } - - fun get(): List? = context.defaultSharedPref.getObject( - "chapterList", - null, - ChapterModel::class.java to ChapterModelDeserializer(genericInfo) - ) - - fun clear() { - context.defaultSharedPref.edit().remove("chapterList").apply() - } +class ChapterHolder { + var chapterModel: ChapterModel? = null + var chapters: List? = null } -class GenericManga(val context: Context) : GenericInfo { +class GenericManga( + val context: Context, + val chapterHolder: ChapterHolder +) : GenericInfo { + + override val sourceType: String get() = "manga" override val deepLinkUri: String get() = "mangaworld://" @@ -141,9 +128,10 @@ class GenericManga(val context: Context) : GenericInfo { activity: FragmentActivity, navController: NavController ) { - ChapterList(context, this@GenericManga).set(allChapters) + chapterHolder.chapters = allChapters if (runBlocking { context.useNewReaderFlow.first() }) { - ReadViewModel.navigateToMangaReader(navController, model, infoModel.title, model.url, model.sourceUrl) + chapterHolder.chapterModel = model + ReadViewModel.navigateToMangaReader(navController, infoModel.title, model.url, model.sourceUrl) } else { context.startActivity( Intent(context, ReadActivity::class.java).apply { @@ -208,23 +196,10 @@ class GenericManga(val context: Context) : GenericInfo { ) { p -> if (p.isGranted) downloadFullChapter(model, infoModel.title.ifBlank { infoModel.url }) } } - override fun sourceList(): List = - if (runBlocking { context.showAdultFlow.first() }) { - Sources.values().toList() - } else { - Sources.values().filterNot(Sources::isAdult).toList() - } - .filterNot(ApiService::notWorking) + override fun sourceList(): List = emptyList() - override fun toSource(s: String): ApiService? = try { - Sources.valueOf(s) - } catch (e: IllegalArgumentException) { - null - } + override fun toSource(s: String): ApiService? = null - @OptIn( - ExperimentalMaterialApi::class - ) @Composable override fun ComposeShimmerItem() { LazyVerticalGrid( @@ -238,7 +213,6 @@ class GenericManga(val context: Context) : GenericInfo { } @OptIn( - ExperimentalMaterialApi::class, ExperimentalFoundationApi::class ) @Composable @@ -303,22 +277,6 @@ class GenericManga(val context: Context) : GenericInfo { } generalSettings { - val scope = rememberCoroutineScope() - val context = LocalContext.current - val showAdult by context.showAdultFlow.collectAsState(false) - - SwitchSetting( - settingTitle = { Text(stringResource(R.string.showAdultSources)) }, - value = showAdult, - settingIcon = { Icon(Icons.Default.TextFormat, null, modifier = Modifier.fillMaxSize()) }, - updateValue = { - scope.launch { context.updatePref(SHOW_ADULT, it) } - if (!it && (sourceFlow.value as? Sources)?.isAdult == true) { - sourceFlow.tryEmit(sourceList().random()) - } - } - ) - /*if (BuildConfig.DEBUG) { val folderLocation by context.folderLocationFlow.collectAsState(initial = DOWNLOAD_FILE_PATH) @@ -455,8 +413,6 @@ class GenericManga(val context: Context) : GenericInfo { @OptIn( ExperimentalMaterial3Api::class, - ExperimentalMaterialApi::class, - ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class, ExperimentalAnimationApi::class, ExperimentalFoundationApi::class @@ -465,8 +421,6 @@ class GenericManga(val context: Context) : GenericInfo { composable( ReadViewModel.MangaReaderRoute, arguments = listOf( - navArgument("currentChapter") { nullable = true }, - navArgument("allChapters") { nullable = true }, navArgument("mangaTitle") { nullable = true }, navArgument("mangaUrl") { nullable = true }, navArgument("mangaInfoUrl") { nullable = true }, diff --git a/mangaworld/src/main/java/com/programmersbox/mangaworld/MainActivity.kt b/mangaworld/src/main/java/com/programmersbox/mangaworld/MainActivity.kt index 02be5a9c3..2ae7cada5 100644 --- a/mangaworld/src/main/java/com/programmersbox/mangaworld/MainActivity.kt +++ b/mangaworld/src/main/java/com/programmersbox/mangaworld/MainActivity.kt @@ -2,34 +2,18 @@ package com.programmersbox.mangaworld import android.os.Build import android.view.WindowManager -import androidx.lifecycle.lifecycleScope import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.glide.GlideImageLoader import com.google.android.gms.ads.MobileAds -import com.programmersbox.manga_sources.Sources -import com.programmersbox.models.sourceFlow import com.programmersbox.uiviews.BaseMainActivity -import com.programmersbox.uiviews.utils.currentService -import kotlinx.coroutines.launch class MainActivity : BaseMainActivity() { - override fun onCreate() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES } BigImageViewer.initialize(GlideImageLoader.with(applicationContext)) - - lifecycleScope.launch { - if (currentService == null) { - sourceFlow.emit(Sources.MANGA_READ) - currentService = Sources.MANGA_READ.serviceName - } - } - MobileAds.initialize(this) } - } \ No newline at end of file diff --git a/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReadActivity.kt b/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReadActivity.kt index e4a38014b..9cbe7c714 100644 --- a/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReadActivity.kt +++ b/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReadActivity.kt @@ -95,7 +95,7 @@ class ReadActivity : AppCompatActivity() { private val adapter2: PageAdapter by lazy { loader.let { val list = intent.getStringExtra("allChapters") - ?.fromJson>(ChapterModel::class.java to ChapterModelDeserializer(genericInfo)) + ?.fromJson>(ChapterModel::class.java to ChapterModelDeserializer()) .orEmpty().also(::println) //intent.getObjectExtra>("allChapters") ?: emptyList() val url = intent.getStringExtra("mangaUrl") ?: "" @@ -174,7 +174,7 @@ class ReadActivity : AppCompatActivity() { mangaTitle = intent.getStringExtra("mangaTitle") model = intent.getStringExtra("currentChapter") - ?.fromJson(ChapterModel::class.java to ChapterModelDeserializer(genericInfo)) + ?.fromJson(ChapterModel::class.java to ChapterModelDeserializer()) isDownloaded = intent.getBooleanExtra("downloaded", false) val file = intent.getSerializableExtra("filePath") as? File diff --git a/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReadViewModel.kt b/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReadViewModel.kt index 00d73d69a..4a0117c6c 100644 --- a/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReadViewModel.kt +++ b/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReadViewModel.kt @@ -17,16 +17,12 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import com.programmersbox.favoritesdatabase.ChapterWatched import com.programmersbox.favoritesdatabase.ItemDatabase -import com.programmersbox.gsonutils.fromJson -import com.programmersbox.gsonutils.toJson -import com.programmersbox.mangaworld.ChapterList +import com.programmersbox.mangaworld.ChapterHolder import com.programmersbox.models.ChapterModel import com.programmersbox.models.Storage import com.programmersbox.sharedutils.FirebaseDb import com.programmersbox.uiviews.GenericInfo import com.programmersbox.uiviews.utils.BatteryInformation -import com.programmersbox.uiviews.utils.ChapterModelDeserializer -import com.programmersbox.uiviews.utils.ChapterModelSerializer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch @@ -40,16 +36,16 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent import java.io.File class ReadViewModel( handle: SavedStateHandle, context: Context, val genericInfo: GenericInfo, + private val chapterHolder: ChapterHolder, val headers: MutableMap = mutableMapOf(), - model: Flow>? = handle - .get("currentChapter") - ?.fromJson(ChapterModel::class.java to ChapterModelDeserializer(genericInfo)) + model: Flow>? = chapterHolder.chapterModel ?.getChapterInfo() ?.map { headers.putAll(it.flatMap { h -> h.headers.toList() }) @@ -61,16 +57,6 @@ class ReadViewModel( val isDownloaded: Boolean = handle.get("downloaded")?.toBoolean() ?: false, filePath: File? = handle.get("filePath")?.let { File(it) }, modelPath: Flow>? = if (isDownloaded && filePath != null) { - /*Single.create> { - filePath - .listFiles() - ?.sortedBy { f -> f.name.split(".").first().toInt() } - ?.fastMap(File::toUri) - ?.fastMap(Uri::toString) - ?.let(it::onSuccess) ?: it.onError(Throwable("Cannot find files")) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread())*/ flow { filePath .listFiles() @@ -84,25 +70,22 @@ class ReadViewModel( } else { model }, -) : ViewModel() { +) : ViewModel(), KoinComponent { companion object { const val MangaReaderRoute = - "mangareader?currentChapter={currentChapter}&mangaTitle={mangaTitle}&mangaUrl={mangaUrl}&mangaInfoUrl={mangaInfoUrl}&downloaded={downloaded}&filePath={filePath}" + "mangareader?mangaTitle={mangaTitle}&mangaUrl={mangaUrl}&mangaInfoUrl={mangaInfoUrl}&downloaded={downloaded}&filePath={filePath}" fun navigateToMangaReader( navController: NavController, - currentChapter: ChapterModel? = null, mangaTitle: String? = null, mangaUrl: String? = null, mangaInfoUrl: String? = null, downloaded: Boolean = false, filePath: String? = null ) { - val current = Uri.encode(currentChapter?.toJson(ChapterModel::class.java to ChapterModelSerializer())) - navController.navigate( - "mangareader?currentChapter=$current&mangaTitle=${mangaTitle}&mangaUrl=${mangaUrl}&mangaInfoUrl=${mangaInfoUrl}&downloaded=$downloaded&filePath=$filePath" + "mangareader?mangaTitle=${mangaTitle}&mangaUrl=${mangaUrl}&mangaInfoUrl=${mangaInfoUrl}&downloaded=$downloaded&filePath=$filePath" ) { launchSingleTop = true } } } @@ -111,7 +94,6 @@ class ReadViewModel( private val dao by lazy { ItemDatabase.getInstance(context).itemDao() } - private val chapterList by lazy { ChapterList(context, genericInfo) } var list by mutableStateOf>(emptyList()) private val mangaUrl by lazy { handle.get("mangaInfoUrl") ?: "" } @@ -138,7 +120,7 @@ class ReadViewModel( } val url = handle.get("mangaUrl") ?: "" - list = chapterList.get().orEmpty() + list = chapterHolder.chapters.orEmpty() currentChapter = list.indexOfFirst { l -> l.url == url } loadPages(modelPath) @@ -190,6 +172,7 @@ class ReadViewModel( override fun onCleared() { super.onCleared() - chapterList.clear() + chapterHolder.chapterModel = null + chapterHolder.chapters = null } } \ No newline at end of file diff --git a/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReaderCompose.kt b/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReaderCompose.kt index cf4ce2ab6..f231eb96d 100644 --- a/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReaderCompose.kt +++ b/mangaworld/src/main/java/com/programmersbox/mangaworld/reader/ReaderCompose.kt @@ -1,4 +1,5 @@ @file:Suppress("INLINE_FROM_HIGHER_PLATFORM") +@file:OptIn(ExperimentalMaterialApi::class) package com.programmersbox.mangaworld.reader @@ -87,9 +88,9 @@ import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DismissDirection import androidx.compose.material3.DismissValue -import androidx.compose.material3.Divider import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeTopAppBar @@ -154,6 +155,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.bumptech.glide.load.model.GlideUrl import com.programmersbox.helpfulutils.battery import com.programmersbox.helpfulutils.timeTick +import com.programmersbox.mangaworld.ChapterHolder import com.programmersbox.mangaworld.LIST_OR_PAGER import com.programmersbox.mangaworld.PAGE_PADDING import com.programmersbox.mangaworld.R @@ -188,9 +190,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.zoomable +import org.koin.compose.koinInject @ExperimentalMaterial3Api -@ExperimentalMaterialApi @ExperimentalComposeUiApi @ExperimentalAnimationApi @ExperimentalFoundationApi @@ -198,11 +200,13 @@ import net.engawapg.lib.zoomable.zoomable fun ReadView( context: Context = LocalContext.current, genericInfo: GenericInfo = LocalGenericInfo.current, + ch: ChapterHolder = koinInject(), readVm: ReadViewModel = viewModel { ReadViewModel( handle = createSavedStateHandle(), context = context, - genericInfo = genericInfo + genericInfo = genericInfo, + chapterHolder = ch ) } ) { @@ -261,7 +265,7 @@ fun ReadView( initialValue = runBlocking { settingsHandling.batteryPercentage.firstOrNull() ?: 20 }, range = 1f..100f ) - Divider() + HorizontalDivider() val activity = LocalActivity.current SliderSetting( scope = scope, @@ -272,7 +276,7 @@ fun ReadView( initialValue = runBlocking { context.dataStore.data.first()[PAGE_PADDING] ?: 4 }, range = 0f..10f ) - Divider() + HorizontalDivider() SwitchSetting( settingTitle = { Text(stringResource(R.string.list_or_pager_title)) }, summaryValue = { Text(stringResource(R.string.list_or_pager_description)) }, @@ -469,7 +473,7 @@ fun DrawerView( shape = RoundedCornerShape(8.0.dp)//MaterialTheme.shapes.medium ) - if (i < readVm.list.lastIndex) Divider() + if (i < readVm.list.lastIndex) HorizontalDivider() } } } @@ -910,7 +914,7 @@ private fun ZoomableImage( contentScale: ContentScale = ContentScale.Fit, onClick: () -> Unit = {} ) { - BoxWithConstraints( + Box( modifier = modifier .fillMaxWidth() .clip(RectangleShape) diff --git a/novel_sources/bestlightnovel/.gitignore b/novel_sources/bestlightnovel/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/novel_sources/bestlightnovel/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/novel_sources/bestlightnovel/build.gradle.kts b/novel_sources/bestlightnovel/build.gradle.kts new file mode 100644 index 000000000..06d705efb --- /dev/null +++ b/novel_sources/bestlightnovel/build.gradle.kts @@ -0,0 +1,42 @@ +import plugins.SourceType + +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + id("otaku-source-application") +} + +android { + namespace = "com.programmersbox.bestlightnovel" + + defaultConfig { + namespace = "com.programmersbox.bestlightnovel" + } +} + +otakuSourceInformation { + name = "BestLightNovel" + classInfo = ".BestLightNovel" + sourceType = SourceType.Novel +} + +dependencies { + testImplementation(TestDeps.junit) + androidTestImplementation(TestDeps.androidJunit) + androidTestImplementation(TestDeps.androidEspresso) + implementation(libs.bundles.okHttpLibs) + + implementation(libs.coroutinesCore) + + implementation(Deps.gsonutils) + implementation(Deps.helpfulutils) + debugImplementation(Deps.loggingutils) + implementation(libs.gson) + + implementation(libs.jsoup) + + implementation(projects.models) + api(projects.sourceUtilities) + implementation(libs.bundles.ktorLibs) + + implementation(libs.bundles.koinLibs) +} \ No newline at end of file diff --git a/novel_sources/bestlightnovel/consumer-rules.pro b/novel_sources/bestlightnovel/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/novel_sources/bestlightnovel/proguard-rules.pro b/novel_sources/bestlightnovel/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/novel_sources/bestlightnovel/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/novel_sources/bestlightnovel/src/main/AndroidManifest.xml b/novel_sources/bestlightnovel/src/main/AndroidManifest.xml new file mode 100644 index 000000000..418151c5b --- /dev/null +++ b/novel_sources/bestlightnovel/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/novel_sources/bestlightnovel/src/main/java/com/programmersbox/bestlightnovel/BestLightNovel.kt b/novel_sources/bestlightnovel/src/main/java/com/programmersbox/bestlightnovel/BestLightNovel.kt new file mode 100644 index 000000000..a3a70fe9d --- /dev/null +++ b/novel_sources/bestlightnovel/src/main/java/com/programmersbox/bestlightnovel/BestLightNovel.kt @@ -0,0 +1,100 @@ +package com.programmersbox.bestlightnovel + +import com.programmersbox.models.ApiService +import com.programmersbox.models.ChapterModel +import com.programmersbox.models.InfoModel +import com.programmersbox.models.ItemModel +import com.programmersbox.models.Storage +import org.jsoup.Jsoup + +object BestLightNovel : ApiService { + override val baseUrl: String get() = "https://bestlightnovel.com" + + override val canDownload: Boolean get() = false + override val canScroll: Boolean get() = true + + override val serviceName: String get() = "BEST_LIGHT_NOVEL" + + override suspend fun recent(page: Int): List { + return Jsoup.connect("$baseUrl/novel_list?type=topview&category=all&state=all&page=$page") + .followRedirects(true) + .get() + .select("div.update_item.list_category") + .map { + ItemModel( + title = it.select("h3 > a").text(), + description = "", + url = it.select("h3 > a").attr("abs:href"), + imageUrl = it.select("img").attr("abs:src"), + source = this + ) + } + } + + override suspend fun allList(page: Int): List { + return super.allList(page) + } + + override suspend fun itemInfo(model: ItemModel): InfoModel { + val doc = model.url.toJsoup() + + return InfoModel( + source = this, + url = model.url, + title = doc.select(".truyen_info_right h1").text().trim(), + description = doc.select("div#noidungm").text(), + imageUrl = doc.select(".info_image img").attr("abs:src"), + genres = emptyList(), + chapters = doc + .select("div.chapter-list div.row") + .map { + ChapterModel( + name = it.select("a").text(), + url = it.select("a").attr("abs:href"), + uploaded = it.select("span:nth-child(2)").text(), + sourceUrl = model.url, + source = this + ) + }, + alternativeNames = emptyList() + ) + } + + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + val doc = chapterModel.url.toJsoup() + return listOf( + Storage( + link = doc.select("div#vung_doc").html() + ) + ) + } + + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + val doc = Jsoup.connect("$baseUrl/search_novels/$searchText").get() + .select("div.update_item.list_category") + .map { + ItemModel( + title = it.select("h3 > a").text(), + description = "", + url = it.select("h3 > a").attr("abs:href"), + imageUrl = it.select("img").attr("abs:src"), + source = this + ) + } + return doc + } + + override suspend fun sourceByUrl(url: String): ItemModel { + val doc = url.toJsoup() + + return ItemModel( + source = this, + url = url, + title = doc.select(".truyen_info_right h1").text().trim(), + description = doc.select("div#noidungm").text(), + imageUrl = doc.select(".info_image img").attr("abs:src") + ) + } +} + +internal fun String.toJsoup() = Jsoup.connect(this).get() \ No newline at end of file diff --git a/novel_sources/bestlightnovel/src/main/res/drawable/ic_launcher_background.xml b/novel_sources/bestlightnovel/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/novel_sources/bestlightnovel/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/novel_sources/bestlightnovel/src/main/res/drawable/ic_launcher_foreground.xml b/novel_sources/bestlightnovel/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/novel_sources/bestlightnovel/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/novel_sources/bestlightnovel/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/novel_sources/bestlightnovel/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/novel_sources/bestlightnovel/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/novel_sources/bestlightnovel/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-hdpi/ic_launcher.webp b/novel_sources/bestlightnovel/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/novel_sources/bestlightnovel/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/novel_sources/bestlightnovel/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/novel_sources/bestlightnovel/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-mdpi/ic_launcher.webp b/novel_sources/bestlightnovel/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/novel_sources/bestlightnovel/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/novel_sources/bestlightnovel/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/novel_sources/bestlightnovel/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-xhdpi/ic_launcher.webp b/novel_sources/bestlightnovel/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/novel_sources/bestlightnovel/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/novel_sources/bestlightnovel/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/novel_sources/bestlightnovel/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/novel_sources/bestlightnovel/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/novel_sources/bestlightnovel/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/novel_sources/bestlightnovel/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/novel_sources/bestlightnovel/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/novel_sources/bestlightnovel/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/novel_sources/bestlightnovel/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/novel_sources/bestlightnovel/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/novel_sources/bestlightnovel/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/novel_sources/bestlightnovel/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/novel_sources/bestlightnovel/src/main/res/values/colors.xml b/novel_sources/bestlightnovel/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/novel_sources/bestlightnovel/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/novel_sources/bestlightnovel/src/main/res/values/strings.xml b/novel_sources/bestlightnovel/src/main/res/values/strings.xml new file mode 100644 index 000000000..a88bfda37 --- /dev/null +++ b/novel_sources/bestlightnovel/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + BestLightNovel + \ No newline at end of file diff --git a/novel_sources/novelupdates/.gitignore b/novel_sources/novelupdates/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/novel_sources/novelupdates/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/novel_sources/novelupdates/build.gradle.kts b/novel_sources/novelupdates/build.gradle.kts new file mode 100644 index 000000000..1a3414069 --- /dev/null +++ b/novel_sources/novelupdates/build.gradle.kts @@ -0,0 +1,41 @@ +import plugins.SourceType + +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + id("otaku-source-application") +} + +android { + namespace = "com.programmersbox.novelupdates" + + defaultConfig { + applicationId = "com.programmersbox.novelupdates" + } +} + +otakuSourceInformation { + name = "NovelUpdates" + classInfo = ".NovelUpdates" + sourceType = SourceType.Novel +} + +dependencies { + testImplementation(TestDeps.junit) + androidTestImplementation(TestDeps.androidJunit) + androidTestImplementation(TestDeps.androidEspresso) + implementation(libs.bundles.okHttpLibs) + + implementation(libs.coroutinesCore) + + implementation(Deps.gsonutils) + implementation(Deps.helpfulutils) + debugImplementation(Deps.loggingutils) + implementation(libs.gson) + + implementation(libs.jsoup) + + implementation(libs.bundles.ktorLibs) + + implementation(libs.bundles.koinLibs) + implementation(projects.models) +} \ No newline at end of file diff --git a/novel_sources/novelupdates/proguard-rules.pro b/novel_sources/novelupdates/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/novel_sources/novelupdates/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/novel_sources/novelupdates/src/main/AndroidManifest.xml b/novel_sources/novelupdates/src/main/AndroidManifest.xml new file mode 100644 index 000000000..418151c5b --- /dev/null +++ b/novel_sources/novelupdates/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/novel_sources/novelupdates/src/main/java/com/programmersbox/novelupdates/NovelUpdates.kt b/novel_sources/novelupdates/src/main/java/com/programmersbox/novelupdates/NovelUpdates.kt new file mode 100644 index 000000000..3bf64c197 --- /dev/null +++ b/novel_sources/novelupdates/src/main/java/com/programmersbox/novelupdates/NovelUpdates.kt @@ -0,0 +1,123 @@ +package com.programmersbox.novelupdates + +import com.programmersbox.models.ApiService +import com.programmersbox.models.ChapterModel +import com.programmersbox.models.InfoModel +import com.programmersbox.models.ItemModel +import com.programmersbox.models.Storage +import com.programmersbox.models.createHttpClient +import io.ktor.client.call.body +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.get +import io.ktor.http.Parameters +import org.jsoup.nodes.Document + +object NovelUpdates : ApiService { + override val canDownload: Boolean get() = false + override val baseUrl: String get() = "https://www.novelupdates.com" + override val canScroll: Boolean get() = true + override val serviceName: String get() = "NOVEL_UPDATES" + private val client = createHttpClient() + + override suspend fun recent(page: Int): List { + val f = client.get("$baseUrl/series-ranking/?rank=week&pg=$page") + val doc = f.body() + return doc + .select("div.search_main_box_nu") + .map { + ItemModel( + title = it.select(".search_title > a").text(), + description = "", + url = it.select(".search_title > a").attr("abs:href"), + imageUrl = it.select("img").attr("abs:src"), + source = this + ) + } + } + + override suspend fun allList(page: Int): List { + val f = client.get("$baseUrl/series-ranking/?rank=popular&pg=$page") + val doc = f.body() + return doc + .select("div.search_main_box_nu") + .map { + ItemModel( + title = it.select(".search_title > a").text(), + description = "", + url = it.select(".search_title > a").attr("abs:href"), + imageUrl = it.select("img").attr("abs:src"), + source = this + ) + } + } + + override suspend fun search(searchText: CharSequence, page: Int, list: List): List { + val f = client.get("$baseUrl/page/$page/?s=$searchText&post_type=seriesplans") + val doc = f.body() + return doc + .select("div.search_main_box_nu") + .map { + ItemModel( + title = it.select(".search_title > a").text(), + description = "", + url = it.select(".search_title > a").attr("abs:href"), + imageUrl = it.select("img").attr("abs:src"), + source = this + ) + } + } + + override suspend fun sourceByUrl(url: String): ItemModel { + val f = client.get(url) + val doc = f.body() + return ItemModel( + title = doc.select(".seriestitlenu").text(), + description = doc.select("#editdescription p").text(), + url = url, + imageUrl = doc.select(".seriesimg > img").attr("abs:src"), + source = this + ) + } + + override suspend fun itemInfo(model: ItemModel): InfoModel { + val f = client.get(model.url) + val doc = f.body() + val bookId = doc.select("#mypostid").attr("value") + val chapters = client.submitForm( + url = "$baseUrl/wp-admin/admin-ajax.php", + formParameters = Parameters.build { + append("action", "nd_getchapters") + append("mygrr", "0") + append("mypostid", bookId) + } + ) + .body() + .select("li.sp_li_chp") + .map { + ChapterModel( + name = it.select("li").text(), + url = it.select("a:nth-child(2)").attr("abs:href"), + source = this, + sourceUrl = model.url, + uploaded = "" + ) + } + return InfoModel( + title = doc.select(".seriestitlenu").text(), + description = doc.select("#editdescription p").text(), + url = model.url, + imageUrl = doc.select(".seriesimg > img").attr("abs:src"), + chapters = chapters, + genres = doc.select("#seriesgenre a").eachText(), + alternativeNames = emptyList(), + source = this + ) + } + + override suspend fun chapterInfo(chapterModel: ChapterModel): List { + val f = client.get(chapterModel.url) + val doc = f.body() + val d = doc.select("p").html() + return listOf(Storage(link = d.replace("", "

"))) + } +} \ No newline at end of file diff --git a/novel_sources/novelupdates/src/main/res/drawable/ic_launcher_background.xml b/novel_sources/novelupdates/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/novel_sources/novelupdates/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/novel_sources/novelupdates/src/main/res/drawable/ic_launcher_foreground.xml b/novel_sources/novelupdates/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/novel_sources/novelupdates/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/novel_sources/novelupdates/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/novel_sources/novelupdates/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/novel_sources/novelupdates/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/novel_sources/novelupdates/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/novel_sources/novelupdates/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/novel_sources/novelupdates/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/novel_sources/novelupdates/src/main/res/mipmap-hdpi/ic_launcher.webp b/novel_sources/novelupdates/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/novel_sources/novelupdates/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/novel_sources/novelupdates/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/novel_sources/novelupdates/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/novel_sources/novelupdates/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/novel_sources/novelupdates/src/main/res/mipmap-mdpi/ic_launcher.webp b/novel_sources/novelupdates/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/novel_sources/novelupdates/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/novel_sources/novelupdates/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/novel_sources/novelupdates/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/novel_sources/novelupdates/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/novel_sources/novelupdates/src/main/res/mipmap-xhdpi/ic_launcher.webp b/novel_sources/novelupdates/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/novel_sources/novelupdates/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/novel_sources/novelupdates/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/novel_sources/novelupdates/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/novel_sources/novelupdates/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/novel_sources/novelupdates/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/novel_sources/novelupdates/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/novel_sources/novelupdates/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/novel_sources/novelupdates/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/novel_sources/novelupdates/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/novel_sources/novelupdates/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/novel_sources/novelupdates/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/novel_sources/novelupdates/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/novel_sources/novelupdates/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/novel_sources/novelupdates/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/novel_sources/novelupdates/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/novel_sources/novelupdates/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/novel_sources/novelupdates/src/main/res/values/colors.xml b/novel_sources/novelupdates/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/novel_sources/novelupdates/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/novel_sources/novelupdates/src/main/res/values/strings.xml b/novel_sources/novelupdates/src/main/res/values/strings.xml new file mode 100644 index 000000000..10fafae3c --- /dev/null +++ b/novel_sources/novelupdates/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + NovelUpdates + \ No newline at end of file diff --git a/novelworld/build.gradle.kts b/novelworld/build.gradle.kts index 8b54ae497..251ee59f1 100644 --- a/novelworld/build.gradle.kts +++ b/novelworld/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { implementation(projects.uiViews) implementation(projects.models) implementation(projects.favoritesdatabase) - implementation(projects.novelSources) implementation(projects.sharedutils) implementation(libs.bundles.roomLibs) diff --git a/novelworld/src/main/AndroidManifest.xml b/novelworld/src/main/AndroidManifest.xml index a42f28509..e2fb5bba7 100644 --- a/novelworld/src/main/AndroidManifest.xml +++ b/novelworld/src/main/AndroidManifest.xml @@ -1,11 +1,6 @@ - - - - - + + + + diff --git a/novelworld/src/main/java/com/programmersbox/novelworld/GenericNovel.kt b/novelworld/src/main/java/com/programmersbox/novelworld/GenericNovel.kt index 19a5c9033..c3bb6366b 100644 --- a/novelworld/src/main/java/com/programmersbox/novelworld/GenericNovel.kt +++ b/novelworld/src/main/java/com/programmersbox/novelworld/GenericNovel.kt @@ -19,7 +19,12 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.FavoriteBorder -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.compositeOver @@ -31,7 +36,9 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.navArgument +import com.google.accompanist.placeholder.PlaceholderHighlight import com.google.accompanist.placeholder.material.placeholder +import com.google.accompanist.placeholder.shimmer import com.programmersbox.favoritesdatabase.DbModel import com.programmersbox.gsonutils.getObject import com.programmersbox.gsonutils.toJson @@ -40,11 +47,14 @@ import com.programmersbox.models.ApiService import com.programmersbox.models.ChapterModel import com.programmersbox.models.InfoModel import com.programmersbox.models.ItemModel -import com.programmersbox.novel_sources.Sources import com.programmersbox.sharedutils.AppUpdate import com.programmersbox.sharedutils.MainLogo import com.programmersbox.uiviews.GenericInfo -import com.programmersbox.uiviews.utils.* +import com.programmersbox.uiviews.utils.ChapterModelDeserializer +import com.programmersbox.uiviews.utils.ChapterModelSerializer +import com.programmersbox.uiviews.utils.ComponentState +import com.programmersbox.uiviews.utils.NotificationLogo +import com.programmersbox.uiviews.utils.combineClickableWithIndication import org.koin.dsl.module val appModule = module { @@ -62,7 +72,7 @@ class ChapterList(private val context: Context, private val genericInfo: Generic fun get(): List? = context.defaultSharedPref.getObject( "chapterList", null, - ChapterModel::class.java to ChapterModelDeserializer(genericInfo) + ChapterModel::class.java to ChapterModelDeserializer() ) } @@ -70,6 +80,8 @@ class GenericNovel(val context: Context) : GenericInfo { override val deepLinkUri: String get() = "novelworld://" + override val sourceType: String get() = "novel" + override fun chapterOnClick( model: ChapterModel, allChapters: List, @@ -88,13 +100,9 @@ class GenericNovel(val context: Context) : GenericInfo { ) } - override fun sourceList(): List = Sources.values().toList() + override fun sourceList(): List = emptyList() - override fun toSource(s: String): ApiService? = try { - Sources.valueOf(s) - } catch (e: IllegalArgumentException) { - null - } + override fun toSource(s: String): ApiService? = null override fun downloadChapter( model: ChapterModel, @@ -111,6 +119,10 @@ class GenericNovel(val context: Context) : GenericInfo { @OptIn(ExperimentalMaterialApi::class) @Composable override fun ComposeShimmerItem() { + val placeholderColor = contentColorFor(backgroundColor = androidx.compose.material3.MaterialTheme.colorScheme.surface) + .copy(0.1f) + .compositeOver(androidx.compose.material3.MaterialTheme.colorScheme.surface) + LazyColumn { items(10) { Surface( @@ -126,9 +138,8 @@ class GenericNovel(val context: Context) : GenericInfo { .fillMaxWidth() .placeholder( true, - color = contentColorFor(backgroundColor = androidx.compose.material3.MaterialTheme.colorScheme.surface) - .copy(0.1f) - .compositeOver(androidx.compose.material3.MaterialTheme.colorScheme.surface) + color = placeholderColor, + highlight = PlaceholderHighlight.shimmer(androidx.compose.material3.MaterialTheme.colorScheme.surface.copy(alpha = .75f)) ) .padding(4.dp) ) @@ -140,7 +151,8 @@ class GenericNovel(val context: Context) : GenericInfo { @OptIn( ExperimentalMaterialApi::class, ExperimentalAnimationApi::class, - ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class + ExperimentalFoundationApi::class, + ExperimentalMaterial3Api::class ) @Composable override fun ItemListView( diff --git a/novelworld/src/main/java/com/programmersbox/novelworld/MainActivity.kt b/novelworld/src/main/java/com/programmersbox/novelworld/MainActivity.kt index 7481ba06c..80bb62248 100644 --- a/novelworld/src/main/java/com/programmersbox/novelworld/MainActivity.kt +++ b/novelworld/src/main/java/com/programmersbox/novelworld/MainActivity.kt @@ -1,21 +1,10 @@ package com.programmersbox.novelworld -import androidx.lifecycle.lifecycleScope -import com.programmersbox.models.sourceFlow -import com.programmersbox.novel_sources.Sources import com.programmersbox.uiviews.BaseMainActivity -import com.programmersbox.uiviews.utils.currentService -import kotlinx.coroutines.launch class MainActivity : BaseMainActivity() { + override fun onCreate() { - lifecycleScope.launch { - if (currentService == null) { - val s = Sources.values().random() - sourceFlow.emit(s) - currentService = s.serviceName - } - } } } \ No newline at end of file diff --git a/novelworld/src/main/java/com/programmersbox/novelworld/ReadingActivity.kt b/novelworld/src/main/java/com/programmersbox/novelworld/ReadingActivity.kt index 23f062a06..0eb2194c1 100644 --- a/novelworld/src/main/java/com/programmersbox/novelworld/ReadingActivity.kt +++ b/novelworld/src/main/java/com/programmersbox/novelworld/ReadingActivity.kt @@ -6,14 +6,34 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.annotation.StringRes -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.* +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowRight @@ -22,17 +42,36 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.* import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.runtime.* +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberDrawerState +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -68,9 +107,27 @@ import com.programmersbox.models.Storage import com.programmersbox.sharedutils.FirebaseDb import com.programmersbox.uiviews.BaseMainActivity import com.programmersbox.uiviews.GenericInfo -import com.programmersbox.uiviews.utils.* -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import com.programmersbox.uiviews.utils.BatteryInformation +import com.programmersbox.uiviews.utils.ChapterModelDeserializer +import com.programmersbox.uiviews.utils.ChapterModelSerializer +import com.programmersbox.uiviews.utils.InsetSmallTopAppBar +import com.programmersbox.uiviews.utils.LifecycleHandle +import com.programmersbox.uiviews.utils.LocalActivity +import com.programmersbox.uiviews.utils.LocalGenericInfo +import com.programmersbox.uiviews.utils.LocalNavController +import com.programmersbox.uiviews.utils.LocalSettingsHandling +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import androidx.compose.material3.MaterialTheme as M3MaterialTheme class ReadViewModel( @@ -78,7 +135,7 @@ class ReadViewModel( handle: SavedStateHandle, genericInfo: GenericInfo, model: Flow>? = handle.get("currentChapter") - ?.fromJson(ChapterModel::class.java to ChapterModelDeserializer(genericInfo)) + ?.fromJson(ChapterModel::class.java to ChapterModelDeserializer()) ?.getChapterInfo() ?.map { it.mapNotNull(Storage::link) } ) : ViewModel() { diff --git a/settings.gradle.kts b/settings.gradle.kts index e0c73b20e..77675b516 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,3 +19,10 @@ include( ) rootProject.name = "OtakuWorld" +include( + ":novel_sources:novelupdates", + ":novel_sources:bestlightnovel" +) +include(":sharedutils:extensionloader") +include(":manga_sources:defaultmangasources") +include(":anime_sources:defaultanimesources") diff --git a/sharedutils/extensionloader/.gitignore b/sharedutils/extensionloader/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/sharedutils/extensionloader/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sharedutils/extensionloader/build.gradle.kts b/sharedutils/extensionloader/build.gradle.kts new file mode 100644 index 000000000..c07ab6518 --- /dev/null +++ b/sharedutils/extensionloader/build.gradle.kts @@ -0,0 +1,14 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + id("otaku-library") +} + +android { + namespace = "com.programmersbox.extensionloader" +} + +dependencies { + implementation(projects.models) + //Custom Libraries + implementation(Deps.jakepurple13Libs) +} \ No newline at end of file diff --git a/sharedutils/extensionloader/consumer-rules.pro b/sharedutils/extensionloader/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/sharedutils/extensionloader/proguard-rules.pro b/sharedutils/extensionloader/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/sharedutils/extensionloader/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sharedutils/extensionloader/src/androidTest/java/com/programmersbox/extensionloader/ExampleInstrumentedTest.kt b/sharedutils/extensionloader/src/androidTest/java/com/programmersbox/extensionloader/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..cbca6a779 --- /dev/null +++ b/sharedutils/extensionloader/src/androidTest/java/com/programmersbox/extensionloader/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.programmersbox.extensionloader + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.programmersbox.extensionloader.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/sharedutils/extensionloader/src/main/AndroidManifest.xml b/sharedutils/extensionloader/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a636c3872 --- /dev/null +++ b/sharedutils/extensionloader/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/sharedutils/extensionloader/src/main/java/com/programmersbox/extensionloader/ExtensionLoader.kt b/sharedutils/extensionloader/src/main/java/com/programmersbox/extensionloader/ExtensionLoader.kt new file mode 100644 index 000000000..251c8f46f --- /dev/null +++ b/sharedutils/extensionloader/src/main/java/com/programmersbox/extensionloader/ExtensionLoader.kt @@ -0,0 +1,118 @@ +package com.programmersbox.extensionloader + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import dalvik.system.PathClassLoader +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking + +private val PACKAGE_FLAGS = + PackageManager.GET_CONFIGURATIONS or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + PackageManager.GET_SIGNING_CERTIFICATES + } else { + @Suppress("DEPRECATION") + PackageManager.GET_SIGNATURES + } + +/** + * Use this to load code from other apks! + * + * Make sure you are creating those apks and they are installed as applications. + * + * @param extensionFeature this should match what you set in the extension module at the manifest level: + * ```xml + * + * ``` + * + * @param metadataClass this should match what you set in the extension module at the application level: + * ```xml + * + * + * + * ``` + * This should match the class name of what you want to access and load. + * If you want to include other metadata types, add more metadata attributes! + */ +class ExtensionLoader( + private val context: Context, + private val extensionFeature: String, + private val metadataClass: String, + private val mapping: (T, ApplicationInfo, PackageInfo) -> R +) { + @SuppressLint("QueryPermissionsNeeded") + fun loadExtensions(mapped: (T, ApplicationInfo, PackageInfo) -> R = mapping): List { + val packageManager = context.packageManager + val packages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())) + } else { + packageManager.getInstalledPackages(PACKAGE_FLAGS) + } + .filter { it.reqFeatures.orEmpty().any { f -> f.name == extensionFeature } } + + return runBlocking { + packages + .map { async { loadExtension(it, mapped) } } + .flatMap { it.await() } + } + } + + @SuppressLint("QueryPermissionsNeeded") + suspend fun loadExtensionsBlocking(mapped: (T, ApplicationInfo, PackageInfo) -> R = mapping): List { + val packageManager = context.packageManager + val packages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())) + } else { + packageManager.getInstalledPackages(PACKAGE_FLAGS) + } + .filter { it.reqFeatures.orEmpty().any { f -> f.name == extensionFeature } } + + return runBlocking { + packages + .map { async { loadExtension(it, mapped) } } + .flatMap { it.await() } + } + } + + private fun loadExtension(packageInfo: PackageInfo, mapped: (T, ApplicationInfo, PackageInfo) -> R): List { + val appInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.getApplicationInfo( + packageInfo.packageName, + PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()) + ) + } else { + context.packageManager.getApplicationInfo( + packageInfo.packageName, + PackageManager.GET_META_DATA + ) + } + + val classLoader = PathClassLoader(appInfo.sourceDir, null, this::class.java.classLoader) + + return appInfo.metaData.getString(metadataClass) + .orEmpty() + .split(";") + .map { + val sourceClass = it.trim() + if (sourceClass.startsWith(".")) { + packageInfo.packageName + sourceClass + } else { + sourceClass + } + } + .mapNotNull { + runCatching { + @Suppress("UNCHECKED_CAST") + Class.forName(it, false, classLoader) + .getDeclaredConstructor() + .newInstance() as? T + } + .onFailure { it.printStackTrace() } + .getOrNull() + } + .map { mapped(it, appInfo, packageInfo) } + } +} \ No newline at end of file diff --git a/sharedutils/extensionloader/src/main/java/com/programmersbox/extensionloader/SourceLoader.kt b/sharedutils/extensionloader/src/main/java/com/programmersbox/extensionloader/SourceLoader.kt new file mode 100644 index 000000000..915923e11 --- /dev/null +++ b/sharedutils/extensionloader/src/main/java/com/programmersbox/extensionloader/SourceLoader.kt @@ -0,0 +1,113 @@ +package com.programmersbox.extensionloader + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.os.Build +import com.programmersbox.models.ApiService +import com.programmersbox.models.ApiServicesCatalog +import com.programmersbox.models.ExternalApiServicesCatalog +import com.programmersbox.models.SourceInformation +import kotlinx.coroutines.runBlocking + + +private const val METADATA_NAME = "programmersbox.otaku.name" +private const val METADATA_CLASS = "programmersbox.otaku.class" +private const val EXTENSION_FEATURE = "programmersbox.otaku.extension" + +class SourceLoader( + application: Application, + private val context: Context, + sourceType: String, + private val sourceRepository: SourceRepository +) { + private val extensionLoader = ExtensionLoader>( + context, + "$EXTENSION_FEATURE.$sourceType", + METADATA_CLASS, + ) { t, a, p -> + when (t) { + is ApiService -> listOf( + SourceInformation( + apiService = t, + name = a.metaData.getString(METADATA_NAME) ?: "Nothing", + icon = context.packageManager.getApplicationIcon(p.packageName), + packageName = p.packageName, + ) + ) + + is ExternalApiServicesCatalog -> { + runBlocking { t.initialize(application) } + t.getSources().map { it.copy(catalog = t) } + } + + is ApiServicesCatalog -> t.createSources().map { + SourceInformation( + apiService = it, + name = a.metaData.getString(METADATA_NAME) ?: "Nothing", + icon = context.packageManager.getApplicationIcon(p.packageName), + packageName = p.packageName, + catalog = t, + ) + } + + else -> emptyList() + } + } + + private val PACKAGE_FLAGS = + PackageManager.GET_CONFIGURATIONS or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + PackageManager.GET_SIGNING_CERTIFICATES + } else { + @Suppress("DEPRECATION") + PackageManager.GET_SIGNATURES + } + + private val extensionType = "$EXTENSION_FEATURE.$sourceType" + + init { + val uninstallApplication: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent) { + if (intent.dataString == null) return + val packageString = intent.dataString.orEmpty().removePrefix("package:") + val isOtakuExtension = runCatching { + val p = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context?.packageManager?.getPackageInfo(packageString, PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())) + } else { + context?.packageManager?.getPackageInfo(packageString, PACKAGE_FLAGS) + } + checkNotNull(p) + val s = sourceRepository.list + .filter { it.catalog is ExternalApiServicesCatalog } + .any { (it.catalog as ExternalApiServicesCatalog).shouldReload(packageString, p) } + p.reqFeatures?.any { it.name == extensionType } == true || s + }.getOrDefault(false) + + if (!isOtakuExtension) return + + when (intent.action) { + Intent.ACTION_PACKAGE_REPLACED -> load() + Intent.ACTION_PACKAGE_ADDED -> load() + Intent.ACTION_PACKAGE_REMOVED -> load() + } + } + } + val intentFilter = IntentFilter() + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED) + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED) + intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED) + intentFilter.addDataScheme("package") + context.registerReceiver(uninstallApplication, intentFilter) + } + + fun load() { + sourceRepository.setSources(extensionLoader.loadExtensions().flatten().sortedBy { it.apiService.serviceName }) + } + + suspend fun blockingLoad() { + sourceRepository.setSources(extensionLoader.loadExtensionsBlocking().flatten().sortedBy { it.apiService.serviceName }) + } +} diff --git a/sharedutils/extensionloader/src/main/java/com/programmersbox/extensionloader/SourceRepository.kt b/sharedutils/extensionloader/src/main/java/com/programmersbox/extensionloader/SourceRepository.kt new file mode 100644 index 000000000..7079721a7 --- /dev/null +++ b/sharedutils/extensionloader/src/main/java/com/programmersbox/extensionloader/SourceRepository.kt @@ -0,0 +1,26 @@ +package com.programmersbox.extensionloader + +import com.programmersbox.models.SourceInformation +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class SourceRepository { + private val sourcesList = MutableStateFlow>(emptyList()) + val sources = sourcesList.asStateFlow() + val list get() = sourcesList.value + val apiServiceList get() = sourcesList.value.map { it.apiService } + + fun setSources(sourceList: List) { + sourcesList.value = sourceList + } + + fun removeSource(sourceInformation: SourceInformation) { + sourcesList.update { + sourcesList.value.toMutableList().apply { remove(sourceInformation) } + } + } + + fun toSource(name: String) = sourcesList.value.find { it.name == name } + fun toSourceByApiServiceName(name: String) = sourcesList.value.find { it.apiService.serviceName == name } +} \ No newline at end of file diff --git a/sharedutils/extensionloader/src/test/java/com/programmersbox/extensionloader/ExampleUnitTest.kt b/sharedutils/extensionloader/src/test/java/com/programmersbox/extensionloader/ExampleUnitTest.kt new file mode 100644 index 000000000..0cf69e9b1 --- /dev/null +++ b/sharedutils/extensionloader/src/test/java/com/programmersbox/extensionloader/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.programmersbox.extensionloader + +import org.junit.Assert.* +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file