feat: make repo adding easier

This commit is contained in:
rebel onion 2024-12-30 19:25:22 -06:00
parent 6f1bb10dec
commit 43dee6ee49
13 changed files with 383 additions and 346 deletions

3
.gitignore vendored
View file

@ -2,6 +2,9 @@
.gradle/ .gradle/
build/ build/
#kotlin
.kotlin/
# Local configuration file (sdk path, etc) # Local configuration file (sdk path, etc)
local.properties local.properties

View file

@ -116,6 +116,7 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:host="*"/>
<data android:mimeType="application/epub+zip"/> <data android:mimeType="application/epub+zip"/>
<data android:mimeType="application/x-mobipocket-ebook" /> <data android:mimeType="application/x-mobipocket-ebook" />
<data android:mimeType="application/vnd.amazon.ebook" /> <data android:mimeType="application/vnd.amazon.ebook" />
@ -374,25 +375,31 @@
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.Main" /> <action android:name="android.intent.action.Main" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" /> <data android:scheme="content" />
<data android:mimeType="*/*" /> <data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" /> <data android:pathPattern=".*\\.ani" />
<data android:pathPattern=".*\\.sani" /> <data android:pathPattern=".*\\.sani" />
<data android:host="*" /> <data android:host="*" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Support both schemes -->
<data android:scheme="tachiyomi"/>
<data android:host="add-repo"/>
<data android:scheme="aniyomi"/>
<data android:host="add-repo"/>
</intent-filter>
</activity> </activity>
<activity <activity
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity" android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"

View file

@ -91,7 +91,6 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.BuildConfig.APPLICATION_ID import ani.dantotsu.BuildConfig.APPLICATION_ID
import ani.dantotsu.connections.anilist.Genre import ani.dantotsu.connections.anilist.Genre
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.bakaupdates.MangaUpdates
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
@ -106,7 +105,6 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.internal.PreferenceKeystore import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.util.CountUpTimer
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
@ -1013,47 +1011,10 @@ fun countDown(media: Media, view: ViewGroup) {
} }
} }
fun sinceWhen(media: Media, view: ViewGroup) {
if (media.status != "RELEASING" && media.status != "HIATUS") return
CoroutineScope(Dispatchers.IO).launch {
MangaUpdates().search(media.mangaName(), media.startDate)?.let {
val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
val timeSince = (System.currentTimeMillis() -
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
withContext(Dispatchers.Main) {
val v =
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
object : CountUpTimer(86400000) {
override fun onTick(second: Int) {
val a = second + timeSince
v.mediaCountdown.text = currActivity()?.getString(
R.string.time_format,
a / 86400,
a % 86400 / 3600,
a % 86400 % 3600 / 60,
a % 86400 % 3600 % 60
)
}
override fun onFinish() {
// The legend will never die.
}
}.start()
}
}
}
}
fun displayTimer(media: Media, view: ViewGroup) { fun displayTimer(media: Media, view: ViewGroup) {
when { when {
media.anime != null -> countDown(media, view) media.anime != null -> countDown(media, view)
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view) else -> {}
else -> {} // No timer yet
} }
} }

View file

@ -116,58 +116,8 @@ class MainActivity : AppCompatActivity() {
} }
} }
val action = intent.action if (Intent.ACTION_VIEW == intent.action) {
val type = intent.type handleViewIntent(intent)
if (Intent.ACTION_VIEW == action && type != null) {
val uri: Uri? = intent.data
try {
if (uri == null) {
throw Exception("Uri is null")
}
val jsonString =
contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name =
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try {
PreferenceKeystore.decryptWithPassword(
password,
encrypted,
salt
)
} catch (e: Exception) {
toast("Incorrect password")
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
toast("Error importing settings")
}
} }
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar) val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
@ -492,6 +442,73 @@ class MainActivity : AppCompatActivity() {
params.updateMargins(bottom = margin.toPx) params.updateMargins(bottom = margin.toPx)
} }
private fun handleViewIntent(intent: Intent) {
val uri: Uri? = intent.data
try {
if (uri == null) {
throw Exception("Uri is null")
}
if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi") && uri.host == "add-repo") {
val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import")
val prefName = if (uri.scheme == "tachiyomi") {
PrefName.MangaExtensionRepos
} else {
PrefName.AnimeExtensionRepos
}
val savedRepos: Set<String> = PrefManager.getVal(prefName)
val newRepos = savedRepos.toMutableSet()
newRepos.add(url)
PrefManager.setVal(prefName, newRepos)
toast("${if (uri.scheme == "tachiyomi") "Manga" else "Anime"} Extension Repo added")
return
}
if (intent.type == null) return
val jsonString =
contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name =
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try {
PreferenceKeystore.decryptWithPassword(
password,
encrypted,
salt
)
} catch (e: Exception) {
toast("Incorrect password")
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
toast("Error importing settings")
}
}
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) { private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') } val password = CharArray(16).apply { fill('0') }

View file

@ -1,133 +0,0 @@
package ani.dantotsu.connections.bakaupdates
import android.content.Context
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import okio.ByteString.Companion.encode
import org.json.JSONException
import org.json.JSONObject
import java.nio.charset.Charset
class MangaUpdates {
private val Int?.dateFormat get() = String.format("%02d", this)
private val apiUrl = "https://api.mangaupdates.com/v1/releases/search"
suspend fun search(title: String, startDate: FuzzyDate?): MangaUpdatesResponse.Results? {
return tryWithSuspend {
val query = JSONObject().apply {
try {
put("search", title.encode(Charset.forName("UTF-8")))
startDate?.let {
put(
"start_date",
"${it.year}-${it.month.dateFormat}-${it.day.dateFormat}"
)
}
put("include_metadata", true)
} catch (e: JSONException) {
e.printStackTrace()
}
}
val res = try {
client.post(apiUrl, json = query).parsed<MangaUpdatesResponse>()
} catch (e: Exception) {
Logger.log(e.toString())
return@tryWithSuspend null
}
coroutineScope {
res.results?.map {
async(Dispatchers.IO) {
Logger.log(it.toString())
}
}
}?.awaitAll()
res.results?.first {
it.metadata.series.lastUpdated?.timestamp != null
&& (it.metadata.series.latestChapter != null
|| (it.record.volume.isNullOrBlank() && it.record.chapter != null))
}
}
}
companion object {
fun getLatestChapter(context: Context, results: MangaUpdatesResponse.Results): String {
return results.metadata.series.latestChapter?.let {
context.getString(R.string.chapter_number, it)
} ?: results.record.chapter!!.substringAfterLast("-").trim().let { chapter ->
chapter.takeIf {
it.toIntOrNull() == null
} ?: context.getString(R.string.chapter_number, chapter.toInt())
}
}
}
@Serializable
data class MangaUpdatesResponse(
@SerialName("total_hits")
val totalHits: Int?,
@SerialName("page")
val page: Int?,
@SerialName("per_page")
val perPage: Int?,
val results: List<Results>? = null
) {
@Serializable
data class Results(
val record: Record,
val metadata: MetaData
) {
@Serializable
data class Record(
@SerialName("id")
val id: Int,
@SerialName("title")
val title: String,
@SerialName("volume")
val volume: String?,
@SerialName("chapter")
val chapter: String?,
@SerialName("release_date")
val releaseDate: String
)
@Serializable
data class MetaData(
val series: Series
) {
@Serializable
data class Series(
@SerialName("series_id")
val seriesId: Long?,
@SerialName("title")
val title: String?,
@SerialName("latest_chapter")
val latestChapter: Int?,
@SerialName("last_updated")
val lastUpdated: LastUpdated?
) {
@Serializable
data class LastUpdated(
@SerialName("timestamp")
val timestamp: Long,
@SerialName("as_rfc3339")
val asRfc3339: String,
@SerialName("as_string")
val asString: String
)
}
}
}
}
}

View file

@ -0,0 +1,138 @@
package ani.dantotsu.settings
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetAddRepositoryBinding
import ani.dantotsu.databinding.ItemRepoBinding
import ani.dantotsu.media.MediaType
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.viewbinding.BindableItem
class RepoItem(
val url: String,
val onRemove: (String) -> Unit
) :BindableItem<ItemRepoBinding>() {
override fun getLayout() = R.layout.item_repo
override fun bind(viewBinding: ItemRepoBinding, position: Int) {
viewBinding.repoNameTextView.text = url
viewBinding.repoDeleteImageView.setOnClickListener {
onRemove(url)
}
}
override fun initializeViewBinding(view: View): ItemRepoBinding {
return ItemRepoBinding.bind(view)
}
}
class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
private var _binding: BottomSheetAddRepositoryBinding? = null
private val binding get() = _binding!!
private var mediaType: MediaType = MediaType.ANIME
private var onRepositoryAdded: ((String, MediaType) -> Unit)? = null
private var repositories: MutableList<String> = mutableListOf()
private var onRepositoryRemoved: ((String) -> Unit)? = null
private var adapter: GroupieAdapter = GroupieAdapter()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetAddRepositoryBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.repositoriesRecyclerView.adapter = adapter
binding.repositoriesRecyclerView.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
adapter.addAll(repositories.map { RepoItem(it, ::onRepositoryRemoved) })
binding.repositoryInput.hint = when(mediaType) {
MediaType.ANIME -> getString(R.string.anime_add_repository)
MediaType.MANGA -> getString(R.string.manga_add_repository)
else -> ""
}
binding.addButton.setOnClickListener {
val input = binding.repositoryInput.text.toString()
val error = isValidUrl(input)
if (error == null) {
onRepositoryAdded?.invoke(input, mediaType)
dismiss()
} else {
binding.repositoryInput.error = error
}
}
binding.cancelButton.setOnClickListener {
dismiss()
}
binding.repositoryInput.setOnEditorActionListener { textView, action, keyEvent ->
if (action == EditorInfo.IME_ACTION_DONE ||
(keyEvent?.action == KeyEvent.ACTION_UP && keyEvent.keyCode == KeyEvent.KEYCODE_ENTER)) {
if (!textView.text.isNullOrBlank()) {
val error = isValidUrl(textView.text.toString())
if (error == null) {
onRepositoryAdded?.invoke(textView.text.toString(), mediaType)
dismiss()
return@setOnEditorActionListener true
} else {
binding.repositoryInput.error = error
}
}
}
false
}
}
private fun onRepositoryRemoved(url: String) {
onRepositoryRemoved?.invoke(url)
repositories.remove(url)
adapter.update(repositories.map { RepoItem(it, ::onRepositoryRemoved) })
}
private fun isValidUrl(url: String): String? {
if (!url.startsWith("https://") && !url.startsWith("http://"))
return "URL must start with http:// or https://"
if (!url.removeSuffix("/").endsWith("index.min.json"))
return "URL must end with index.min.json"
return null
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
fun newInstance(
mediaType: MediaType,
repositories: List<String>,
onRepositoryAdded: (String, MediaType) -> Unit,
onRepositoryRemoved: (String) -> Unit
): AddRepositoryBottomSheet {
return AddRepositoryBottomSheet().apply {
this.mediaType = mediaType
this.repositories.addAll(repositories)
this.onRepositoryAdded = onRepositoryAdded
this.onRepositoryRemoved = onRepositoryRemoved
}
}
}
}

View file

@ -27,7 +27,6 @@ import ani.dantotsu.restartApp
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.customAlertDialog import ani.dantotsu.util.customAlertDialog
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
@ -117,14 +116,13 @@ class SettingsExtensionsActivity : AppCompatActivity() {
} }
fun processUserInput(input: String, mediaType: MediaType, view: ViewGroup) { fun processUserInput(input: String, mediaType: MediaType, view: ViewGroup) {
val entry = val validLink = if (input.contains("github.com") && input.contains("blob")) {
if (input.endsWith("/") || input.endsWith("index.min.json")) input.substring( input.replace("github.com", "raw.githubusercontent.com")
0, .replace("/blob/", "/")
input.lastIndexOf("/") } else input
) else input
if (mediaType == MediaType.ANIME) { if (mediaType == MediaType.ANIME) {
val anime = val anime =
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(entry) PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(validLink)
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime) PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager.findAvailableExtensions() animeExtensionManager.findAvailableExtensions()
@ -133,7 +131,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
} }
if (mediaType == MediaType.MANGA) { if (mediaType == MediaType.MANGA) {
val manga = val manga =
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(entry) PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(validLink)
PrefManager.setVal(PrefName.MangaExtensionRepos, manga) PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
mangaExtensionManager.findAvailableExtensions() mangaExtensionManager.findAvailableExtensions()
@ -142,25 +140,6 @@ class SettingsExtensionsActivity : AppCompatActivity() {
} }
} }
fun processEditorAction(
dialog: AlertDialog,
editText: EditText,
mediaType: MediaType,
view: ViewGroup
) {
editText.setOnEditorActionListener { textView, action, keyEvent ->
if (action == EditorInfo.IME_ACTION_SEARCH || action == EditorInfo.IME_ACTION_DONE || (keyEvent?.action == KeyEvent.ACTION_UP && keyEvent.keyCode == KeyEvent.KEYCODE_ENTER)) {
return@setOnEditorActionListener if (textView.text.isNullOrBlank()) {
false
} else {
processUserInput(textView.text.toString(), mediaType, view)
dialog.dismiss()
true
}
}
false
}
}
settingsRecyclerView.adapter = SettingsAdapter( settingsRecyclerView.adapter = SettingsAdapter(
arrayListOf( arrayListOf(
Settings( Settings(
@ -169,31 +148,19 @@ class SettingsExtensionsActivity : AppCompatActivity() {
desc = getString(R.string.anime_add_repository_desc), desc = getString(R.string.anime_add_repository_desc),
icon = R.drawable.ic_github, icon = R.drawable.ic_github,
onClick = { onClick = {
val dialogView = DialogUserAgentBinding.inflate(layoutInflater) val animeRepos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos)
val editText = dialogView.userAgentTextBox.apply { AddRepositoryBottomSheet.newInstance(
hint = getString(R.string.anime_add_repository)
}
context.customAlertDialog().apply {
setTitle(R.string.anime_add_repository)
setCustomView(dialogView.root)
setPosButton(getString(R.string.ok)) {
if (!editText.text.isNullOrBlank()) processUserInput(
editText.text.toString(),
MediaType.ANIME, MediaType.ANIME,
it.attachView animeRepos.toList(),
) onRepositoryAdded = { input, mediaType ->
} processUserInput(input, mediaType, it.attachView)
setNegButton(getString(R.string.cancel)) },
attach { dialog -> onRepositoryRemoved = { item ->
processEditorAction( val repos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).minus(item)
dialog, PrefManager.setVal(PrefName.AnimeExtensionRepos, repos)
editText, setExtensionOutput(it.attachView, MediaType.ANIME)
MediaType.ANIME,
it.attachView
)
}
show()
} }
).show(supportFragmentManager, "add_repo")
}, },
attach = { attach = {
setExtensionOutput(it.attachView, MediaType.ANIME) setExtensionOutput(it.attachView, MediaType.ANIME)
@ -205,31 +172,19 @@ class SettingsExtensionsActivity : AppCompatActivity() {
desc = getString(R.string.manga_add_repository_desc), desc = getString(R.string.manga_add_repository_desc),
icon = R.drawable.ic_github, icon = R.drawable.ic_github,
onClick = { onClick = {
val dialogView = DialogUserAgentBinding.inflate(layoutInflater) val mangaRepos = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos)
val editText = dialogView.userAgentTextBox.apply { AddRepositoryBottomSheet.newInstance(
hint = getString(R.string.manga_add_repository)
}
context.customAlertDialog().apply {
setTitle(R.string.manga_add_repository)
setCustomView(dialogView.root)
setPosButton(R.string.ok) {
if (!editText.text.isNullOrBlank()) processUserInput(
editText.text.toString(),
MediaType.MANGA, MediaType.MANGA,
it.attachView mangaRepos.toList(),
) onRepositoryAdded = { input, mediaType ->
processUserInput(input, mediaType, it.attachView)
},
onRepositoryRemoved = { item ->
val repos = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).minus(item)
PrefManager.setVal(PrefName.MangaExtensionRepos, repos)
setExtensionOutput(it.attachView, MediaType.MANGA)
} }
setNegButton(R.string.cancel) ).show(supportFragmentManager, "add_repo")
attach { dialog ->
processEditorAction(
dialog,
editText,
MediaType.MANGA,
it.attachView
)
}
}.show()
}, },
attach = { attach = {
setExtensionOutput(it.attachView, MediaType.MANGA) setExtensionOutput(it.attachView, MediaType.MANGA)

View file

@ -1,22 +0,0 @@
package ani.dantotsu.util
import android.os.CountDownTimer
// https://stackoverflow.com/a/40422151/461982
abstract class CountUpTimer protected constructor(
private val duration: Long
) : CountDownTimer(duration, INTERVAL_MS) {
abstract fun onTick(second: Int)
override fun onTick(msUntilFinished: Long) {
val second = ((duration - msUntilFinished) / 1000).toInt()
onTick(second)
}
override fun onFinish() {
onTick(duration / 1000)
}
companion object {
private const val INTERVAL_MS: Long = 1000
}
}

View file

@ -89,13 +89,6 @@ internal class ExtensionGithubApi {
.toAnimeExtensions(it) .toAnimeExtensions(it)
} }
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
//if (repoExtensions.size < 10) {
// throw Exception()
//}
// No official repo now so this won't be needed anymore. User-made repo can have less than 10 extensions
extensions.addAll(repoExtensions) extensions.addAll(repoExtensions)
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.log("Failed to get extensions from GitHub") Logger.log("Failed to get extensions from GitHub")
@ -156,13 +149,18 @@ internal class ExtensionGithubApi {
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList() PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
repos.forEach { repos.forEach {
val repoUrl = if (it.contains("index.min.json")) {
it
} else {
"$it${if (it.endsWith('/')) "" else "/"}index.min.json"
}
try { try {
val githubResponse = try { val githubResponse = try {
networkService.client networkService.client
.newCall(GET("${it}/index.min.json")) .newCall(GET(repoUrl))
.awaitSuccess() .awaitSuccess()
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.log("Failed to get repo: $it") Logger.log("Failed to get repo: $repoUrl")
Logger.log(e) Logger.log(e)
null null
} }
@ -179,13 +177,6 @@ internal class ExtensionGithubApi {
.toMangaExtensions(it) .toMangaExtensions(it)
} }
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
//if (repoExtensions.size < 10) {
// throw Exception()
//}
// No official repo now so this won't be needed anymore. User made repo can have less than 10 extensions.
extensions.addAll(repoExtensions) extensions.addAll(repoExtensions)
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.log("Failed to get extensions from GitHub") Logger.log("Failed to get extensions from GitHub")
@ -203,8 +194,11 @@ internal class ExtensionGithubApi {
private fun fallbackRepoUrl(repoUrl: String): String? { private fun fallbackRepoUrl(repoUrl: String): String? {
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/" var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
val strippedRepoUrl = val strippedRepoUrl = repoUrl
repoUrl.removePrefix("https://").removePrefix("http://").removeSuffix("/") .removePrefix("https://")
.removePrefix("http://")
.removeSuffix("/")
.removeSuffix("/index.min.json")
val repoUrlParts = strippedRepoUrl.split("/") val repoUrlParts = strippedRepoUrl.split("/")
if (repoUrlParts.size < 3) { if (repoUrlParts.size < 3) {
return null return null

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/addRepositoryText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/add_repository"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/addRepositoryDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/add_repository_desc"
android:textSize="16sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<EditText
android:id="@+id/repositoryInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:imeOptions="actionDone"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel"
style="@style/Widget.MaterialComponents.Button.TextButton" />
<Button
android:id="@+id/addButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ok"
style="@style/Widget.MaterialComponents.Button" />
</LinearLayout>
<TextView
android:id="@+id/currentRepositoriesText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/current_repositories"
android:textSize="20sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/repositoriesRecyclerView"
tools:listitem="@layout/item_repo"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/extensionCardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<TextView
android:id="@+id/repoNameTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:fontFamily="@font/poppins_semi_bold"
android:text="@string/placeholder"
android:textSize="15sp" />
<ImageView
android:id="@+id/repoDeleteImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="10dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="3dp"
android:background="?android:attr/selectableItemBackground"
app:srcCompat="@drawable/ic_delete"
app:tint="?attr/colorOnBackground"
tools:ignore="ContentDescription" />
</LinearLayout>

View file

@ -1088,5 +1088,8 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
<string name="textview_sub">Textview Subtitles (Experimental)</string> <string name="textview_sub">Textview Subtitles (Experimental)</string>
<string name="textview_sub_stroke">Subtitle Stroke</string> <string name="textview_sub_stroke">Subtitle Stroke</string>
<string name="textview_sub_bottom_margin">Bottom Margin</string> <string name="textview_sub_bottom_margin">Bottom Margin</string>
<string name="add_repository">Add Repository</string>
<string name="add_repository_desc">A repository link should look like this: https://raw.githubusercontent.com/username/repo/branch/index.min.json</string>
<string name="current_repositories">Current Repositories</string>
</resources> </resources>

View file

@ -12,7 +12,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.7.0' classpath 'com.android.tools.build:gradle:8.7.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.google.devtools.ksp:symbol-processing-api:$ksp_version" classpath "com.google.devtools.ksp:symbol-processing-api:$ksp_version"