Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ All notable changes to this project are documented in this file.

The format is based on "Keep a Changelog" and follows semantic versioning where possible.

## 9.8.1
Date: 2026-08-16

### Highlights
- New mihon source settings feature
- Support for multiple extension repo
- Support for .pb files in extension manager

### Fixes
- Fixed extension manager updates error

### Maintenance
- Updated parsers and dependencies

## 9.8
Date: 2026-07-18

Expand Down
6 changes: 4 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ android {
applicationId 'io.github.landwarderer.futon'
minSdk = 23
targetSdk = 36
versionCode project.hasProperty('versionCode') ? project.property('versionCode').toInteger() : 90800
versionName project.hasProperty('versionName') ? project.property('versionName') : '9.8'
versionCode project.hasProperty('versionCode') ? project.property('versionCode').toInteger() : 90801
versionName project.hasProperty('versionName') ? project.property('versionName') : '9.8.1'
generatedDensities = []
testInstrumentationRunner 'io.github.landwarderer.futon.HiltTestRunner'
ksp {
Expand Down Expand Up @@ -203,6 +203,7 @@ dependencies {
implementation libs.okio
implementation libs.kotlinx.serialization.json
implementation libs.kotlinx.serialization.json.okio
implementation libs.kotlinx.serialization.protobuf

implementation libs.adapterdelegates
implementation libs.adapterdelegates.viewbinding
Expand Down Expand Up @@ -245,6 +246,7 @@ dependencies {
testImplementation libs.junit
testImplementation libs.json
testImplementation libs.kotlinx.coroutines.test
testImplementation 'org.mockito:mockito-core:5.12.0'

androidTestImplementation libs.androidx.runner
androidTestImplementation libs.androidx.rules
Expand Down
20 changes: 17 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="io.github.landwarderer.futon"
android:versionCode="90800"
android:versionName="9.8"
android:versionCode="90801"
android:versionName="9.8.1"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

Expand Down Expand Up @@ -295,7 +295,21 @@
android:label="@string/sources_catalog" />
<activity
android:name="io.github.landwarderer.futon.settings.sources.extension.ExtensionDownloaderActivity"
android:label="@string/extensions_manager" />
android:exported="true"
android:label="@string/extensions_manager">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mihon" android:host="extension-store" />
<data android:scheme="mihon" android:host="add-repo" />
<data android:scheme="tachiyomi" android:host="extension-repo" />
<data android:scheme="tachiyomi" android:host="add-repo" />
<data android:scheme="futon" android:host="extension-repo" />
<data android:scheme="futon" android:host="extension-store" />
<data android:scheme="futon" android:host="add-repo" />
</intent-filter>
</activity>
<activity
android:name="io.github.landwarderer.futon.scrobbling.kitsu.ui.KitsuAuthActivity"
android:exported="false"
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/kotlin/eu/kanade/tachiyomi/AppInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object AppInfo {
/**
* Version name of the host application.
*/
fun getVersionName(): String = BuildConfig.VERSION_NAME
fun getVersionName(): String = "0.20.1"

/**
* Supported image MIME types by the reader.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,5 @@ class DatabasePrePopulateCallback(private val resources: Resources) : RoomDataba
0L,
)
)

val now = System.currentTimeMillis()
db.execSQL(
"INSERT INTO external_extension_repos (type, baseUrl, name, shortName, website, signingKeyFingerprint, createdAt, updatedAt, lastSuccessAt) VALUES (?,?,?,?,?,?,?,?,?)",
arrayOf(
"MIHON",
"https://raw.githubusercontent.com/keiyoushi/extensions/refs/heads/repo",
"Keiyoushi",
"Keiyoushi",
"https://keiyoushi.github.io/extensions",
"508c909405615d0234a41316b230230559f6b9a89c3f15c13b306b38c2306f50",
now,
now,
now,
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ enum class ListItemType {
CHAPTER_LIST,
CHAPTER_GRID,
EXTENSION,
EXTENSION_REPO_HEADER,
EXTENSION_REPO_ITEM,
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class TypedListSpacingDecoration(
-> outRect.set(0)

ListItemType.HEADER,
ListItemType.EXTENSION_REPO_HEADER,
ListItemType.EXTENSION_REPO_ITEM,
ListItemType.FEED,
ListItemType.EXPLORE_SOURCE_LIST,
ListItemType.MANGA_SCROBBLING,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,68 +1,86 @@
package io.github.landwarderer.futon.mihon

import android.util.Log
import dalvik.system.PathClassLoader
import java.io.IOException
import java.io.InputStream
import java.net.URL
import java.util.Enumeration

/**
* A ClassLoader that loads classes from its own path before delegating to its parent.
*
* This is necessary for Mihon extensions because they may bundle different versions
* of libraries than App uses, and we need to isolate them.
* A parent-last class loader that will try in order:
* - the system class loader
* - the child class loader
* - the parent class loader.
*/
class ChildFirstPathClassLoader(
dexPath: String,
librarySearchPath: String?,
parent: ClassLoader,
) : PathClassLoader(dexPath, librarySearchPath, parent) {

/**
* List of packages that should always be loaded from the parent ClassLoader.
* These are core Android/Kotlin classes and Mihon API classes that must be shared.
*/
private val parentPackages = setOf(
"java.",
"javax.",
"kotlin.",
"kotlinx.",
"android.",
"androidx.",
"org.json.",
"org.jsoup.",
"okhttp3.",
"okio.",
"rx.",
"eu.kanade.tachiyomi.source.",
"eu.kanade.tachiyomi.network.",
"eu.kanade.tachiyomi.util.",
"uy.kohesive.injekt.",
"ireader.core.",
"io.ktor.",
"com.fleeksoft.",
)
private val systemClassLoader: ClassLoader? = getSystemClassLoader()

override fun loadClass(name: String, resolve: Boolean): Class<*> {
// Check if we should delegate to parent immediately
if (parentPackages.any { name.startsWith(it) }) {
override fun loadClass(name: String?, resolve: Boolean): Class<*> {
var c = findLoadedClass(name)

if (c == null && systemClassLoader != null) {
try {
return parent.loadClass(name)
} catch (e: ClassNotFoundException) {
// fall through to child loading
c = systemClassLoader.loadClass(name)
} catch (_: ClassNotFoundException) {}
}

if (c == null) {
c = try {
findClass(name)
} catch (_: ClassNotFoundException) {
super.loadClass(name, resolve)
}
}

// Try to find the class in our own path first
return try {
findLoadedClass(name) ?: findClass(name)
} catch (e: ClassNotFoundException) {
// Fall back to parent ClassLoader
try {
parent.loadClass(name)
} catch (e2: ClassNotFoundException) {
if (name.contains("tachiyomi")) {
Log.w("ChildFirstLoader", "Class not found: $name")
}
throw e2
if (resolve) {
resolveClass(c)
}

return c
}

override fun getResource(name: String?): URL? {
return systemClassLoader?.getResource(name)
?: findResource(name)
?: super.getResource(name)
}

override fun getResources(name: String?): Enumeration<URL> {
val systemUrls = systemClassLoader?.getResources(name)
val localUrls = findResources(name)
val parentUrls = parent?.getResources(name)
val urls = buildList {
while (systemUrls?.hasMoreElements() == true) {
add(systemUrls.nextElement())
}

while (localUrls?.hasMoreElements() == true) {
add(localUrls.nextElement())
}

while (parentUrls?.hasMoreElements() == true) {
add(parentUrls.nextElement())
}
}

return object : Enumeration<URL> {
val iterator = urls.iterator()

override fun hasMoreElements() = iterator.hasNext()
override fun nextElement() = iterator.next()
}
}

override fun getResourceAsStream(name: String?): InputStream? {
return try {
getResource(name)?.openStream()
} catch (_: IOException) {
return null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class MihonExtensionLoader @Inject constructor(

// Supported library version range
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.9
const val LIB_VERSION_MAX = 2.5

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package io.github.landwarderer.futon.mihon.compat

import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.network.NetworkHelper
import io.github.landwarderer.futon.core.network.webview.WebViewExecutor
import kotlinx.serialization.SerialFormat
Expand Down Expand Up @@ -58,6 +60,11 @@ class MihonInjektBridge(
addSingletonFactory<OkHttpClient> { httpClient }
addSingletonFactory<CookieJar> { cookieJar }

// SharedPreferences
addSingletonFactory<SharedPreferences> {
PreferenceManager.getDefaultSharedPreferences(context)
}

// JSON - explicitly type it to ensure Injekt matches correctly
val json = Json {
ignoreUnknownKeys = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,16 @@ class ExtensionInstallService @Inject constructor(
GitHubMirror.KKGITHUB -> url.replace("raw.githubusercontent.com", "raw.kkgithub.com")
GitHubMirror.GHPROXY -> "https://mirror.ghproxy.com/$url"
GitHubMirror.GHPROXY_NET -> "https://ghproxy.net/$url"
GitHubMirror.KEIYOUSHI -> url.replace("raw.githubusercontent.com", "raw.github.com")
GitHubMirror.KEIYOUSHI -> url.replace("raw.githubusercontent.com", "raw.github.com")
}
}
if (url.startsWith("https://github.com/")) {
return when (settings.gitHubMirror) {
GitHubMirror.NATIVE -> url
GitHubMirror.KKGITHUB -> url.replace("github.com", "kkgithub.com")
GitHubMirror.GHPROXY -> "https://mirror.ghproxy.com/$url"
GitHubMirror.GHPROXY_NET -> "https://ghproxy.net/$url"
GitHubMirror.KEIYOUSHI -> url
}
}
return url
Expand All @@ -55,7 +64,11 @@ class ExtensionInstallService @Inject constructor(
val downloadStates: StateFlow<Map<String, ExtensionInstallDownloadState>> = _downloadStates.asStateFlow()

suspend fun createInstallIntent(extension: RepoAvailableExtension): Intent? = withContext(Dispatchers.IO) {
val apkUrl = applyMirror("${extension.repoUrl}/apk/${extension.apkName}")
val apkUrl = if (extension.apkName.startsWith("http://") || extension.apkName.startsWith("https://")) {
applyMirror(extension.apkName)
} else {
applyMirror("${extension.repoUrl}/apk/${extension.apkName}")
}
val outputDir = File(context.cacheDir, "extension-installs").apply { mkdirs() }
val outputFile = File(outputDir, "${extension.pkgName}-${extension.versionCode}.apk")
val call = httpClient.newCachelessCallWithProgress(GET(apkUrl), ExtensionInstallProgressListener(extension.pkgName))
Expand Down
Loading
Loading