Simple login screen & Koin DI integration
This commit is contained in:
@@ -14,6 +14,14 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
// export correct artifact to use all classes of library directly from Swift
|
||||||
|
targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget::class.java).all {
|
||||||
|
binaries.withType(org.jetbrains.kotlin.gradle.plugin.mpp.Framework::class.java).all {
|
||||||
|
export("dev.icerock.moko:mvvm-core:0.16.1")
|
||||||
|
export("dev.icerock.moko:mvvm-state:0.16.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
androidTarget {
|
androidTarget {
|
||||||
compilations.all {
|
compilations.all {
|
||||||
compileTaskProvider {
|
compileTaskProvider {
|
||||||
@@ -58,6 +66,9 @@ kotlin {
|
|||||||
implementation(libs.multiplatformSettings)
|
implementation(libs.multiplatformSettings)
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
|
|
||||||
|
api("dev.icerock.moko:mvvm-compose:0.16.1")
|
||||||
|
api("dev.icerock.moko:mvvm-flow-compose:0.16.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
@@ -89,11 +100,11 @@ kotlin {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "dev.meloda.overseerr"
|
namespace = "dev.meloda.overseerr"
|
||||||
compileSdk = 35
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 34
|
||||||
|
|
||||||
applicationId = "dev.meloda.overseerr.androidApp"
|
applicationId = "dev.meloda.overseerr.androidApp"
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".common.AppGlobal"
|
||||||
android:icon="@android:drawable/ic_menu_compass"
|
android:icon="@android:drawable/ic_menu_compass"
|
||||||
android:label="Overseerr"
|
android:label="Overseerr"
|
||||||
android:theme="@android:style/Theme.Material.NoActionBar">
|
android:theme="@android:style/Theme.Material.NoActionBar">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".AppActivity"
|
android:name=".AppActivity"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||||
@@ -12,8 +14,8 @@
|
|||||||
android:windowSoftInputMode="adjustPan"
|
android:windowSoftInputMode="adjustPan"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package dev.meloda.overseerr.common
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import dev.meloda.overseerr.di.appModule
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
|
||||||
|
class AppGlobal : Application() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
modules(appModule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.meloda.overseerr.model
|
||||||
|
|
||||||
|
actual class Platform actual constructor() {
|
||||||
|
actual val name: String
|
||||||
|
get() = "Android"
|
||||||
|
}
|
||||||
@@ -1,100 +1,78 @@
|
|||||||
package dev.meloda.overseerr
|
package dev.meloda.overseerr
|
||||||
|
|
||||||
import androidx.compose.animation.core.*
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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 androidx.compose.ui.unit.dp
|
||||||
import overseerr.composeapp.generated.resources.*
|
import dev.meloda.overseerr.model.Platform
|
||||||
import dev.meloda.overseerr.theme.AppTheme
|
import dev.meloda.overseerr.theme.AppTheme
|
||||||
import dev.meloda.overseerr.theme.LocalThemeIsDark
|
import org.koin.compose.KoinContext
|
||||||
import kotlinx.coroutines.isActive
|
import org.koin.compose.koinInject
|
||||||
import org.jetbrains.compose.resources.Font
|
|
||||||
import org.jetbrains.compose.resources.stringResource
|
|
||||||
import org.jetbrains.compose.resources.vectorResource
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun App() = AppTheme {
|
internal fun App() = KoinContext {
|
||||||
Column(
|
val platform: Platform = koinInject()
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
AppTheme {
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
var loginValue by rememberSaveable {
|
||||||
.padding(16.dp),
|
mutableStateOf("")
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
}
|
||||||
) {
|
var passwordValue by rememberSaveable {
|
||||||
|
mutableStateOf("")
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(text = "Log in")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(Res.string.cyclone),
|
text = "Current platform: ${platform.name}",
|
||||||
fontFamily = FontFamily(Font(Res.font.IndieFlower_Regular)),
|
|
||||||
style = MaterialTheme.typography.displayLarge
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
) { padding ->
|
||||||
}
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
Image(
|
.padding(padding),
|
||||||
modifier = Modifier
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
.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))
|
TextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
|
value = loginValue,
|
||||||
|
onValueChange = { newText -> loginValue = newText },
|
||||||
|
placeholder = { Text(text = "Login") }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
|
value = passwordValue,
|
||||||
|
onValueChange = { newText -> passwordValue = newText },
|
||||||
|
placeholder = { Text(text = "Password") }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = "Authorize")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.meloda.overseerr.di
|
||||||
|
|
||||||
|
import dev.meloda.overseerr.model.Platform
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val appModule = module {
|
||||||
|
singleOf(::Platform)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.meloda.overseerr.login
|
||||||
|
|
||||||
|
import dev.icerock.moko.mvvm.viewmodel.ViewModel
|
||||||
|
|
||||||
|
class LoginViewModel : ViewModel() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package dev.meloda.overseerr.login.model
|
||||||
|
|
||||||
|
class LoginScreenState {
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package dev.meloda.overseerr.login.presentation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import dev.icerock.moko.mvvm.compose.getViewModel
|
||||||
|
import dev.meloda.overseerr.login.LoginViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginScreen(viewModel: LoginViewModel) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.meloda.overseerr.model
|
||||||
|
|
||||||
|
expect class Platform() {
|
||||||
|
val name: String
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.meloda.overseerr.model
|
||||||
|
|
||||||
|
actual class Platform actual constructor() {
|
||||||
|
actual val name: String
|
||||||
|
get() = "iOS"
|
||||||
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
import androidx.compose.ui.window.ComposeUIViewController
|
import androidx.compose.ui.window.ComposeUIViewController
|
||||||
import dev.meloda.overseerr.App
|
import dev.meloda.overseerr.App
|
||||||
|
import dev.meloda.overseerr.di.appModule
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
import platform.UIKit.UIViewController
|
import platform.UIKit.UIViewController
|
||||||
|
|
||||||
fun MainViewController(): UIViewController = ComposeUIViewController { App() }
|
fun MainViewController(): UIViewController = ComposeUIViewController {
|
||||||
|
startKoin {
|
||||||
|
modules(appModule)
|
||||||
|
}
|
||||||
|
App()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.meloda.overseerr.model
|
||||||
|
|
||||||
|
actual class Platform actual constructor() {
|
||||||
|
actual val name: String
|
||||||
|
get() = "JVM"
|
||||||
|
}
|
||||||
@@ -2,10 +2,16 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
import java.awt.Dimension
|
|
||||||
import dev.meloda.overseerr.App
|
import dev.meloda.overseerr.App
|
||||||
|
import dev.meloda.overseerr.di.appModule
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import java.awt.Dimension
|
||||||
|
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
|
startKoin {
|
||||||
|
modules(appModule)
|
||||||
|
}
|
||||||
|
|
||||||
Window(
|
Window(
|
||||||
title = "Overseerr",
|
title = "Overseerr",
|
||||||
state = rememberWindowState(width = 800.dp, height = 600.dp),
|
state = rememberWindowState(width = 800.dp, height = 600.dp),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ org.gradle.parallel=true
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
kotlin.js.compiler=ir
|
kotlin.js.compiler=ir
|
||||||
kotlin.daemon.jvmargs=-Xmx4G
|
kotlin.daemon.jvmargs=-Xmx4G
|
||||||
|
kotlin.native.ignoreDisabledTargets=true
|
||||||
|
|
||||||
#Android
|
#Android
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|||||||
+3
-1
@@ -14,6 +14,9 @@ pluginManagement {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
||||||
|
}
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositories {
|
repositories {
|
||||||
@@ -31,4 +34,3 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
include(":composeApp")
|
include(":composeApp")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user