Initial commit

This commit is contained in:
2024-08-03 21:18:19 +03:00
commit ddca42e38e
37 changed files with 2170 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import com.android.build.api.dsl.ManagedVirtualDevice
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
plugins {
alias(libs.plugins.multiplatform)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.compose)
alias(libs.plugins.android.application)
alias(libs.plugins.kotlinx.serialization)
}
kotlin {
androidTarget {
compilations.all {
compileTaskProvider {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
//https://jakewharton.com/gradle-toolchains-are-rarely-a-good-idea/#what-do-i-do
freeCompilerArgs.add("-Xjdk-release=${JavaVersion.VERSION_17}")
}
}
}
//https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html
@OptIn(ExperimentalKotlinGradlePluginApi::class)
instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test)
}
jvm()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.voyager.navigator)
implementation(libs.coil)
implementation(libs.coil.network.ktor)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.ktor.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.multiplatformSettings)
implementation(libs.koin.core)
implementation(libs.koin.compose)
}
commonTest.dependencies {
implementation(kotlin("test"))
@OptIn(ExperimentalComposeLibrary::class)
implementation(compose.uiTest)
implementation(libs.kotlinx.coroutines.test)
}
androidMain.dependencies {
implementation(compose.uiTooling)
implementation(libs.androidx.activityCompose)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.ktor.client.okhttp)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
implementation(libs.ktor.client.okhttp)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}
android {
namespace = "dev.meloda.overseerr"
compileSdk = 35
defaultConfig {
minSdk = 26
targetSdk = 35
applicationId = "dev.meloda.overseerr.androidApp"
versionCode = 1
versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
//https://developer.android.com/studio/test/gradle-managed-devices
@Suppress("UnstableApiUsage")
testOptions {
managedDevices.devices {
maybeCreate<ManagedVirtualDevice>("pixel5").apply {
device = "Pixel 5"
apiLevel = 34
systemImageSource = "aosp"
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
//https://developer.android.com/develop/ui/compose/testing#setup
dependencies {
androidTestImplementation(libs.androidx.uitest.junit4)
debugImplementation(libs.androidx.uitest.testManifest)
//temporary fix: https://youtrack.jetbrains.com/issue/CMP-5864
androidTestImplementation("androidx.test:monitor") {
version { strictly("1.6.1") }
}
}
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "dev.meloda.overseerr.desktopApp"
packageVersion = "1.0.0"
}
}
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:icon="@android:drawable/ic_menu_compass"
android:label="Overseerr"
android:theme="@android:style/Theme.Material.NoActionBar">
<activity
android:name=".AppActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:launchMode="singleInstance"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,20 @@
package dev.meloda.overseerr
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
class AppActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent { App() }
}
}
@Preview
@Composable
fun AppPreview() { App() }
@@ -0,0 +1,19 @@
package dev.meloda.overseerr.theme
import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowInsetsControllerCompat
@Composable
internal actual fun SystemAppearance(isDark: Boolean) {
val view = LocalView.current
LaunchedEffect(isDark) {
val window = (view.context as Activity).window
WindowInsetsControllerCompat(window, window.decorView).apply {
isAppearanceLightStatusBars = isDark
isAppearanceLightNavigationBars = isDark
}
}
}
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4c0,2.21 1.79,4 4,4c2.21,0 4,-1.79 4,-4C16,9.79 14.21,8 12,8zM12,14c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C14,13.1 13.1,14 12,14z" />
<path
android:fillColor="#000000"
android:pathData="M22,7.47V5.35C20.05,4.77 16.56,4 12,4C9.85,4 7.89,4.86 6.46,6.24C6.59,5.39 6.86,3.84 7.47,2H5.35C4.77,3.95 4,7.44 4,12c0,2.15 0.86,4.11 2.24,5.54c-0.85,-0.14 -2.4,-0.4 -4.24,-1.01v2.12C3.95,19.23 7.44,20 12,20c2.15,0 4.11,-0.86 5.54,-2.24c-0.14,0.85 -0.4,2.4 -1.01,4.24h2.12C19.23,20.05 20,16.56 20,12c0,-2.15 -0.86,-4.11 -2.24,-5.54C18.61,6.59 20.16,6.86 22,7.47zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6s6,2.69 6,6S15.31,18 12,18z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9s9,-4.03 9,-9c0,-0.46 -0.04,-0.92 -0.1,-1.36c-0.98,1.37 -2.58,2.26 -4.4,2.26c-2.98,0 -5.4,-2.42 -5.4,-5.4c0,-1.81 0.89,-3.42 2.26,-4.4C12.92,3.04 12.46,3 12,3L12,3z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5S14.76,7 12,7L12,7zM2,13l2,0c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1l-2,0c-0.55,0 -1,0.45 -1,1S1.45,13 2,13zM20,13l2,0c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1l-2,0c-0.55,0 -1,0.45 -1,1S19.45,13 20,13zM11,2v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1V2c0,-0.55 -0.45,-1 -1,-1S11,1.45 11,2zM11,20v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1C11.45,19 11,19.45 11,20zM5.99,4.58c-0.39,-0.39 -1.03,-0.39 -1.41,0c-0.39,0.39 -0.39,1.03 0,1.41l1.06,1.06c0.39,0.39 1.03,0.39 1.41,0s0.39,-1.03 0,-1.41L5.99,4.58zM18.36,16.95c-0.39,-0.39 -1.03,-0.39 -1.41,0c-0.39,0.39 -0.39,1.03 0,1.41l1.06,1.06c0.39,0.39 1.03,0.39 1.41,0c0.39,-0.39 0.39,-1.03 0,-1.41L18.36,16.95zM19.42,5.99c0.39,-0.39 0.39,-1.03 0,-1.41c-0.39,-0.39 -1.03,-0.39 -1.41,0l-1.06,1.06c-0.39,0.39 -0.39,1.03 0,1.41s1.03,0.39 1.41,0L19.42,5.99zM7.05,18.36c0.39,-0.39 0.39,-1.03 0,-1.41c-0.39,-0.39 -1.03,-0.39 -1.41,0l-1.06,1.06c-0.39,0.39 -0.39,1.03 0,1.41s1.03,0.39 1.41,0L7.05,18.36z" />
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z" />
</vector>
@@ -0,0 +1,7 @@
<resources>
<string name="cyclone">Cyclone</string>
<string name="open_github">Open github</string>
<string name="run">Run</string>
<string name="stop">Stop</string>
<string name="theme">Theme</string>
</resources>
@@ -0,0 +1,100 @@
package dev.meloda.overseerr
import androidx.compose.animation.core.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import overseerr.composeapp.generated.resources.*
import dev.meloda.overseerr.theme.AppTheme
import dev.meloda.overseerr.theme.LocalThemeIsDark
import kotlinx.coroutines.isActive
import org.jetbrains.compose.resources.Font
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
@Composable
internal fun App() = AppTheme {
Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(Res.string.cyclone),
fontFamily = FontFamily(Font(Res.font.IndieFlower_Regular)),
style = MaterialTheme.typography.displayLarge
)
var isRotating by remember { mutableStateOf(false) }
val rotate = remember { Animatable(0f) }
val target = 360f
if (isRotating) {
LaunchedEffect(Unit) {
while (isActive) {
val remaining = (target - rotate.value) / target
rotate.animateTo(target, animationSpec = tween((1_000 * remaining).toInt(), easing = LinearEasing))
rotate.snapTo(0f)
}
}
}
Image(
modifier = Modifier
.size(250.dp)
.padding(16.dp)
.run { rotate(rotate.value) },
imageVector = vectorResource(Res.drawable.ic_cyclone),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
contentDescription = null
)
ElevatedButton(
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 4.dp)
.widthIn(min = 200.dp),
onClick = { isRotating = !isRotating },
content = {
Icon(vectorResource(Res.drawable.ic_rotate_right), contentDescription = null)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(
stringResource(if (isRotating) Res.string.stop else Res.string.run)
)
}
)
var isDark by LocalThemeIsDark.current
val icon = remember(isDark) {
if (isDark) Res.drawable.ic_light_mode
else Res.drawable.ic_dark_mode
}
ElevatedButton(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp).widthIn(min = 200.dp),
onClick = { isDark = !isDark },
content = {
Icon(vectorResource(icon), contentDescription = null)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(Res.string.theme))
}
)
val uriHandler = LocalUriHandler.current
TextButton(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp).widthIn(min = 200.dp),
onClick = { uriHandler.openUri("https://github.com/terrakok") },
) {
Text(stringResource(Res.string.open_github))
}
}
}
@@ -0,0 +1,71 @@
package dev.meloda.overseerr.theme
import androidx.compose.ui.graphics.Color
//generated by https://m3.material.io/theme-builder#/custom
//Color palette was taken here: https://colorhunt.co/palettes/popular
internal val md_theme_light_primary = Color(0xFF00687A)
internal val md_theme_light_onPrimary = Color(0xFFFFFFFF)
internal val md_theme_light_primaryContainer = Color(0xFFABEDFF)
internal val md_theme_light_onPrimaryContainer = Color(0xFF001F26)
internal val md_theme_light_secondary = Color(0xFF00696E)
internal val md_theme_light_onSecondary = Color(0xFFFFFFFF)
internal val md_theme_light_secondaryContainer = Color(0xFF6FF6FE)
internal val md_theme_light_onSecondaryContainer = Color(0xFF002022)
internal val md_theme_light_tertiary = Color(0xFF904D00)
internal val md_theme_light_onTertiary = Color(0xFFFFFFFF)
internal val md_theme_light_tertiaryContainer = Color(0xFFFFDCC2)
internal val md_theme_light_onTertiaryContainer = Color(0xFF2E1500)
internal val md_theme_light_error = Color(0xFFBA1A1A)
internal val md_theme_light_errorContainer = Color(0xFFFFDAD6)
internal val md_theme_light_onError = Color(0xFFFFFFFF)
internal val md_theme_light_onErrorContainer = Color(0xFF410002)
internal val md_theme_light_background = Color(0xFFFFFBFF)
internal val md_theme_light_onBackground = Color(0xFF221B00)
internal val md_theme_light_surface = Color(0xFFFFFBFF)
internal val md_theme_light_onSurface = Color(0xFF221B00)
internal val md_theme_light_surfaceVariant = Color(0xFFDBE4E7)
internal val md_theme_light_onSurfaceVariant = Color(0xFF3F484B)
internal val md_theme_light_outline = Color(0xFF70797B)
internal val md_theme_light_inverseOnSurface = Color(0xFFFFF0C0)
internal val md_theme_light_inverseSurface = Color(0xFF3A3000)
internal val md_theme_light_inversePrimary = Color(0xFF55D6F4)
internal val md_theme_light_shadow = Color(0xFF000000)
internal val md_theme_light_surfaceTint = Color(0xFF00687A)
internal val md_theme_light_outlineVariant = Color(0xFFBFC8CB)
internal val md_theme_light_scrim = Color(0xFF000000)
internal val md_theme_dark_primary = Color(0xFF55D6F4)
internal val md_theme_dark_onPrimary = Color(0xFF003640)
internal val md_theme_dark_primaryContainer = Color(0xFF004E5C)
internal val md_theme_dark_onPrimaryContainer = Color(0xFFABEDFF)
internal val md_theme_dark_secondary = Color(0xFF4CD9E2)
internal val md_theme_dark_onSecondary = Color(0xFF00373A)
internal val md_theme_dark_secondaryContainer = Color(0xFF004F53)
internal val md_theme_dark_onSecondaryContainer = Color(0xFF6FF6FE)
internal val md_theme_dark_tertiary = Color(0xFFFFB77C)
internal val md_theme_dark_onTertiary = Color(0xFF4D2700)
internal val md_theme_dark_tertiaryContainer = Color(0xFF6D3900)
internal val md_theme_dark_onTertiaryContainer = Color(0xFFFFDCC2)
internal val md_theme_dark_error = Color(0xFFFFB4AB)
internal val md_theme_dark_errorContainer = Color(0xFF93000A)
internal val md_theme_dark_onError = Color(0xFF690005)
internal val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
internal val md_theme_dark_background = Color(0xFF221B00)
internal val md_theme_dark_onBackground = Color(0xFFFFE264)
internal val md_theme_dark_surface = Color(0xFF221B00)
internal val md_theme_dark_onSurface = Color(0xFFFFE264)
internal val md_theme_dark_surfaceVariant = Color(0xFF3F484B)
internal val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CB)
internal val md_theme_dark_outline = Color(0xFF899295)
internal val md_theme_dark_inverseOnSurface = Color(0xFF221B00)
internal val md_theme_dark_inverseSurface = Color(0xFFFFE264)
internal val md_theme_dark_inversePrimary = Color(0xFF00687A)
internal val md_theme_dark_shadow = Color(0xFF000000)
internal val md_theme_dark_surfaceTint = Color(0xFF55D6F4)
internal val md_theme_dark_outlineVariant = Color(0xFF3F484B)
internal val md_theme_dark_scrim = Color(0xFF000000)
internal val seed = Color(0xFF2C3639)
@@ -0,0 +1,31 @@
package dev.meloda.overseerr.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.*
internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) }
@Composable
internal fun AppTheme(
content: @Composable () -> Unit
) {
val systemIsDark = isSystemInDarkTheme()
val isDarkState = remember { mutableStateOf(systemIsDark) }
CompositionLocalProvider(
LocalThemeIsDark provides isDarkState
) {
val isDark by isDarkState
SystemAppearance(!isDark)
MaterialTheme(
colorScheme = if (isDark) darkColorScheme() else lightColorScheme(),
content = { Surface(content = content) }
)
}
}
@Composable
internal expect fun SystemAppearance(isDark: Boolean)
@@ -0,0 +1,45 @@
package dev.meloda.overseerr
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.runComposeUiTest
import kotlin.test.Test
@OptIn(ExperimentalTestApi::class)
class ComposeTest {
@Test
fun simpleCheck() = runComposeUiTest {
setContent {
var txt by remember { mutableStateOf("Go") }
Column {
Text(
text = txt,
modifier = Modifier.testTag("t_text")
)
Button(
onClick = { txt += "." },
modifier = Modifier.testTag("t_button")
) {
Text("click me")
}
}
}
onNodeWithTag("t_button").apply {
repeat(3) { performClick() }
}
onNodeWithTag("t_text").assertTextEquals("Go...")
}
}
@@ -0,0 +1,17 @@
package dev.meloda.overseerr.theme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import platform.UIKit.UIApplication
import platform.UIKit.UIStatusBarStyleDarkContent
import platform.UIKit.UIStatusBarStyleLightContent
import platform.UIKit.setStatusBarStyle
@Composable
internal actual fun SystemAppearance(isDark: Boolean) {
LaunchedEffect(isDark) {
UIApplication.sharedApplication.setStatusBarStyle(
if (isDark) UIStatusBarStyleDarkContent else UIStatusBarStyleLightContent
)
}
}
+5
View File
@@ -0,0 +1,5 @@
import androidx.compose.ui.window.ComposeUIViewController
import dev.meloda.overseerr.App
import platform.UIKit.UIViewController
fun MainViewController(): UIViewController = ComposeUIViewController { App() }
@@ -0,0 +1,7 @@
package dev.meloda.overseerr.theme
import androidx.compose.runtime.Composable
@Composable
internal actual fun SystemAppearance(isDark: Boolean) {
}
+17
View File
@@ -0,0 +1,17 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import java.awt.Dimension
import dev.meloda.overseerr.App
fun main() = application {
Window(
title = "Overseerr",
state = rememberWindowState(width = 800.dp, height = 600.dp),
onCloseRequest = ::exitApplication,
) {
window.minimumSize = Dimension(350, 600)
App()
}
}