feat: make repo adding easier
This commit is contained in:
parent
6f1bb10dec
commit
43dee6ee49
13 changed files with 383 additions and 346 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,6 +2,9 @@
|
|||
.gradle/
|
||||
build/
|
||||
|
||||
#kotlin
|
||||
.kotlin/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
|
|
|
@ -116,7 +116,8 @@
|
|||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/epub+zip" />
|
||||
<data android:host="*"/>
|
||||
<data android:mimeType="application/epub+zip"/>
|
||||
<data android:mimeType="application/x-mobipocket-ebook" />
|
||||
<data android:mimeType="application/vnd.amazon.ebook" />
|
||||
<data android:mimeType="application/fb2+zip" />
|
||||
|
@ -374,25 +375,31 @@
|
|||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.Main" />
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:pathPattern=".*\\.ani" />
|
||||
<data android:pathPattern=".*\\.sani" />
|
||||
<data android:host="*" />
|
||||
</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
|
||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
|
||||
|
|
|
@ -91,7 +91,6 @@ import androidx.viewpager2.widget.ViewPager2
|
|||
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
||||
import ani.dantotsu.connections.anilist.Genre
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.bakaupdates.MangaUpdates
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.ItemCountDownBinding
|
||||
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.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
||||
import ani.dantotsu.util.CountUpTimer
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.bumptech.glide.Glide
|
||||
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) {
|
||||
when {
|
||||
media.anime != null -> countDown(media, view)
|
||||
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
|
||||
else -> {} // No timer yet
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -116,58 +116,8 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
val action = intent.action
|
||||
val type = intent.type
|
||||
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")
|
||||
}
|
||||
if (Intent.ACTION_VIEW == intent.action) {
|
||||
handleViewIntent(intent)
|
||||
}
|
||||
|
||||
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
|
@ -492,6 +442,73 @@ class MainActivity : AppCompatActivity() {
|
|||
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) {
|
||||
val password = CharArray(16).apply { fill('0') }
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,7 +27,6 @@ import ani.dantotsu.restartApp
|
|||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
|
@ -117,14 +116,13 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
fun processUserInput(input: String, mediaType: MediaType, view: ViewGroup) {
|
||||
val entry =
|
||||
if (input.endsWith("/") || input.endsWith("index.min.json")) input.substring(
|
||||
0,
|
||||
input.lastIndexOf("/")
|
||||
) else input
|
||||
val validLink = if (input.contains("github.com") && input.contains("blob")) {
|
||||
input.replace("github.com", "raw.githubusercontent.com")
|
||||
.replace("/blob/", "/")
|
||||
} else input
|
||||
if (mediaType == MediaType.ANIME) {
|
||||
val anime =
|
||||
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(entry)
|
||||
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).plus(validLink)
|
||||
PrefManager.setVal(PrefName.AnimeExtensionRepos, anime)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
|
@ -133,7 +131,7 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
|||
}
|
||||
if (mediaType == MediaType.MANGA) {
|
||||
val manga =
|
||||
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(entry)
|
||||
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).plus(validLink)
|
||||
PrefManager.setVal(PrefName.MangaExtensionRepos, manga)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
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(
|
||||
arrayListOf(
|
||||
Settings(
|
||||
|
@ -169,31 +148,19 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
|||
desc = getString(R.string.anime_add_repository_desc),
|
||||
icon = R.drawable.ic_github,
|
||||
onClick = {
|
||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
||||
val editText = dialogView.userAgentTextBox.apply {
|
||||
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,
|
||||
it.attachView
|
||||
)
|
||||
val animeRepos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos)
|
||||
AddRepositoryBottomSheet.newInstance(
|
||||
MediaType.ANIME,
|
||||
animeRepos.toList(),
|
||||
onRepositoryAdded = { input, mediaType ->
|
||||
processUserInput(input, mediaType, it.attachView)
|
||||
},
|
||||
onRepositoryRemoved = { item ->
|
||||
val repos = PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).minus(item)
|
||||
PrefManager.setVal(PrefName.AnimeExtensionRepos, repos)
|
||||
setExtensionOutput(it.attachView, MediaType.ANIME)
|
||||
}
|
||||
setNegButton(getString(R.string.cancel))
|
||||
attach { dialog ->
|
||||
processEditorAction(
|
||||
dialog,
|
||||
editText,
|
||||
MediaType.ANIME,
|
||||
it.attachView
|
||||
)
|
||||
}
|
||||
show()
|
||||
}
|
||||
).show(supportFragmentManager, "add_repo")
|
||||
},
|
||||
attach = {
|
||||
setExtensionOutput(it.attachView, MediaType.ANIME)
|
||||
|
@ -205,31 +172,19 @@ class SettingsExtensionsActivity : AppCompatActivity() {
|
|||
desc = getString(R.string.manga_add_repository_desc),
|
||||
icon = R.drawable.ic_github,
|
||||
onClick = {
|
||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
||||
val editText = dialogView.userAgentTextBox.apply {
|
||||
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,
|
||||
it.attachView
|
||||
)
|
||||
val mangaRepos = PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos)
|
||||
AddRepositoryBottomSheet.newInstance(
|
||||
MediaType.MANGA,
|
||||
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)
|
||||
attach { dialog ->
|
||||
processEditorAction(
|
||||
dialog,
|
||||
editText,
|
||||
MediaType.MANGA,
|
||||
it.attachView
|
||||
)
|
||||
}
|
||||
}.show()
|
||||
|
||||
).show(supportFragmentManager, "add_repo")
|
||||
},
|
||||
attach = {
|
||||
setExtensionOutput(it.attachView, MediaType.MANGA)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -89,13 +89,6 @@ internal class ExtensionGithubApi {
|
|||
.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)
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Failed to get extensions from GitHub")
|
||||
|
@ -156,13 +149,18 @@ internal class ExtensionGithubApi {
|
|||
PrefManager.getVal<Set<String>>(PrefName.MangaExtensionRepos).toMutableList()
|
||||
|
||||
repos.forEach {
|
||||
val repoUrl = if (it.contains("index.min.json")) {
|
||||
it
|
||||
} else {
|
||||
"$it${if (it.endsWith('/')) "" else "/"}index.min.json"
|
||||
}
|
||||
try {
|
||||
val githubResponse = try {
|
||||
networkService.client
|
||||
.newCall(GET("${it}/index.min.json"))
|
||||
.newCall(GET(repoUrl))
|
||||
.awaitSuccess()
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Failed to get repo: $it")
|
||||
Logger.log("Failed to get repo: $repoUrl")
|
||||
Logger.log(e)
|
||||
null
|
||||
}
|
||||
|
@ -179,13 +177,6 @@ internal class ExtensionGithubApi {
|
|||
.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)
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Failed to get extensions from GitHub")
|
||||
|
@ -203,8 +194,11 @@ internal class ExtensionGithubApi {
|
|||
|
||||
private fun fallbackRepoUrl(repoUrl: String): String? {
|
||||
var fallbackRepoUrl = "https://gcore.jsdelivr.net/gh/"
|
||||
val strippedRepoUrl =
|
||||
repoUrl.removePrefix("https://").removePrefix("http://").removeSuffix("/")
|
||||
val strippedRepoUrl = repoUrl
|
||||
.removePrefix("https://")
|
||||
.removePrefix("http://")
|
||||
.removeSuffix("/")
|
||||
.removeSuffix("/index.min.json")
|
||||
val repoUrlParts = strippedRepoUrl.split("/")
|
||||
if (repoUrlParts.size < 3) {
|
||||
return null
|
||||
|
|
79
app/src/main/res/layout/bottom_sheet_add_repository.xml
Normal file
79
app/src/main/res/layout/bottom_sheet_add_repository.xml
Normal 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>
|
35
app/src/main/res/layout/item_repo.xml
Normal file
35
app/src/main/res/layout/item_repo.xml
Normal 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>
|
|
@ -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_stroke">Subtitle Stroke</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>
|
||||
|
|
|
@ -12,7 +12,7 @@ buildscript {
|
|||
}
|
||||
|
||||
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-serialization:$kotlin_version"
|
||||
classpath "com.google.devtools.ksp:symbol-processing-api:$ksp_version"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue