diff --git a/app/build.gradle b/app/build.gradle index 9dca6e60..0de853b4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,6 +97,9 @@ dependencies { implementation 'com.alexvasilkov:gesture-views:2.8.3' implementation 'com.github.VipulOG:ebook-reader:0.1.6' +// string matching + implementation 'me.xdrop:fuzzywuzzy:1.4.0' + // Aniyomi implementation 'io.reactivex:rxjava:1.3.8' implementation 'io.reactivex:rxandroid:1.2.1' diff --git a/app/src/main/java/ani/dantotsu/App.kt b/app/src/main/java/ani/dantotsu/App.kt index b5c7ebad..49a50aac 100644 --- a/app/src/main/java/ani/dantotsu/App.kt +++ b/app/src/main/java/ani/dantotsu/App.kt @@ -11,6 +11,7 @@ import ani.dantotsu.aniyomi.anime.custom.PreferenceModule import eu.kanade.tachiyomi.data.notification.Notifications import tachiyomi.core.util.system.logcat import ani.dantotsu.others.DisabledReports +import com.google.android.material.color.DynamicColors import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase import logcat.AndroidLogcatLogger @@ -33,6 +34,11 @@ class App : MultiDexApplication() { override fun onCreate() { super.onCreate() + val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false) + if(useMaterialYou) { + DynamicColors.applyToActivitiesIfAvailable(this) + } registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks) Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports) diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt index 172a6ef1..36145f39 100644 --- a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt +++ b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt @@ -10,6 +10,7 @@ import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.network.NetworkPreferences import kotlinx.serialization.json.Json import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar @@ -21,7 +22,7 @@ class AppModule(val app: Application) : InjektModule { override fun InjektRegistrar.registerInjectables() { addSingleton(app) - addSingletonFactory { NetworkHelper(app) } + addSingletonFactory { NetworkHelper(app, get()) } addSingletonFactory { AnimeExtensionManager(app) } @@ -44,6 +45,13 @@ class PreferenceModule(val application: Application) : InjektModule { AndroidPreferenceStore(application) } + addSingletonFactory { + NetworkPreferences( + preferenceStore = get(), + verboseLogging = false, + ) + } + addSingletonFactory { SourcePreferences(get()) } diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt index 2381562a..8bbcc2ec 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt @@ -50,25 +50,14 @@ class MediaDetailsViewModel : ViewModel() { fun loadSelected(media: Media): Selected { val data = loadData("${media.id}-select") ?: Selected().let { - it.source = if (media.isAdult) "" else when (media.anime != null) { - true -> loadData("settings_def_anime_source") ?: "" - else -> loadData("settings_def_manga_source") ?: "" + it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) { + true -> loadData("settings_def_anime_source_s_r") ?: 0 + else -> loadData("settings_def_manga_source_s_r") ?: 0 } it.preferDub = loadData("settings_prefer_dub") ?: false - it.sourceIndex = loadSelectedStringLocation(it.source) saveSelected(media.id, it) it } - if (media.anime != null) { - val sources = if (media.isAdult) HAnimeSources else AnimeSources - data.sourceIndex = sources.list.indexOfFirst { it.name == data.source } - } else { - val sources = if (media.isAdult) HMangaSources else MangaSources - data.sourceIndex = sources.list.indexOfFirst { it.name == data.source } - } - if (data.sourceIndex == -1) { - data.sourceIndex = 0 - } return data } @@ -194,8 +183,8 @@ class MediaDetailsViewModel : ViewModel() { val server = selected.server ?: return false val link = ep.link ?: return false - ep.extractors = mutableListOf(watchSources?.get(loadSelectedStringLocation(selected.source))?.let { - selected.sourceIndex = loadSelectedStringLocation(selected.source) + ep.extractors = mutableListOf(watchSources?.get(selected.sourceIndex)?.let { + selected.sourceIndex = selected.sourceIndex if (!post && !it.allowsPreloading) null else ep.sEpisode?.let { it1 -> it.loadSingleVideoServer(server, link, ep.extra, @@ -266,7 +255,7 @@ class MediaDetailsViewModel : ViewModel() { suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean { return tryWithSuspend(true) { chapter.addImages( - mangaReadSources?.get(loadSelectedStringLocation(selected.source))?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false + mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false ) if (post) mangaChapter.postValue(chapter) true @@ -289,7 +278,7 @@ class MediaDetailsViewModel : ViewModel() { } suspend fun autoSearchNovels(media: Media) { - val source = novelSources[loadSelectedStringLocation(media.selected?.source?:"")] + val source = novelSources[media.selected?.sourceIndex?:0] tryWithSuspend(post = true) { if (source != null) { novelResponses.postValue(source.sortedSearch(media)) diff --git a/app/src/main/java/ani/dantotsu/media/Selected.kt b/app/src/main/java/ani/dantotsu/media/Selected.kt index 3849c272..ddb30f58 100644 --- a/app/src/main/java/ani/dantotsu/media/Selected.kt +++ b/app/src/main/java/ani/dantotsu/media/Selected.kt @@ -7,7 +7,7 @@ data class Selected( var recyclerStyle: Int? = null, var recyclerReversed: Boolean = false, var chip: Int = 0, - var source: String = "", + //var source: String = "", var sourceIndex: Int = 0, var preferDub: Boolean = false, var server: String? = null, diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt index 210e7b07..ff6349c7 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt @@ -1,22 +1,33 @@ package ani.dantotsu.media.manga +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore import android.util.LruCache import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream data class ImageData( val page: Page, - val source: HttpSource, + val source: HttpSource ){ - suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource): Bitmap? { + suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? { return withContext(Dispatchers.IO) { try { // Fetch the image val response = httpSource.getImage(page) + println("Response: ${response.code}") + println("Response: ${response.message}") // Convert the Response to an InputStream val inputStream = response.body?.byteStream() @@ -25,6 +36,7 @@ data class ImageData( val bitmap = BitmapFactory.decodeStream(inputStream) inputStream?.close() + saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100) return@withContext bitmap } catch (e: Exception) { @@ -36,6 +48,39 @@ data class ImageData( } } +fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, filename) + put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}") + put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga") + } + + val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + + uri?.let { + contentResolver.openOutputStream(it)?.use { os -> + bitmap.compress(format, quality, os) + } + } + } else { + val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime") + if (!directory.exists()) { + directory.mkdirs() + } + + val file = File(directory, filename) + FileOutputStream(file).use { outputStream -> + bitmap.compress(format, quality, outputStream) + } + } + } catch (e: Exception) { + // Handle exception here + println("Exception while saving image: ${e.message}") + } +} + class MangaCache() { private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024 / 2).toInt() private val cache = LruCache(maxMemory) diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt index a9a41547..aa0fafea 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt @@ -161,7 +161,7 @@ abstract class BaseImageAdapter( println(mangaCache.get(link.url)) println("cache size: ${mangaCache.size()}") mangaCache.get(link.url)?.let { imageData -> - val bitmap = imageData.fetchAndProcessImage(imageData.page, imageData.source) + val bitmap = imageData.fetchAndProcessImage(imageData.page, imageData.source, context = this@loadBitmap) it.load(bitmap) .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.NONE) diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index 6bfb01ac..2d681d57 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -2,6 +2,7 @@ package ani.dantotsu.parsers import android.content.ContentResolver import android.content.ContentValues +import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri @@ -199,12 +200,14 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { return coroutineScope { try { + println("source.name " + source.name) val res = source.getPageList(sChapter) val reIndexedPages = res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) } val deferreds = reIndexedPages.map { page -> async(Dispatchers.IO) { mangaCache.put(page.imageUrl ?: "", ImageData(page, source)) + logger("put page: ${page.imageUrl}") pageToMangaImage(page) } } @@ -212,11 +215,44 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { deferreds.awaitAll() } catch (e: Exception) { logger("loadImages Exception: $e") + Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT).show() emptyList() } } } + suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? { + return withContext(Dispatchers.IO) { + try { + // Fetch the image + val response = httpSource.getImage(page) + println("Response: ${response.code}") + println("Response: ${response.message}") + + // Convert the Response to an InputStream + val inputStream = response.body?.byteStream() + + // Convert InputStream to Bitmap + val bitmap = BitmapFactory.decodeStream(inputStream) + + inputStream?.close() + ani.dantotsu.media.manga.saveImage( + bitmap, + context.contentResolver, + page.imageUrl!!, + Bitmap.CompressFormat.JPEG, + 100 + ) + + return@withContext bitmap + } catch (e: Exception) { + // Handle any exceptions + println("An error occurred: ${e.message}") + return@withContext null + } + } + } + fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) { diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt index 5aa826b1..369714a9 100644 --- a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt @@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.source.model.SManga import java.io.Serializable import java.net.URLDecoder import java.net.URLEncoder +import me.xdrop.fuzzywuzzy.FuzzySearch + abstract class BaseParser { @@ -55,21 +57,41 @@ abstract class BaseParser { setUserText("Searching : ${mediaObj.mainName()}") val results = search(mediaObj.mainName()) val sortedResults = if (results.isNotEmpty()) { - StringMatcher.closestShowMovedToTop(mediaObj.mainName(), results) + results.sortedByDescending { FuzzySearch.ratio(it.name, mediaObj.mainName()) } } else { emptyList() } response = sortedResults.firstOrNull() - if (response == null) { + if (response == null || FuzzySearch.ratio(response.name, mediaObj.mainName()) < 100) { setUserText("Searching : ${mediaObj.nameRomaji}") val romajiResults = search(mediaObj.nameRomaji) val sortedRomajiResults = if (romajiResults.isNotEmpty()) { - StringMatcher.closestShowMovedToTop(mediaObj.nameRomaji, romajiResults) + romajiResults.sortedByDescending { FuzzySearch.ratio(it.name, mediaObj.nameRomaji) } } else { emptyList() } - response = sortedRomajiResults.firstOrNull() + val closestRomaji = sortedRomajiResults.firstOrNull() + logger("Closest match from RomajiResults: ${closestRomaji?.name ?: "None"}") + + response = if (response == null) { + logger("No exact match found in results. Using closest match from RomajiResults.") + closestRomaji + } else { + val romajiRatio = FuzzySearch.ratio(closestRomaji?.name ?: "", mediaObj.nameRomaji) + val mainNameRatio = FuzzySearch.ratio(response.name, mediaObj.mainName()) + logger("Fuzzy ratio for closest match in results: $mainNameRatio") + logger("Fuzzy ratio for closest match in RomajiResults: $romajiRatio") + + if (romajiRatio > mainNameRatio) { + logger("RomajiResults has a closer match. Replacing response.") + closestRomaji + } else { + logger("Results has a closer or equal match. Keeping existing response.") + response + } + } + } saveShowResponse(mediaObj.id, response) } diff --git a/app/src/main/java/ani/dantotsu/parsers/StringMatcher.kt b/app/src/main/java/ani/dantotsu/parsers/StringMatcher.kt index 4afd9a9d..a7bf0cd3 100644 --- a/app/src/main/java/ani/dantotsu/parsers/StringMatcher.kt +++ b/app/src/main/java/ani/dantotsu/parsers/StringMatcher.kt @@ -1,5 +1,7 @@ package ani.dantotsu.parsers +import ani.dantotsu.logger + class StringMatcher { companion object { private fun levenshteinDistance(s1: String, s2: String): Int { @@ -52,8 +54,10 @@ class StringMatcher { val closestShowAndIndex = closestShow(target, shows) val closestIndex = closestShowAndIndex.second if (closestIndex == -1) { + logger("No closest show found for $target") return shows // Return original list if no closest show found } + logger("Closest show found for $target is ${closestShowAndIndex.first.name}") return listOf(shows[closestIndex]) + shows.subList(0, closestIndex) + shows.subList(closestIndex + 1, shows.size) } diff --git a/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt index 8db88476..879336db 100644 --- a/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt @@ -11,6 +11,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat.getSystemService import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope @@ -18,6 +19,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.R import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding +import ani.dantotsu.loadData import com.bumptech.glide.Glide import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager @@ -35,13 +37,57 @@ class AnimeExtensionsFragment : Fragment(), SearchQueryHandler { private var _binding: FragmentAnimeExtensionsBinding? = null private val binding get() = _binding!! + val skipIcons = loadData("skip_extension_icons") ?: false + private lateinit var extensionsRecyclerView: RecyclerView private lateinit var allextenstionsRecyclerView: RecyclerView private val animeExtensionManager: AnimeExtensionManager = Injekt.get() - private val extensionsAdapter = AnimeExtensionsAdapter { pkgName -> - animeExtensionManager.uninstallExtension(pkgName) - } - private val allExtensionsAdapter = AllAnimeExtensionsAdapter(lifecycleScope) { pkgName -> + private val extensionsAdapter = AnimeExtensionsAdapter ({ pkg -> + if(pkg.hasUpdate){ + val notificationManager = + requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + animeExtensionManager.updateExtension(pkg) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { installStep -> + val builder = NotificationCompat.Builder( + requireContext(), + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(R.drawable.ic_round_sync_24) + .setContentTitle("Updating extension") + .setContentText("Step: $installStep") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + }, + { error -> + val builder = NotificationCompat.Builder( + requireContext(), + Notifications.CHANNEL_DOWNLOADER_ERROR + ) + .setSmallIcon(R.drawable.ic_round_info_24) + .setContentTitle("Update failed") + .setContentText("Error: ${error.message}") + .setPriority(NotificationCompat.PRIORITY_HIGH) + notificationManager.notify(1, builder.build()) + }, + { + val builder = NotificationCompat.Builder( + requireContext(), + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) + .setContentTitle("Update complete") + .setContentText("The extension has been successfully updated.") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + } + ) + }else { + animeExtensionManager.uninstallExtension(pkg.pkgName) + } + }, skipIcons) + private val allExtensionsAdapter = AllAnimeExtensionsAdapter(lifecycleScope, { pkgName -> val notificationManager = requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -84,7 +130,7 @@ class AnimeExtensionsFragment : Fragment(), SearchQueryHandler { notificationManager.notify(1, builder.build()) } ) - } + }, skipIcons) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentAnimeExtensionsBinding.inflate(inflater, container, false) @@ -136,9 +182,10 @@ class AnimeExtensionsFragment : Fragment(), SearchQueryHandler { } - private class AnimeExtensionsAdapter(private val onUninstallClicked: (String) -> Unit) : RecyclerView.Adapter() { + private class AnimeExtensionsAdapter(private val onUninstallClicked: (AnimeExtension.Installed) -> Unit, skipIcons: Boolean) : RecyclerView.Adapter() { private var extensions: List = emptyList() + val skipIcons = skipIcons fun updateData(newExtensions: List) { extensions = newExtensions @@ -154,10 +201,17 @@ class AnimeExtensionsFragment : Fragment(), SearchQueryHandler { override fun onBindViewHolder(holder: ViewHolder, position: Int) { val extension = extensions[position] holder.extensionNameTextView.text = extension.name - holder.extensionIconImageView.setImageDrawable(extension.icon) - holder.closeTextView.text = "Uninstall" + if (!skipIcons) { + holder.extensionIconImageView.setImageDrawable(extension.icon) + } + if(extension.hasUpdate){ + holder.closeTextView.text = "Update" + holder.closeTextView.setTextColor(ContextCompat.getColor(holder.itemView.context, R.color.warning)) + }else{ + holder.closeTextView.text = "Uninstall" + } holder.closeTextView.setOnClickListener { - onUninstallClicked(extension.pkgName) + onUninstallClicked(extension) } } @@ -171,8 +225,9 @@ class AnimeExtensionsFragment : Fragment(), SearchQueryHandler { } private class AllAnimeExtensionsAdapter(private val coroutineScope: CoroutineScope, - private val onButtonClicked: (AnimeExtension.Available) -> Unit) : RecyclerView.Adapter() { + private val onButtonClicked: (AnimeExtension.Available) -> Unit, skipIcons: Boolean) : RecyclerView.Adapter() { private var extensions: List = emptyList() + val skipIcons = skipIcons fun updateData(newExtensions: List, installedExtensions: List = emptyList()) { val installedPkgNames = installedExtensions.map { it.pkgName }.toSet() @@ -190,9 +245,11 @@ class AnimeExtensionsFragment : Fragment(), SearchQueryHandler { override fun onBindViewHolder(holder: ViewHolder, position: Int) { val extension = filteredExtensions[position] holder.extensionNameTextView.text = extension.name - coroutineScope.launch { - val drawable = urlToDrawable(holder.itemView.context, extension.iconUrl) - holder.extensionIconImageView.setImageDrawable(drawable) + if (!skipIcons) { + coroutineScope.launch { + val drawable = urlToDrawable(holder.itemView.context, extension.iconUrl) + holder.extensionIconImageView.setImageDrawable(drawable) + } } holder.closeTextView.text = "Install" holder.closeTextView.setOnClickListener { diff --git a/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt index 595fbe1f..53da89e1 100644 --- a/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt @@ -10,6 +10,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager @@ -17,6 +18,7 @@ import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.R import ani.dantotsu.databinding.FragmentMangaBinding import ani.dantotsu.databinding.FragmentMangaExtensionsBinding +import ani.dantotsu.loadData import com.bumptech.glide.Glide import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager @@ -34,14 +36,58 @@ class MangaExtensionsFragment : Fragment(), SearchQueryHandler { private var _binding: FragmentMangaExtensionsBinding? = null private val binding get() = _binding!! + val skipIcons = loadData("skip_extension_icons") ?: false + private lateinit var extensionsRecyclerView: RecyclerView private lateinit var allextenstionsRecyclerView: RecyclerView private val mangaExtensionManager:MangaExtensionManager = Injekt.get() - private val extensionsAdapter = MangaExtensionsAdapter { pkgName -> - mangaExtensionManager.uninstallExtension(pkgName) - } + private val extensionsAdapter = MangaExtensionsAdapter ({ pkg -> + if(pkg.hasUpdate){ + val notificationManager = + requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + mangaExtensionManager.updateExtension(pkg) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { installStep -> + val builder = NotificationCompat.Builder( + requireContext(), + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(R.drawable.ic_round_sync_24) + .setContentTitle("Updating extension") + .setContentText("Step: $installStep") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + }, + { error -> + val builder = NotificationCompat.Builder( + requireContext(), + Notifications.CHANNEL_DOWNLOADER_ERROR + ) + .setSmallIcon(R.drawable.ic_round_info_24) + .setContentTitle("Update failed") + .setContentText("Error: ${error.message}") + .setPriority(NotificationCompat.PRIORITY_HIGH) + notificationManager.notify(1, builder.build()) + }, + { + val builder = NotificationCompat.Builder( + requireContext(), + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) + .setContentTitle("Update complete") + .setContentText("The extension has been successfully updated.") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + } + ) + }else { + mangaExtensionManager.uninstallExtension(pkg.pkgName) + } + }, skipIcons) private val allExtensionsAdapter = - AllMangaExtensionsAdapter(lifecycleScope) { pkgName -> + AllMangaExtensionsAdapter(lifecycleScope, { pkgName -> val notificationManager = requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -84,7 +130,8 @@ class MangaExtensionsFragment : Fragment(), SearchQueryHandler { notificationManager.notify(1, builder.build()) } ) - } + }, skipIcons) + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentMangaExtensionsBinding.inflate(inflater, container, false) @@ -134,9 +181,10 @@ class MangaExtensionsFragment : Fragment(), SearchQueryHandler { super.onDestroyView();_binding = null } - private class MangaExtensionsAdapter(private val onUninstallClicked: (String) -> Unit) : RecyclerView.Adapter() { + private class MangaExtensionsAdapter(private val onUninstallClicked: (MangaExtension.Installed) -> Unit, skipIcons: Boolean) : RecyclerView.Adapter() { private var extensions: List = emptyList() + val skipIcons = skipIcons fun updateData(newExtensions: List) { extensions = newExtensions @@ -152,10 +200,17 @@ class MangaExtensionsFragment : Fragment(), SearchQueryHandler { override fun onBindViewHolder(holder: ViewHolder, position: Int) { val extension = extensions[position] holder.extensionNameTextView.text = extension.name - holder.extensionIconImageView.setImageDrawable(extension.icon) - holder.closeTextView.text = "Uninstall" + if(!skipIcons) { + holder.extensionIconImageView.setImageDrawable(extension.icon) + } + if(extension.hasUpdate){ + holder.closeTextView.text = "Update" + holder.closeTextView.setTextColor(ContextCompat.getColor(holder.itemView.context, R.color.warning)) + }else{ + holder.closeTextView.text = "Uninstall" + } holder.closeTextView.setOnClickListener { - onUninstallClicked(extension.pkgName) + onUninstallClicked(extension) } } @@ -169,8 +224,9 @@ class MangaExtensionsFragment : Fragment(), SearchQueryHandler { } private class AllMangaExtensionsAdapter(private val coroutineScope: CoroutineScope, - private val onButtonClicked: (MangaExtension.Available) -> Unit) : RecyclerView.Adapter() { + private val onButtonClicked: (MangaExtension.Available) -> Unit, skipIcons: Boolean) : RecyclerView.Adapter() { private var extensions: List = emptyList() + val skipIcons = skipIcons fun updateData(newExtensions: List, installedExtensions: List = emptyList()) { val installedPkgNames = installedExtensions.map { it.pkgName }.toSet() @@ -188,9 +244,11 @@ class MangaExtensionsFragment : Fragment(), SearchQueryHandler { override fun onBindViewHolder(holder: ViewHolder, position: Int) { val extension = filteredExtensions[position] holder.extensionNameTextView.text = extension.name - coroutineScope.launch { - val drawable = urlToDrawable(holder.itemView.context, extension.iconUrl) - holder.extensionIconImageView.setImageDrawable(drawable) + if (!skipIcons) { + coroutineScope.launch { + val drawable = urlToDrawable(holder.itemView.context, extension.iconUrl) + holder.extensionIconImageView.setImageDrawable(drawable) + } } holder.closeTextView.text = "Install" holder.closeTextView.setOnClickListener { diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt index 68a663da..ee02f3e1 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt @@ -2,6 +2,7 @@ package ani.dantotsu.settings import android.annotation.SuppressLint import android.app.AlertDialog +import android.content.Context import android.content.Intent import android.graphics.drawable.Animatable import android.os.Build.* @@ -31,6 +32,7 @@ import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes import eu.kanade.domain.base.BasePreferences +import eu.kanade.tachiyomi.network.NetworkPreferences import io.noties.markwon.Markwon import io.noties.markwon.SoftBreakAddsNewLinePlugin import kotlinx.coroutines.Dispatchers @@ -47,6 +49,7 @@ class SettingsActivity : AppCompatActivity() { } lateinit var binding: ActivitySettingsBinding private val extensionInstaller = Injekt.get().extensionInstaller() + private val networkPreferences = Injekt.get() @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { @@ -92,18 +95,21 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) onBackPressedDispatcher.onBackPressed() } - val animeSourceName = loadData("settings_def_anime_source") ?: AnimeSources.names[0] - // Set the dropdown item in the UI if the name exists in the list. - if (AnimeSources.names.contains(animeSourceName)) { - binding.animeSource.setText(animeSourceName, false) + binding.settingsUseMaterialYou.isChecked = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_material_you", false) + binding.settingsUseMaterialYou.setOnCheckedChangeListener { _, isChecked -> + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean("use_material_you", isChecked).apply() + } + + val animeSource = loadData("settings_def_anime_source_s")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0 + if (MangaSources.names.isNotEmpty() && animeSource in 0 until MangaSources.names.size) { + binding.mangaSource.setText(MangaSources.names[animeSource], false) + } binding.animeSource.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, AnimeSources.names)) - // Set up the item click listener for the dropdown. + binding.animeSource.setOnItemClickListener { _, _, i, _ -> - val selectedName = AnimeSources.names[i] - // Save the string name of the selected item. - saveData("settings_def_anime_source", selectedName) + saveData("settings_def_anime_source_s", i) binding.animeSource.clearFocus() } @@ -131,6 +137,17 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) } } + binding.skipExtensionIcons.isChecked = loadData("skip_extension_icons") ?: false + binding.skipExtensionIcons.setOnCheckedChangeListener { _, isChecked -> + saveData("skip_extension_icons", isChecked) + } + + binding.userAgent.setText(networkPreferences.defaultUserAgent().get()) + binding.userAgent.setOnEditorActionListener { _, _, _ -> + networkPreferences.defaultUserAgent().set(binding.userAgent.text.toString()) + true + } + binding.settingsDownloadInSd.isChecked = loadData("sd_dl") ?: false binding.settingsDownloadInSd.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { @@ -169,12 +186,9 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) saveData("settings_prefer_dub", isChecked) } - // Load the saved manga source name from data storage. - val mangaSourceName = loadData("settings_def_manga_source") ?: MangaSources.names[0] - - // Set the dropdown item in the UI if the name exists in the list. - if (MangaSources.names.contains(mangaSourceName)) { - binding.mangaSource.setText(mangaSourceName, false) + val mangaSource = loadData("settings_def_manga_source_s")?.let { if (it >= MangaSources.names.size) 0 else it } ?: 0 + if (MangaSources.names.isNotEmpty() && mangaSource in 0 until MangaSources.names.size) { + binding.mangaSource.setText(MangaSources.names[mangaSource], false) } // Set up the dropdown adapter. @@ -182,9 +196,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) // Set up the item click listener for the dropdown. binding.mangaSource.setOnItemClickListener { _, _, i, _ -> - val selectedName = MangaSources.names[i] - // Save the string name of the selected item. - saveData("settings_def_manga_source", selectedName) + saveData("settings_def_manga_source_s", i) binding.mangaSource.clearFocus() } diff --git a/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt b/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt index 0ed1b8e7..f637929f 100644 --- a/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt +++ b/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt @@ -15,21 +15,13 @@ class SubscriptionHelper { companion object { private fun loadSelected(context: Context, mediaId: Int, isAdult: Boolean, isAnime: Boolean): Selected { val data = loadData("${mediaId}-select", context) ?: Selected().let { - it.source = - if (isAdult) "" - else if (isAnime) {loadData("settings_def_anime_source", context) ?: ""} - else loadData("settings_def_manga_source", context) ?: "" + it.sourceIndex = + if (isAdult) 0 + else if (isAnime) {loadData("settings_def_anime_source_s_r", context) ?: 0} + else loadData("settings_def_manga_source_s_r", context) ?: 0 it.preferDub = loadData("settings_prefer_dub", context) ?: false it } - if (isAnime){ - val sources = if (isAdult) HAnimeSources else AnimeSources - data.sourceIndex = sources.list.indexOfFirst { it.name == data.source } - }else{ - val sources = if (isAdult) HMangaSources else MangaSources - data.sourceIndex = sources.list.indexOfFirst { it.name == data.source } - } - if (data.sourceIndex == -1) {data.sourceIndex = 0} return data } @@ -40,9 +32,7 @@ class SubscriptionHelper { fun getAnimeParser(context: Context, isAdult: Boolean, id: Int): AnimeParser { val sources = if (isAdult) HAnimeSources else AnimeSources val selected = loadSelected(context, id, isAdult, true) - var location = sources.list.indexOfFirst { it.name == selected.source } - if (location == -1) {location = 0} - val parser = sources[location] + val parser = sources[selected.sourceIndex] parser.selectDub = selected.preferDub return parser } @@ -69,9 +59,7 @@ class SubscriptionHelper { fun getMangaParser(context: Context, isAdult: Boolean, id: Int): MangaParser { val sources = if (isAdult) HMangaSources else MangaSources val selected = loadSelected(context, id, isAdult, false) - var location = sources.list.indexOfFirst { it.name == selected.source } - if (location == -1) {location = 0} - return sources[location] + return sources[selected.sourceIndex] } suspend fun getChapter(context: Context, parser: MangaParser, id: Int, isAdult: Boolean): MangaChapter? { diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index 8f5afb9f..fdd7bd69 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -11,11 +11,13 @@ import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import okhttp3.Cache import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import java.io.File import java.util.concurrent.TimeUnit class NetworkHelper( context: Context, + private val preferences: NetworkPreferences, ) { private val cacheDir = File(context.cacheDir, "network_cache") @@ -40,18 +42,17 @@ class NetworkHelper( .addInterceptor(UncaughtExceptionInterceptor()) .addInterceptor(userAgentInterceptor) - /*if (preferences.verboseLogging().get()) { + if (preferences.verboseLogging().get()) { val httpLoggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.HEADERS } builder.addNetworkInterceptor(httpLoggingInterceptor) - }*/ + } - //when (preferences.dohProvider().get()) { - when (PREF_DOH_CLOUDFLARE) { + when (preferences.dohProvider().get()) { PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() PREF_DOH_GOOGLE -> builder.dohGoogle() - /*PREF_DOH_ADGUARD -> builder.dohAdGuard() + PREF_DOH_ADGUARD -> builder.dohAdGuard() PREF_DOH_QUAD9 -> builder.dohQuad9() PREF_DOH_ALIDNS -> builder.dohAliDNS() PREF_DOH_DNSPOD -> builder.dohDNSPod() @@ -60,7 +61,7 @@ class NetworkHelper( PREF_DOH_MULLVAD -> builder.dohMullvad() PREF_DOH_CONTROLD -> builder.dohControlD() PREF_DOH_NJALLA -> builder.dohNajalla() - PREF_DOH_SHECAN -> builder.dohShecan()*/ + PREF_DOH_SHECAN -> builder.dohShecan() } return builder @@ -75,5 +76,5 @@ class NetworkHelper( .build() } - fun defaultUserAgentProvider() = "Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"//preferences.defaultUserAgent().get().trim() + fun defaultUserAgentProvider() = preferences.defaultUserAgent().get().trim() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkPreferences.kt new file mode 100644 index 00000000..21a5eb08 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkPreferences.kt @@ -0,0 +1,22 @@ +package eu.kanade.tachiyomi.network + +import tachiyomi.core.preference.Preference +import tachiyomi.core.preference.PreferenceStore + +class NetworkPreferences( + private val preferenceStore: PreferenceStore, + private val verboseLogging: Boolean = false, +) { + + fun verboseLogging(): Preference { + return preferenceStore.getBoolean("verbose_logging", verboseLogging) + } + + fun dohProvider(): Preference { + return preferenceStore.getInt("doh_provider", 1) + } + + fun defaultUserAgent(): Preference { + return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0") + } +} diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index ef411f3c..6881048c 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -74,77 +74,110 @@ android:paddingStart="32dp" android:paddingEnd="32dp"> - + android:layout_height="wrap_content" + android:orientation="vertical"> + android:textColor="?attr/colorSecondary" + app:drawableEndCompat="@drawable/ic_round_arrow_drop_down_24" + tools:ignore="TextContrastCheck" /> - + android:orientation="horizontal"> - - + - + + + + + + + + + + + + + + + + + - - - - - - - - - - + android:checked="false" + android:drawableStart="@drawable/ic_round_new_releases_24" + android:drawablePadding="16dp" + android:elegantTextHeight="true" + android:fontFamily="@font/poppins_bold" + android:minHeight="64dp" + android:text="@string/use_material_you" + android:textAlignment="viewStart" + android:textColor="@color/bg_opp" + app:cornerRadius="0dp" + app:drawableTint="?attr/colorPrimary" + app:showText="false" + app:thumbTint="@color/button_switch_track" /> + + + + + + + + + + + android:orientation="horizontal" + android:paddingTop="16dp" + android:paddingBottom="16dp"> diff --git a/app/src/main/res/layout/item_extension_all.xml b/app/src/main/res/layout/item_extension_all.xml index 03fe1643..292d438e 100644 --- a/app/src/main/res/layout/item_extension_all.xml +++ b/app/src/main/res/layout/item_extension_all.xml @@ -28,6 +28,6 @@ android:layout_gravity="center_vertical" android:textStyle="bold" android:text="Install" - android:textColor="@color/fav" + android:textColor="@color/pink_500" android:textSize="14sp"/> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 13916a76..ced851d3 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -21,4 +21,5 @@ #80FFFFFF #ad5edd #54FF8400 + #FF0000 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a30d721f..2cfd2da4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -624,5 +624,7 @@ View Manga Force Legacy Installer Extensions + skip loading extension icons + Use Material You