diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 873b469260..7aed59ddcb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -155,6 +155,7 @@ android { dependencies { implementation(projects.core) + implementation(projects.i18n) implementation(projects.sourceApi) // Compose diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 25187628be..b6f85365cb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -17,7 +17,6 @@ import android.net.wifi.WifiManager import android.os.Build import android.os.PowerManager import android.provider.Settings -import android.util.TypedValue import android.view.View import androidx.annotation.AttrRes import androidx.annotation.ColorInt @@ -103,30 +102,6 @@ fun Context.contextCompatDrawable(@DrawableRes resource: Int): Drawable? { return ContextCompat.getDrawable(this, resource) } -/** - * Converts to dp. - */ -val Int.pxToDp: Int - get() = (this / Resources.getSystem().displayMetrics.density).toInt() - -val Float.pxToDp: Float - get() = (this / Resources.getSystem().displayMetrics.density) - -/** - * Converts to px. - */ -val Int.dpToPx: Int - get() = this.toFloat().dpToPx.toInt() - -val Int.spToPx: Int - get() = this.toFloat().spToPx.toInt() - -val Float.spToPx: Float - get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics) - -val Float.dpToPx: Float - get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics) - /** Converts to px and takes into account LTR/RTL layout */ fun Float.dpToPxEnd(resources: Resources): Float { return this * resources.displayMetrics.density * if (resources.isLTR) 1 else -1 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 060454eb11..c05e49c91b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1116,12 +1116,6 @@ No file picker app found Failed to acquire persistent folder access. The app may behave unexpectedly. - - Failed to bypass Cloudflare - Please update the WebView app for better compatibility - - WebView is required for Tachiyomi - See your recently updated library entries Widget not available when app lock is enabled diff --git a/build.gradle.kts b/build.gradle.kts index 38914a27f1..7cbeb0930d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ buildscript { classpath(kotlinx.serialization.gradle) classpath(libs.firebase.crashlytics.gradle) classpath(libs.sqldelight.gradle) + classpath(libs.moko.generator) } repositories { gradlePluginPortal() diff --git a/buildSrc/src/main/kotlin/LocalesConfigPlugin.kt b/buildSrc/src/main/kotlin/LocalesConfigPlugin.kt new file mode 100644 index 0000000000..c6a13f7c42 --- /dev/null +++ b/buildSrc/src/main/kotlin/LocalesConfigPlugin.kt @@ -0,0 +1,41 @@ +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.TaskContainerScope + +fun TaskContainerScope.registerLocalesConfigTask(project: Project): TaskProvider { + return with(project) { + register("generateLocalesConfig") { + val emptyResourcesElement = "\\s*|".toRegex() + val valuesPrefix = "values-?".toRegex() + + val languages = fileTree("$projectDir/src/main/res/") + .matching { + include("**/strings.xml") + } + .filterNot { + it.readText().contains(emptyResourcesElement) + } + .map { it.parentFile.name } + .sorted() + .joinToString(separator = "\n") { + val language = it + .replace(valuesPrefix, "") + .replace("-r", "-") + .takeIf(String::isNotBlank) ?: "en" + " " + } + + val content = """ + + +$languages + + """.trimIndent() + + val localeFile = file("$projectDir/src/main/res/xml/locales_config.xml") + localeFile.parentFile.mkdirs() + localeFile.writeText(content) + } + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 02a4370ef1..cbfc6566c0 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -9,16 +9,18 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - api(libs.okhttp) - api(libs.okhttp.logging.interceptor) - api(libs.okhttp.dnsoverhttps) - api(libs.okhttp.brotli) - api(libs.okio) + implementation(projects.i18n) api(libs.bundles.logging) } } val androidMain by getting { dependencies { + api(libs.okhttp) + api(libs.okhttp.logging.interceptor) + api(libs.okhttp.dnsoverhttps) + api(libs.okhttp.brotli) + api(libs.okio) + api(androidx.core) api(androidx.annotation) api(libs.rxjava) diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index 2b7ea26e02..ca3d155395 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -5,7 +5,6 @@ import android.content.Context import android.webkit.WebView import android.widget.Toast import androidx.core.content.ContextCompat -import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.network.AndroidCookieJar import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.isOutdated @@ -15,6 +14,8 @@ import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response +import yokai.i18n.MR +import yokai.util.lang.getMString import java.io.IOException import java.util.concurrent.* @@ -48,7 +49,7 @@ class CloudflareInterceptor( // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that // we don't crash the entire app catch (e: CloudflareBypassException) { - throw IOException(context.getString(R.string.failed_to_bypass_cloudflare)) + throw IOException(context.getMString(MR.strings.failed_to_bypass_cloudflare)) } catch (e: Exception) { throw IOException(e) } @@ -130,7 +131,7 @@ class CloudflareInterceptor( if (!cloudflareBypassed) { // Prompt user to update WebView if it seems too outdated if (isWebViewOutdated) { - context.toast(R.string.please_update_webview, Toast.LENGTH_LONG) + context.toast(MR.strings.please_update_webview, Toast.LENGTH_LONG) } throw CloudflareBypassException() diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt index 1f366a189c..47018c7443 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt @@ -5,7 +5,6 @@ import android.os.Build import android.webkit.WebSettings import android.webkit.WebView import android.widget.Toast -import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.launchUI @@ -15,6 +14,7 @@ import okhttp3.Headers import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response +import yokai.i18n.MR import java.util.* import java.util.concurrent.* @@ -56,7 +56,7 @@ abstract class WebViewInterceptor( if (!WebViewUtil.supportsWebView(context)) { launchUI { - context.toast(R.string.webview_is_required, Toast.LENGTH_LONG) + context.toast(MR.strings.webview_is_required, Toast.LENGTH_LONG) } return response } diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DensityExtensions.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DensityExtensions.kt new file mode 100644 index 0000000000..2fc2ef21d4 --- /dev/null +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DensityExtensions.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.util.system + +import android.content.res.Resources +import android.util.TypedValue + +/** + * Converts to dp. + */ +val Int.pxToDp: Int + get() = (this / Resources.getSystem().displayMetrics.density).toInt() + +val Float.pxToDp: Float + get() = (this / Resources.getSystem().displayMetrics.density) + +/** + * Converts to px. + */ +val Int.dpToPx: Int + get() = this.toFloat().dpToPx.toInt() + +val Int.spToPx: Int + get() = this.toFloat().spToPx.toInt() + +val Float.spToPx: Float + get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics) + +val Float.dpToPx: Float + get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/DeviceUtil.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/util/system/DeviceUtil.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/DeviceUtil.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt index 6c04883652..1930e6eae7 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt @@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.util.system import android.content.Context import android.widget.Toast import androidx.annotation.StringRes +import dev.icerock.moko.resources.StringResource +import yokai.util.lang.getMString /** * Display a toast in this context. @@ -14,6 +16,16 @@ fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) Toast.makeText(this, resource, duration).show() } +/** + * Display a toast in this context. + * + * @param resource the text resource. + * @param duration the duration of the toast. Defaults to short. + */ +fun Context.toast(resource: StringResource, duration: Int = Toast.LENGTH_SHORT) { + toast(getMString(resource), duration) +} + /** * Display a toast in this context. * diff --git a/core/src/androidMain/kotlin/yokai/util/lang/MokoExtensions.kt b/core/src/androidMain/kotlin/yokai/util/lang/MokoExtensions.kt new file mode 100644 index 0000000000..4d431d9f6c --- /dev/null +++ b/core/src/androidMain/kotlin/yokai/util/lang/MokoExtensions.kt @@ -0,0 +1,8 @@ +package yokai.util.lang + +import android.content.Context +import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.desc.Resource +import dev.icerock.moko.resources.desc.StringDesc + +fun Context.getMString(stringRes: StringResource): String = StringDesc.Resource(stringRes).toString(this) diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt similarity index 75% rename from core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt rename to core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt index fc122c9538..8a1023b082 100644 --- a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt +++ b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt @@ -1,13 +1,10 @@ package eu.kanade.tachiyomi.network -import android.os.Build -import androidx.annotation.RequiresApi import eu.kanade.tachiyomi.core.preference.PreferenceStore class NetworkPreferences(private val preferenceStore: PreferenceStore) { fun dohProvider() = preferenceStore.getInt("doh_provider", -1) - @RequiresApi(Build.VERSION_CODES.GINGERBREAD) fun defaultUserAgent() = preferenceStore.getString("default_user_agent", NetworkHelper.DEFAULT_USER_AGENT) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6ef3370c25..9335bac840 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ chucker = "3.5.2" coil3 = "3.0.0-alpha06" flexible-adapter = "c8013533" fast_adapter = "5.6.0" +moko = "0.24.0" nucleus = "3.0.0" okhttp = "5.0.0-alpha.14" shizuku = "12.1.0" @@ -59,6 +60,10 @@ junit-android = { module = "androidx.test.ext:junit", version = "1.1.5" } junrar = { module = "com.github.junrar:junrar", version = "7.5.5" } loading-button = { module = "br.com.simplepass:loading-button-android", version = "2.2.0" } mockk = { module = "io.mockk:mockk", version = "1.13.11" } + +moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko" } +moko-generator = { module = "dev.icerock.moko:resources-generator", version.ref = "moko" } + okio = { module = "com.squareup.okio:okio", version = "3.9.0" } okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" } okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" } diff --git a/i18n/.gitignore b/i18n/.gitignore new file mode 100644 index 0000000000..4d99b14242 --- /dev/null +++ b/i18n/.gitignore @@ -0,0 +1,2 @@ +# Generated +locales_config.xml diff --git a/i18n/build.gradle.kts b/i18n/build.gradle.kts new file mode 100644 index 0000000000..69893be26c --- /dev/null +++ b/i18n/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") + id("dev.icerock.mobile.multiplatform-resources") +} + +kotlin { + androidTarget() + + applyDefaultHierarchyTemplate() + + sourceSets { + val commonMain by getting { + dependencies { + api(libs.moko.resources) + } + } + val androidMain by getting { + dependsOn(commonMain) + } + } +} + +android { + namespace = "yokai.i18n" + + sourceSets { + named("main") { + res.srcDir("src/commonMain/moko-resources") + } + } +} + +multiplatformResources { + resourcesPackage.set("yokai.i18n") +} + +tasks { + val localesConfigTask = registerLocalesConfigTask(project) + preBuild { + dependsOn(localesConfigTask) + } + + withType { + kotlinOptions.freeCompilerArgs += listOf( + "-Xexpect-actual-classes", + ) + } +} diff --git a/i18n/src/androidMain/AndroidManifest.xml b/i18n/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..8072ee00db --- /dev/null +++ b/i18n/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml new file mode 100644 index 0000000000..7b09b1a533 --- /dev/null +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -0,0 +1,8 @@ + + + + Failed to bypass Cloudflare + Please update the WebView app for better compatibility + + WebView is required for Tachiyomi + diff --git a/settings.gradle.kts b/settings.gradle.kts index 72192d1c8b..0be46720cc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,7 @@ rootProject.name = "Yokai" include(":app") include(":core") include(":domain") +include(":i18n") include(":presentation-core") include(":presentation-widget") include(":source-api")