Skip to content

Commit 9ccce0b

Browse files
authored
Add support for mihon extensions (#52)
* feat: add mihon extension support * fix: adjust class loader * feat: implemented support for mihon extensions * fix: manga titles not showing * feat: add extension downloader activity * feat: hide button that opens extension downloader as it, will be enabled later * feat: add install and uninstall functionality for extensions * Update gradle.properties * fix: corrected case mismatch base_url was converted to the correct case baseUrl * fix: fixed explore tab crashing on open fixes sources(tachiyomi) returning null. MangaSource.getSummary(context) returned null for MihonMangaSource types * fix: Fetch Tachiyomi source icons Replace the previous white placeholder for MihonMangaRepository with a new fetchMihonIcon method that loads the extension's application icon via PackageManager. * feat: add search functionality for extensions in Extension Downloader * feat: add summary string for extensions manager in preferences
1 parent 2c5833d commit 9ccce0b

269 files changed

Lines changed: 12032 additions & 119 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.idea/gradle.xml

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,18 @@ dependencies {
173173
implementation libs.material
174174
implementation libs.androidx.lifecycle.common.java8
175175
implementation libs.androidx.webkit
176+
implementation libs.injekt.core
176177

177178
implementation libs.androidx.work.runtime
178179
implementation libs.guava
180+
implementation libs.quickjs.kt
179181

180182
// Foldable/Window layout
181183
implementation libs.androidx.window
182184

185+
implementation libs.rxjava
186+
implementation libs.rxandroid
187+
183188
implementation libs.androidx.room.runtime
184189
implementation libs.androidx.room.ktx
185190
ksp libs.androidx.room.compiler

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
2525
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
2626
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
27+
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
2728
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
2829
<uses-permission
2930
android:name="android.permission.QUERY_ALL_PACKAGES"
@@ -287,6 +288,9 @@
287288
<activity
288289
android:name="io.github.landwarderer.futon.settings.sources.catalog.SourcesCatalogActivity"
289290
android:label="@string/sources_catalog" />
291+
<activity
292+
android:name="io.github.landwarderer.futon.settings.sources.extension.ExtensionDownloaderActivity"
293+
android:label="@string/extensions_manager" />
290294
<activity
291295
android:name="io.github.landwarderer.futon.scrobbling.kitsu.ui.KitsuAuthActivity"
292296
android:exported="false"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package eu.kanade.tachiyomi
2+
3+
import io.github.landwarderer.futon.BuildConfig
4+
5+
/**
6+
* Stub class for Mihon extensions that reference AppInfo.
7+
* Extensions may call these methods for User-Agent strings or version checks.
8+
*
9+
* @since extension-lib 1.3
10+
*/
11+
@Suppress("UNUSED")
12+
object AppInfo {
13+
/**
14+
* Version code of the host application.
15+
*/
16+
fun getVersionCode(): Int = BuildConfig.VERSION_CODE
17+
18+
/**
19+
* Version name of the host application.
20+
*/
21+
fun getVersionName(): String = BuildConfig.VERSION_NAME
22+
23+
/**
24+
* Supported image MIME types by the reader.
25+
*/
26+
fun getSupportedImageMimeTypes(): List<String> = listOf(
27+
"image/jpeg",
28+
"image/png",
29+
"image/gif",
30+
"image/webp",
31+
"image/avif",
32+
"image/heif",
33+
"image/jxl",
34+
)
35+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package eu.kanade.tachiyomi.network
2+
3+
import android.content.Context
4+
import com.dokar.quickjs.QuickJs
5+
import kotlinx.coroutines.Dispatchers
6+
7+
/**
8+
* Util for evaluating JavaScript in sources.
9+
*
10+
* Uses QuickJS (with Rhino fallback) to execute JavaScript code.
11+
* This provides compatibility with Mihon extensions that use JavaScriptEngine.
12+
*
13+
* @since extensions-lib 1.4
14+
*/
15+
class JavaScriptEngine(private val context: Context) {
16+
17+
/**
18+
* Evaluate arbitrary JavaScript code and get the result as a primitive type
19+
* (e.g., String, Int).
20+
*
21+
* @param script JavaScript to execute.
22+
* @return Result of JavaScript code as a primitive type.
23+
*/
24+
@Suppress("UNCHECKED_CAST")
25+
suspend fun <T> evaluate(script: String): T {
26+
return QuickJs.create(jobDispatcher = Dispatchers.Default).use { qjs ->
27+
qjs.maxStackSize = 1L shl 20 // 1MB
28+
qjs.memoryLimit = 64L shl 20 // 64MB soft limit
29+
val result = qjs.evaluate<Any?>(script)
30+
result as T
31+
}
32+
}
33+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package eu.kanade.tachiyomi.network
2+
3+
import okhttp3.OkHttpClient
4+
5+
/**
6+
* Mihon-compatible NetworkHelper interface.
7+
* Provides access to OkHttpClient for extensions.
8+
*
9+
* This will be implemented by app to bridge with its existing network stack.
10+
*/
11+
abstract class NetworkHelper {
12+
13+
/**
14+
* The default OkHttpClient with CloudFlare bypassing.
15+
*/
16+
abstract val client: OkHttpClient
17+
18+
/**
19+
* @deprecated Since extension-lib 1.5
20+
*/
21+
@Deprecated("The regular client handles Cloudflare by default")
22+
open val cloudflareClient: OkHttpClient
23+
get() = client
24+
25+
/**
26+
* Returns the default user agent string.
27+
*/
28+
abstract fun defaultUserAgentProvider(): String
29+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package eu.kanade.tachiyomi.network
2+
3+
import kotlinx.coroutines.suspendCancellableCoroutine
4+
import okhttp3.Call
5+
import okhttp3.Callback
6+
import okhttp3.MediaType.Companion.toMediaType
7+
import okhttp3.OkHttpClient
8+
import okhttp3.Request
9+
import okhttp3.Response
10+
import rx.Observable
11+
import rx.Producer
12+
import rx.Subscription
13+
import java.io.IOException
14+
import java.util.concurrent.atomic.AtomicBoolean
15+
import kotlin.coroutines.resumeWithException
16+
17+
/**
18+
* OkHttp extension functions for Mihon compatibility.
19+
*/
20+
21+
val jsonMime = "application/json; charset=utf-8".toMediaType()
22+
23+
fun Call.asObservable(): Observable<Response> {
24+
return Observable.unsafeCreate { subscriber ->
25+
val call = clone()
26+
27+
val requestArbiter = object : Producer, Subscription {
28+
val boolean = AtomicBoolean(false)
29+
override fun request(n: Long) {
30+
if (n == 0L || !boolean.compareAndSet(false, true)) return
31+
32+
try {
33+
val response = call.execute()
34+
if (!subscriber.isUnsubscribed) {
35+
subscriber.onNext(response)
36+
subscriber.onCompleted()
37+
}
38+
} catch (e: Exception) {
39+
if (!subscriber.isUnsubscribed) {
40+
subscriber.onError(e)
41+
}
42+
}
43+
}
44+
45+
override fun unsubscribe() {
46+
call.cancel()
47+
}
48+
49+
override fun isUnsubscribed(): Boolean {
50+
return call.isCanceled()
51+
}
52+
}
53+
54+
subscriber.add(requestArbiter)
55+
subscriber.setProducer(requestArbiter)
56+
}
57+
}
58+
59+
fun Call.asObservableSuccess(): Observable<Response> {
60+
return asObservable().doOnNext { response ->
61+
if (!response.isSuccessful) {
62+
response.close()
63+
throw HttpException(response.code)
64+
}
65+
}
66+
}
67+
68+
suspend fun Call.await(): Response {
69+
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
70+
return suspendCancellableCoroutine { continuation ->
71+
val callback = object : Callback {
72+
override fun onResponse(call: Call, response: Response) {
73+
continuation.resume(response) {
74+
response.body.close()
75+
}
76+
}
77+
78+
override fun onFailure(call: Call, e: IOException) {
79+
if (continuation.isCancelled) return
80+
val exception = IOException(e.message, e).apply { stackTrace = callStack }
81+
continuation.resumeWithException(exception)
82+
}
83+
}
84+
85+
enqueue(callback)
86+
87+
continuation.invokeOnCancellation {
88+
try {
89+
cancel()
90+
} catch (ex: Throwable) {
91+
// Ignore cancel exception
92+
}
93+
}
94+
}
95+
}
96+
97+
/**
98+
* @since extensions-lib 1.5
99+
*/
100+
suspend fun Call.awaitSuccess(): Response {
101+
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
102+
val response = await()
103+
if (!response.isSuccessful) {
104+
response.close()
105+
throw HttpException(response.code).apply { stackTrace = callStack }
106+
}
107+
return response
108+
}
109+
110+
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
111+
val progressClient = newBuilder()
112+
.cache(null)
113+
.addNetworkInterceptor { chain ->
114+
val originalResponse = chain.proceed(chain.request())
115+
originalResponse.newBuilder()
116+
.body(ProgressResponseBody(originalResponse.body, listener))
117+
.build()
118+
}
119+
.build()
120+
121+
return progressClient.newCall(request)
122+
}
123+
124+
/**
125+
* Exception that handles HTTP codes considered not successful by OkHttp.
126+
* Use it to have a standardized error message in the app across the extensions.
127+
*
128+
* @since extensions-lib 1.5
129+
* @param code [Int] the HTTP status code
130+
*/
131+
class HttpException(val code: Int) : IllegalStateException("HTTP error $code")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package eu.kanade.tachiyomi.network
2+
3+
/**
4+
* Progress listener interface for tracking download progress.
5+
*/
6+
interface ProgressListener {
7+
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
8+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package eu.kanade.tachiyomi.network
2+
3+
import okhttp3.MediaType
4+
import okhttp3.ResponseBody
5+
import okio.Buffer
6+
import okio.BufferedSource
7+
import okio.ForwardingSource
8+
import okio.Source
9+
import okio.buffer
10+
11+
/**
12+
* ResponseBody wrapper that reports download progress.
13+
*/
14+
class ProgressResponseBody(
15+
private val responseBody: ResponseBody,
16+
private val progressListener: ProgressListener,
17+
) : ResponseBody() {
18+
19+
private val bufferedSource: BufferedSource by lazy {
20+
source(responseBody.source()).buffer()
21+
}
22+
23+
override fun contentType(): MediaType? {
24+
return responseBody.contentType()
25+
}
26+
27+
override fun contentLength(): Long {
28+
return responseBody.contentLength()
29+
}
30+
31+
override fun source(): BufferedSource {
32+
return bufferedSource
33+
}
34+
35+
private fun source(source: Source): Source {
36+
return object : ForwardingSource(source) {
37+
var totalBytesRead = 0L
38+
39+
override fun read(sink: Buffer, byteCount: Long): Long {
40+
val bytesRead = super.read(sink, byteCount)
41+
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
42+
progressListener.update(
43+
totalBytesRead,
44+
responseBody.contentLength(),
45+
bytesRead == -1L
46+
)
47+
return bytesRead
48+
}
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)