()?.sourceSets?.removeAll {
- it.name in setOf(
- "androidAndroidTestRelease",
- )
- }
- }
-}
-
-tasks.register("stage") {
- dependsOn("server:shadowJar")
-}
diff --git a/clean.sh b/clean.sh
deleted file mode 100755
index c93c7155..00000000
--- a/clean.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-docker compose --file docker-compose.yml --env-file .env down
-docker compose --file docker-compose.yml --env-file .env rm
-
diff --git a/desktop-app/.gitignore b/desktop-app/.gitignore
deleted file mode 100644
index 327034ee..00000000
--- a/desktop-app/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-/build
-/release
-/debug
diff --git a/desktop-app/build.gradle.kts b/desktop-app/build.gradle.kts
deleted file mode 100644
index 3321ac68..00000000
--- a/desktop-app/build.gradle.kts
+++ /dev/null
@@ -1,52 +0,0 @@
-import org.jetbrains.compose.desktop.application.dsl.TargetFormat
-
-plugins {
- kotlin("multiplatform")
- id("org.jetbrains.compose")
- id("build-src-plugin")
-}
-
-kotlin {
- jvmToolchain(17)
-
- jvm()
-
- sourceSets {
- jvmMain.dependencies {
- implementation(project(":shared-client"))
- implementation(compose.desktop.common)
- implementation(compose.desktop.currentOs)
- }
- }
-}
-
-compose.desktop {
- application {
- mainClass = "ml.dev.kotlin.minigames.app.MainAppKt"
- version = VERSION
-
- nativeDistributions {
- targetFormats(TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Dmg)
- packageName = "MiniGames"
-
- windows {
- menu = true
- upgradeUuid = "e60c3562-48f8-47db-91d9-ca54dfa92f35"
- iconFile.set(projectDir.resolve("src/jvmMain/resources/ic_launcher.ico"))
- }
-
- linux {
- iconFile.set(projectDir.resolve("src/jvmMain/resources/ic_launcher.png"))
- }
-
- macOS {
- bundleID = "ml.dev.kotlin.minigames.app"
- appStore = false
- iconFile.set(projectDir.resolve("src/jvmMain/resources/ic_launcher.icns"))
- signing {
- sign.set(false)
- }
- }
- }
- }
-}
diff --git a/desktop-app/src/jvmMain/kotlin/ml/dev/kotlin/minigames/app/MainApp.kt b/desktop-app/src/jvmMain/kotlin/ml/dev/kotlin/minigames/app/MainApp.kt
deleted file mode 100644
index 3d95f860..00000000
--- a/desktop-app/src/jvmMain/kotlin/ml/dev/kotlin/minigames/app/MainApp.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package ml.dev.kotlin.minigames.app
-
-import ml.dev.kotlin.minigames.shared.mainDesktopApp
-
-fun main(): Unit = mainDesktopApp()
diff --git a/desktop-app/src/jvmMain/resources/ic_launcher.icns b/desktop-app/src/jvmMain/resources/ic_launcher.icns
deleted file mode 100644
index 899d3eb2..00000000
Binary files a/desktop-app/src/jvmMain/resources/ic_launcher.icns and /dev/null differ
diff --git a/desktop-app/src/jvmMain/resources/ic_launcher.ico b/desktop-app/src/jvmMain/resources/ic_launcher.ico
deleted file mode 100644
index 459a9c77..00000000
Binary files a/desktop-app/src/jvmMain/resources/ic_launcher.ico and /dev/null differ
diff --git a/desktop-app/src/jvmMain/resources/ic_launcher.png b/desktop-app/src/jvmMain/resources/ic_launcher.png
deleted file mode 100644
index d269256a..00000000
Binary files a/desktop-app/src/jvmMain/resources/ic_launcher.png and /dev/null differ
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 788b9f14..00000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-version: '3.8'
-
-services:
-
- mini-games-dev-server:
- build:
- dockerfile: ./Dockerfile
- context: .
- args:
- POSTGRES_PORT: ${POSTGRES_PORT}
- POSTGRES_DB: ${POSTGRES_DB}
- POSTGRES_USER: ${POSTGRES_USER}
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
- JDBC_DATABASE_URL: ${JDBC_DATABASE_URL}
- JDBC_DRIVER: ${JDBC_DRIVER}
- HOST: ${HOST}
- PORT: ${PORT}
- FORCE_HTTPS: ${FORCE_HTTPS}
- JWT_REALM: ${JWT_REALM}
- JWT_SECRET: ${JWT_SECRET}
- JWT_AUDIENCE: ${JWT_AUDIENCE}
- JWT_ISSUER: ${JWT_ISSUER}
- EMAIL_HOST: ${EMAIL_HOST}
- EMAIL_PORT: ${EMAIL_PORT}
- EMAIL_USERNAME: ${EMAIL_USERNAME}
- EMAIL_PASSWORD: ${EMAIL_PASSWORD}
- REQUIRE_EMAIL_VERIFY: ${REQUIRE_EMAIL_VERIFY}
- SCHEME_EMAIL_VERIFY: ${SCHEME_EMAIL_VERIFY}
- HOST_EMAIL_VERIFY: ${HOST_EMAIL_VERIFY}
- ports:
- - "${PORT}:${PORT}"
-
- mini-games-dev-postgres:
- image: postgres:alpine3.14
- environment:
- - POSTGRES_DB=${POSTGRES_DB}
- - POSTGRES_USER=${POSTGRES_USER}
- - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- ports:
- - "${POSTGRES_PORT}:${POSTGRES_PORT}"
diff --git a/gradle.properties b/gradle.properties
deleted file mode 100644
index d2e239bb..00000000
--- a/gradle.properties
+++ /dev/null
@@ -1,27 +0,0 @@
-kotlin.code.style=official
-
-org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
-android.useAndroidX=true
-android.enableJetifier=true
-
-xcodeproj=./ios-app
-
-org.jetbrains.compose.experimental.jscanvas.enabled=true
-org.jetbrains.compose.experimental.macos.enabled=true
-org.jetbrains.compose.experimental.uikit.enabled=true
-
-kotlin.native.cocoapods.generate.wrapper=true
-#kotlin.native.cacheKind=none
-kotlin.native.useEmbeddableCompilerJar=true
-kotlin.native.binary.memoryModel=experimental
-kotlin.mpp.androidSourceSetLayoutVersion=2
-kotlin.mpp.stability.nowarn=true
-kotlin.mpp.enableCInteropCommonization=true
-
-systemProp.kotlinVersion=1.9.21
-systemProp.composeVersion=1.6.0-alpha01
-systemProp.agpVersion=8.2.0
-systemProp.buildkonfigVersion=0.15.1
-systemProp.shadowVersion=7.1.2
-systemProp.parcelizeDarwinVersion=0.2.3
-systemProp.foojayResolverVersion=0.7.0
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 033e24c4..00000000
Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 9f4197d5..00000000
--- a/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
-networkTimeout=10000
-validateDistributionUrl=true
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
deleted file mode 100755
index fcb6fca1..00000000
--- a/gradlew
+++ /dev/null
@@ -1,248 +0,0 @@
-#!/bin/sh
-
-#
-# Copyright © 2015-2021 the original authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-##############################################################################
-#
-# Gradle start up script for POSIX generated by Gradle.
-#
-# Important for running:
-#
-# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
-# noncompliant, but you have some other compliant shell such as ksh or
-# bash, then to run this script, type that shell name before the whole
-# command line, like:
-#
-# ksh Gradle
-#
-# Busybox and similar reduced shells will NOT work, because this script
-# requires all of these POSIX shell features:
-# * functions;
-# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
-# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
-# * compound commands having a testable exit status, especially «case»;
-# * various built-in commands including «command», «set», and «ulimit».
-#
-# Important for patching:
-#
-# (2) This script targets any POSIX shell, so it avoids extensions provided
-# by Bash, Ksh, etc; in particular arrays are avoided.
-#
-# The "traditional" practice of packing multiple parameters into a
-# space-separated string is a well documented source of bugs and security
-# problems, so this is (mostly) avoided, by progressively accumulating
-# options in "$@", and eventually passing that to Java.
-#
-# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
-# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
-# see the in-line comments for details.
-#
-# There are tweaks for specific operating systems such as AIX, CygWin,
-# Darwin, MinGW, and NonStop.
-#
-# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
-# within the Gradle project.
-#
-# You can find Gradle at https://github.com/gradle/gradle/.
-#
-##############################################################################
-
-# Attempt to set APP_HOME
-
-# Resolve links: $0 may be a link
-app_path=$0
-
-# Need this for daisy-chained symlinks.
-while
- APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
- [ -h "$app_path" ]
-do
- ls=$( ls -ld "$app_path" )
- link=${ls#*' -> '}
- case $link in #(
- /*) app_path=$link ;; #(
- *) app_path=$APP_HOME$link ;;
- esac
-done
-
-# This is normally unused
-# shellcheck disable=SC2034
-APP_BASE_NAME=${0##*/}
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD=maximum
-
-warn () {
- echo "$*"
-} >&2
-
-die () {
- echo
- echo "$*"
- echo
- exit 1
-} >&2
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "$( uname )" in #(
- CYGWIN* ) cygwin=true ;; #(
- Darwin* ) darwin=true ;; #(
- MSYS* | MINGW* ) msys=true ;; #(
- NONSTOP* ) nonstop=true ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD=$JAVA_HOME/jre/sh/java
- else
- JAVACMD=$JAVA_HOME/bin/java
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD=java
- if ! command -v java >/dev/null 2>&1
- then
- die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-fi
-
-# Increase the maximum file descriptors if we can.
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
- case $MAX_FD in #(
- max*)
- # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
- MAX_FD=$( ulimit -H -n ) ||
- warn "Could not query maximum file descriptor limit"
- esac
- case $MAX_FD in #(
- '' | soft) :;; #(
- *)
- # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
- ulimit -n "$MAX_FD" ||
- warn "Could not set maximum file descriptor limit to $MAX_FD"
- esac
-fi
-
-# Collect all arguments for the java command, stacking in reverse order:
-# * args from the command line
-# * the main class name
-# * -classpath
-# * -D...appname settings
-# * --module-path (only if needed)
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if "$cygwin" || "$msys" ; then
- APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
-
- JAVACMD=$( cygpath --unix "$JAVACMD" )
-
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- for arg do
- if
- case $arg in #(
- -*) false ;; # don't mess with options #(
- /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
- [ -e "$t" ] ;; #(
- *) false ;;
- esac
- then
- arg=$( cygpath --path --ignore --mixed "$arg" )
- fi
- # Roll the args list around exactly as many times as the number of
- # args, so each arg winds up back in the position where it started, but
- # possibly modified.
- #
- # NB: a `for` loop captures its iteration list before it begins, so
- # changing the positional parameters here affects neither the number of
- # iterations, nor the values presented in `arg`.
- shift # remove old arg
- set -- "$@" "$arg" # push replacement arg
- done
-fi
-
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
-# Collect all arguments for the java command;
-# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-# shell script including quotes and variable substitutions, so put them in
-# double quotes to make sure that they get re-expanded; and
-# * put everything else in single quotes, so that it's not re-expanded.
-
-set -- \
- "-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
- "$@"
-
-# Stop when "xargs" is not available.
-if ! command -v xargs >/dev/null 2>&1
-then
- die "xargs is not available"
-fi
-
-# Use "xargs" to parse quoted args.
-#
-# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
-#
-# In Bash we could simply go:
-#
-# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
-# set -- "${ARGS[@]}" "$@"
-#
-# but POSIX shell has neither arrays nor command substitution, so instead we
-# post-process each arg (as a line of input to sed) to backslash-escape any
-# character that might be a shell metacharacter, then use eval to reverse
-# that process (while maintaining the separation between arguments), and wrap
-# the whole thing up as a single "set" statement.
-#
-# This will of course break if any of these variables contains a newline or
-# an unmatched quote.
-#
-
-eval "set -- $(
- printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
- xargs -n1 |
- sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
- tr '\n' ' '
- )" '"$@"'
-
-exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
deleted file mode 100644
index 93e3f59f..00000000
--- a/gradlew.bat
+++ /dev/null
@@ -1,92 +0,0 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..10c2a254
--- /dev/null
+++ b/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Hello World!
+
+
+ This is an example paragraph. Anything in the body tag will appear on the page, just like this p tag and its contents.
+
+
\ No newline at end of file
diff --git a/ios-app/Configuration/Config.xcconfig b/ios-app/Configuration/Config.xcconfig
deleted file mode 100644
index 20196c97..00000000
--- a/ios-app/Configuration/Config.xcconfig
+++ /dev/null
@@ -1,3 +0,0 @@
-TEAM_ID=macieekprocyk@gmail.com
-BUNDLE_ID=ml.dev.kotlin.MiniGames
-APP_NAME=Mini Games
diff --git a/ios-app/Podfile b/ios-app/Podfile
deleted file mode 100644
index 2052e08b..00000000
--- a/ios-app/Podfile
+++ /dev/null
@@ -1,5 +0,0 @@
-target 'iosApp' do
- use_frameworks!
- platform :ios, '15.2'
- pod 'shared_client', :path => '../shared-client'
-end
\ No newline at end of file
diff --git a/ios-app/iosApp.xcodeproj/project.pbxproj b/ios-app/iosApp.xcodeproj/project.pbxproj
deleted file mode 100644
index db90af93..00000000
--- a/ios-app/iosApp.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,421 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 53;
- objects = {
-
-/* Begin PBXBuildFile section */
- 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
- 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
- 999A4DF829E3FD3D0069883C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
- 99E785A429E1BADC007D6E89 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1049432C0C2B312090ABF6 /* Pods_iosApp.framework */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXFileReference section */
- 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
- 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
- 4FF3202A603A284706412EDC /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; };
- 6B1049432C0C2B312090ABF6 /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 7555FF7B242A565900829871 /* Mini Games.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mini Games.app"; sourceTree = BUILT_PRODUCTS_DIR; };
- 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
- 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
- FF8CA3F5360CEAB49D74065F /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- F85CB1118929364A9C6EFABC /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 99E785A429E1BADC007D6E89 /* Pods_iosApp.framework in Frameworks */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- 058557D7273AAEEB004C7B11 /* Preview Content */ = {
- isa = PBXGroup;
- children = (
- 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,
- );
- path = "Preview Content";
- sourceTree = "";
- };
- 42799AB246E5F90AF97AA0EF /* Frameworks */ = {
- isa = PBXGroup;
- children = (
- 6B1049432C0C2B312090ABF6 /* Pods_iosApp.framework */,
- );
- name = Frameworks;
- sourceTree = "";
- };
- 7555FF72242A565900829871 = {
- isa = PBXGroup;
- children = (
- AB1DB47929225F7C00F7AF9C /* Configuration */,
- 7555FF7D242A565900829871 /* iosApp */,
- 7555FF7C242A565900829871 /* Products */,
- FEFF387C0A8D172AA4D59CAE /* Pods */,
- 42799AB246E5F90AF97AA0EF /* Frameworks */,
- );
- sourceTree = "";
- };
- 7555FF7C242A565900829871 /* Products */ = {
- isa = PBXGroup;
- children = (
- 7555FF7B242A565900829871 /* Mini Games.app */,
- );
- name = Products;
- sourceTree = "";
- };
- 7555FF7D242A565900829871 /* iosApp */ = {
- isa = PBXGroup;
- children = (
- 058557BA273AAA24004C7B11 /* Assets.xcassets */,
- 7555FF82242A565900829871 /* ContentView.swift */,
- 7555FF8C242A565B00829871 /* Info.plist */,
- 2152FB032600AC8F00CF470E /* iOSApp.swift */,
- 058557D7273AAEEB004C7B11 /* Preview Content */,
- );
- path = iosApp;
- sourceTree = "";
- };
- AB1DB47929225F7C00F7AF9C /* Configuration */ = {
- isa = PBXGroup;
- children = (
- AB3632DC29227652001CCB65 /* Config.xcconfig */,
- );
- path = Configuration;
- sourceTree = "";
- };
- FEFF387C0A8D172AA4D59CAE /* Pods */ = {
- isa = PBXGroup;
- children = (
- 4FF3202A603A284706412EDC /* Pods-iosApp.debug.xcconfig */,
- FF8CA3F5360CEAB49D74065F /* Pods-iosApp.release.xcconfig */,
- );
- path = Pods;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- 7555FF7A242A565900829871 /* iosApp */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
- buildPhases = (
- 98D614C51D2DA07C614CC46E /* [CP] Check Pods Manifest.lock */,
- 7555FF77242A565900829871 /* Sources */,
- 7555FF79242A565900829871 /* Resources */,
- F85CB1118929364A9C6EFABC /* Frameworks */,
- 48A719D81F8C266AB8F40151 /* [CP] Copy Pods Resources */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = iosApp;
- productName = iosApp;
- productReference = 7555FF7B242A565900829871 /* Mini Games.app */;
- productType = "com.apple.product-type.application";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- 7555FF73242A565900829871 /* Project object */ = {
- isa = PBXProject;
- attributes = {
- BuildIndependentTargetsInParallel = YES;
- LastSwiftUpdateCheck = 1130;
- LastUpgradeCheck = 1430;
- ORGANIZATIONNAME = orgName;
- TargetAttributes = {
- 7555FF7A242A565900829871 = {
- CreatedOnToolsVersion = 11.3.1;
- };
- };
- };
- buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
- compatibilityVersion = "Xcode 9.3";
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- Base,
- );
- mainGroup = 7555FF72242A565900829871;
- productRefGroup = 7555FF7C242A565900829871 /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- 7555FF7A242A565900829871 /* iosApp */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- 7555FF79242A565900829871 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 999A4DF829E3FD3D0069883C /* Assets.xcassets in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
- 48A719D81F8C266AB8F40151 /* [CP] Copy Pods Resources */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist",
- );
- name = "[CP] Copy Pods Resources";
- outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n";
- showEnvVarsInLog = 0;
- };
- 98D614C51D2DA07C614CC46E /* [CP] Check Pods Manifest.lock */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- );
- inputPaths = (
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
- "${PODS_ROOT}/Manifest.lock",
- );
- name = "[CP] Check Pods Manifest.lock";
- outputFileListPaths = (
- );
- outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
- showEnvVarsInLog = 0;
- };
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- 7555FF77242A565900829871 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
- 7555FF83242A565900829871 /* ContentView.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin XCBuildConfiguration section */
- 7555FFA3242A565B00829871 /* Debug */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- GCC_C_LANGUAGE_STANDARD = gnu11;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 15.2;
- MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
- MTL_FAST_MATH = YES;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = iphoneos;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- };
- name = Debug;
- };
- 7555FFA4242A565B00829871 /* Release */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- GCC_C_LANGUAGE_STANDARD = gnu11;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 15.2;
- MTL_ENABLE_DEBUG_INFO = NO;
- MTL_FAST_MATH = YES;
- SDKROOT = iphoneos;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- VALIDATE_PRODUCT = YES;
- };
- name = Release;
- };
- 7555FFA6242A565B00829871 /* Debug */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 4FF3202A603A284706412EDC /* Pods-iosApp.debug.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
- CODE_SIGN_IDENTITY = "Apple Development";
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
- DEVELOPMENT_TEAM = S684J2AMV2;
- ENABLE_PREVIEWS = YES;
- INFOPLIST_FILE = iosApp/Info.plist;
- INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
- IPHONEOS_DEPLOYMENT_TARGET = 15.2;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = ml.dev.kotlin.MiniGames;
- PRODUCT_NAME = "${APP_NAME}";
- PROVISIONING_PROFILE_SPECIFIER = "";
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- 7555FFA7242A565B00829871 /* Release */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = FF8CA3F5360CEAB49D74065F /* Pods-iosApp.release.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
- CODE_SIGN_IDENTITY = "Apple Development";
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
- DEVELOPMENT_TEAM = S684J2AMV2;
- ENABLE_PREVIEWS = YES;
- INFOPLIST_FILE = iosApp/Info.plist;
- INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
- IPHONEOS_DEPLOYMENT_TARGET = 15.2;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = ml.dev.kotlin.MiniGames;
- PRODUCT_NAME = "${APP_NAME}";
- PROVISIONING_PROFILE_SPECIFIER = "";
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 7555FFA3242A565B00829871 /* Debug */,
- 7555FFA4242A565B00829871 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 7555FFA6242A565B00829871 /* Debug */,
- 7555FFA7242A565B00829871 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = 7555FF73242A565900829871 /* Project object */;
-}
diff --git a/ios-app/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/ios-app/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
deleted file mode 100644
index 10d25a7f..00000000
--- a/ios-app/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "colors" : [
- {
- "color" : {
- "color-space" : "display-p3",
- "components" : {
- "alpha" : "1.000",
- "blue" : "0.208",
- "green" : "0.208",
- "red" : "0.208"
- }
- },
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- },
- "properties" : {
- "localizable" : true
- }
-}
diff --git a/ios-app/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios-app/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index a657e336..00000000
--- a/ios-app/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "images" : [
- {
- "filename" : "icon.png",
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/ios-app/iosApp/Assets.xcassets/AppIcon.appiconset/icon.png b/ios-app/iosApp/Assets.xcassets/AppIcon.appiconset/icon.png
deleted file mode 100644
index 9fd53109..00000000
Binary files a/ios-app/iosApp/Assets.xcassets/AppIcon.appiconset/icon.png and /dev/null differ
diff --git a/ios-app/iosApp/Assets.xcassets/Contents.json b/ios-app/iosApp/Assets.xcassets/Contents.json
deleted file mode 100644
index 4aa7c535..00000000
--- a/ios-app/iosApp/Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
\ No newline at end of file
diff --git a/ios-app/iosApp/ContentView.swift b/ios-app/iosApp/ContentView.swift
deleted file mode 100644
index ddbd35bd..00000000
--- a/ios-app/iosApp/ContentView.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-import UIKit
-import SwiftUI
-import shared_client
-
-struct ComposeView: UIViewControllerRepresentable {
-
- let component: MiniGamesAppComponent
-
- func makeUIViewController(context: Context) -> UIViewController {
- Main_iosKt.MainViewController(component: component)
- }
-
- func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
-}
-
-struct ContentView: View {
-
- let component: MiniGamesAppComponent
-
- @Environment(\.colorScheme) var colorScheme
-
- var body: some View {
- ComposeView(component: component)
- .ignoresSafeArea(.all)
- }
-}
-
diff --git a/ios-app/iosApp/Info.plist b/ios-app/iosApp/Info.plist
deleted file mode 100644
index 9f96d054..00000000
--- a/ios-app/iosApp/Info.plist
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- $(PRODUCT_BUNDLE_PACKAGE_TYPE)
- CFBundleShortVersionString
- 1.0
- CFBundleVersion
- 1
- LSRequiresIPhoneOS
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
- UIApplicationSceneManifest
-
- UIApplicationSupportsMultipleScenes
-
-
- UILaunchScreen
-
- UIRequiredDeviceCapabilities
-
- armv7
-
- UIStatusBarStyle
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
-
-
diff --git a/ios-app/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/ios-app/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
deleted file mode 100644
index 4aa7c535..00000000
--- a/ios-app/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
\ No newline at end of file
diff --git a/ios-app/iosApp/iOSApp.swift b/ios-app/iosApp/iOSApp.swift
deleted file mode 100644
index 9be02a25..00000000
--- a/ios-app/iosApp/iOSApp.swift
+++ /dev/null
@@ -1,82 +0,0 @@
-import SwiftUI
-import shared_client
-
-@main
-struct iOSApp: App {
-
- @UIApplicationDelegateAdaptor(AppDelegate.self)
- var appDelegate: AppDelegate
-
- private var rootHolder: RootHolder { appDelegate.getRootHolder() }
-
- var body: some Scene {
- WindowGroup {
- ContentView(component: rootHolder.root)
- .ignoresSafeArea(.all)
- .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
- LifecycleRegistryExtKt.resume(rootHolder.lifecycle)
- }
- .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
- LifecycleRegistryExtKt.pause(rootHolder.lifecycle)
- }
- .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
- LifecycleRegistryExtKt.stop(rootHolder.lifecycle)
- }
- .onReceive(NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification)) { _ in
- LifecycleRegistryExtKt.destroy(rootHolder.lifecycle)
- }
- }
- }
-}
-
-private let STATE_KEY: String = "mini-games-saved-state"
-
-class AppDelegate: NSObject, UIApplicationDelegate {
-
- private var rootHolder: RootHolder?
-
- func application(_ application: UIApplication, shouldSaveSecureApplicationState coder: NSCoder) -> Bool {
- StateKeeperUtilsKt.save(coder: coder, state: rootHolder!.stateKeeper.save())
- return true
- }
-
- func application(_ application: UIApplication, shouldRestoreSecureApplicationState coder: NSCoder) -> Bool {
- do {
- let savedState = StateKeeperUtilsKt.restore(coder: coder)
- rootHolder = RootHolder(savedState: savedState)
- return true
- } catch {
- return false
- }
- }
-
- fileprivate func getRootHolder() -> RootHolder {
- if (rootHolder == nil) {
- rootHolder = RootHolder(savedState: nil)
- }
- return rootHolder!
- }
-}
-
-private class RootHolder {
- let lifecycle: LifecycleRegistry
- let stateKeeper: StateKeeperDispatcher
- let root: MiniGamesAppComponent
-
- init(savedState: SerializableContainer?) {
- lifecycle = LifecycleRegistryKt.LifecycleRegistry()
- stateKeeper = StateKeeperDispatcherKt.StateKeeperDispatcher(savedState: savedState)
-
- root = MiniGamesAppComponentImpl(
- appContext: MiniGamesAppComponentContext(),
- componentContext: DefaultComponentContext(
- lifecycle: lifecycle,
- stateKeeper: stateKeeper,
- instanceKeeper: nil,
- backHandler: nil
- )
- )
-
- LifecycleRegistryExtKt.create(lifecycle)
- }
-}
diff --git a/server/.gitignore b/server/.gitignore
deleted file mode 100644
index 42afabfd..00000000
--- a/server/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/server/build.gradle.kts b/server/build.gradle.kts
deleted file mode 100644
index ba1041f9..00000000
--- a/server/build.gradle.kts
+++ /dev/null
@@ -1,76 +0,0 @@
-import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
-plugins {
- kotlin("multiplatform")
- id("org.gradle.java")
- id("com.github.johnrengelman.shadow")
- id("build-src-plugin")
-}
-
-group = "ml.dev.kotlin.minigames"
-version = VERSION
-
-kotlin {
- jvm()
-
- sourceSets {
- jvmMain.dependencies {
- implementation(project(":shared"))
-
- implementation(Dependencies.ktorServerCore)
- implementation(Dependencies.ktorServerNetty)
- implementation(Dependencies.ktorServerSerialization)
- implementation(Dependencies.ktorServerWebsockets)
- implementation(Dependencies.ktorServerContentNegotiation)
- implementation(Dependencies.ktorServerAuth)
- implementation(Dependencies.ktorServerAuthJwt)
- implementation(Dependencies.ktorServerHtmlBuilder)
- implementation(Dependencies.logbackClassic)
-
- implementation(Dependencies.simpleMailCore)
- implementation(Dependencies.simpleMailClient)
-
- implementation(Dependencies.bCrypt)
-
- implementation(Dependencies.exposedCore)
- implementation(Dependencies.exposedDao)
- implementation(Dependencies.exposedJdbc)
- implementation(Dependencies.exposedJavaTime)
- implementation(Dependencies.postgresSqlDriver)
- }
- }
-}
-
-java {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
-}
-
-tasks.withType {
- kotlinOptions {
- jvmTarget = "17"
- }
-}
-
-tasks.withType {
- sourceCompatibility = "${JavaVersion.VERSION_17}"
- targetCompatibility = "${JavaVersion.VERSION_17}"
-}
-
-val shadowJarTasks = tasks.withType {
- manifest {
- attributes("Main-Class" to "ml.dev.kotlin.minigames.server.ServerKt")
- }
- archiveClassifier.set("all")
- val main by kotlin.jvm().compilations
- from(main.output)
- configurations += main.compileDependencyFiles as Configuration
- configurations += main.runtimeDependencyFiles as Configuration
-}
-
-tasks.create("run") {
- dependsOn(shadowJarTasks)
- mainClass.set("-jar")
- args = listOf("${buildDir.resolve("libs").resolve("server-$version-all.jar")}")
-}
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/db/DbSettings.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/db/DbSettings.kt
deleted file mode 100644
index cd826ccc..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/db/DbSettings.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package ml.dev.kotlin.minigames.db
-
-import kotlinx.coroutines.CoroutineDispatcher
-import ml.dev.kotlin.minigames.util.envVar
-import org.jetbrains.exposed.sql.Database
-import org.jetbrains.exposed.sql.SqlLogger
-import org.jetbrains.exposed.sql.Transaction
-import org.jetbrains.exposed.sql.addLogger
-import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.jetbrains.exposed.sql.transactions.transactionManager
-
-object DbSettings {
-
- val db by lazy {
- Database.connect(
- url = envVar("JDBC_DATABASE_URL"),
- driver = envVar("JDBC_DRIVER"),
- )
- }
-
- val defaultLogger: SqlLogger? = null
-}
-
-fun txn(
- transactionIsolation: Int = DbSettings.db.transactionManager.defaultIsolationLevel,
- readOnly: Boolean = false,
- logger: SqlLogger? = DbSettings.defaultLogger,
- statement: Transaction.() -> T,
-): T = transaction(transactionIsolation, readOnly, DbSettings.db) {
- logger?.let { addLogger(it) }
- statement()
-}
-
-suspend fun suspendedTxn(
- context: CoroutineDispatcher? = null,
- transactionIsolation: Int = DbSettings.db.transactionManager.defaultIsolationLevel,
- logger: SqlLogger? = DbSettings.defaultLogger,
- statement: Transaction.() -> T,
-): T = newSuspendedTransaction(context, DbSettings.db, transactionIsolation) {
- logger?.let { addLogger(it) }
- statement()
-}
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/db/model/BaseUUID.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/db/model/BaseUUID.kt
deleted file mode 100644
index 8a2b3356..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/db/model/BaseUUID.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package ml.dev.kotlin.minigames.db.model
-
-import org.jetbrains.exposed.dao.*
-import org.jetbrains.exposed.dao.id.EntityID
-import org.jetbrains.exposed.dao.id.UUIDTable
-import org.jetbrains.exposed.sql.javatime.timestamp
-import java.time.Instant
-import java.util.*
-
-abstract class BaseUUIDTable(name: String) : UUIDTable(name) {
- val createdAt = timestamp("created_at").clientDefault(Instant::now)
- val updatedAt = timestamp("updated_at").nullable()
-}
-
-abstract class BaseUUIDEntity(id: EntityID, table: BaseUUIDTable) : UUIDEntity(id) {
- val createdAt by table.createdAt
- var updatedAt by table.updatedAt
-}
-
-abstract class BaseUUIDEntityClass(table: BaseUUIDTable) : UUIDEntityClass(table) {
- init {
- EntityHook.subscribe { action ->
- if (action.changeType == EntityChangeType.Updated) {
- try {
- action.toEntity(this)?.updatedAt = Instant.now()
- } catch (_: Exception) {
- }
- }
- }
- }
-}
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/db/model/User.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/db/model/User.kt
deleted file mode 100644
index adff2ae0..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/db/model/User.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package ml.dev.kotlin.minigames.db.model
-
-import org.jetbrains.exposed.dao.id.EntityID
-import org.jetbrains.exposed.sql.ResultRow
-import java.util.*
-
-object UsersTable : BaseUUIDTable("users") {
- val email = text("email").uniqueIndex()
- val username = text("username").uniqueIndex()
- val passwordHash = text("password_hash").index()
- val confirmed = bool("confirmed").default(false)
- val confirmHash = text("confirm_hash")
-}
-
-class UserEntity(id: EntityID) : BaseUUIDEntity(id, UsersTable) {
- companion object : BaseUUIDEntityClass(UsersTable)
-
- var email by UsersTable.email
- var username by UsersTable.username
- var passwordHash by UsersTable.passwordHash
- var confirmed by UsersTable.confirmed
- var confirmHash by UsersTable.confirmHash
-}
-
-fun ResultRow.toUserEntity() = UserEntity(this[UsersTable.id]).also {
- it.email = this[UsersTable.email]
- it.username = this[UsersTable.username]
- it.passwordHash = this[UsersTable.passwordHash]
- it.confirmed = this[UsersTable.confirmed]
- it.confirmHash = this[UsersTable.confirmHash]
-}
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/AppConfiguration.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/AppConfiguration.kt
deleted file mode 100644
index b3d685db..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/AppConfiguration.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package ml.dev.kotlin.minigames.server
-
-import com.auth0.jwt.JWT
-import com.auth0.jwt.algorithms.Algorithm
-import io.ktor.serialization.kotlinx.cbor.*
-import io.ktor.server.application.*
-import io.ktor.server.auth.*
-import io.ktor.server.auth.jwt.*
-import io.ktor.server.plugins.contentnegotiation.*
-import io.ktor.server.websocket.*
-import kotlinx.serialization.ExperimentalSerializationApi
-
-@OptIn(ExperimentalSerializationApi::class)
-fun Application.installCbor() = install(ContentNegotiation) { cbor() }
-
-fun Application.installWebSockets() = install(WebSockets)
-
-fun Application.installJWTAuth() = install(Authentication) {
- jwt(Jwt.CONFIG) {
- realm = Jwt.REALM
- verifier(
- JWT.require(Algorithm.HMAC256(Jwt.SECRET))
- .withAudience(Jwt.AUDIENCE)
- .withIssuer(Jwt.ISSUER)
- .build()
- )
- validate { credential ->
- val username = credential.payload.getClaim(Jwt.CLAIM).asString()
- username.takeUnless { it.isNullOrBlank() }?.let(Jwt::User)
- }
- }
-}
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/Jwt.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/Jwt.kt
deleted file mode 100644
index 866aeb67..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/Jwt.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package ml.dev.kotlin.minigames.server
-
-import com.auth0.jwt.JWT
-import com.auth0.jwt.algorithms.Algorithm
-import io.ktor.server.auth.*
-import ml.dev.kotlin.minigames.shared.model.JwtToken
-import ml.dev.kotlin.minigames.util.envVar
-import java.util.*
-
-object Jwt {
- val AUDIENCE: String = envVar("JWT_AUDIENCE")
- val REALM: String = envVar("JWT_REALM")
- val SECRET: String = envVar("JWT_SECRET")
- val ISSUER: String = envVar("JWT_ISSUER")
- const val CLAIM = "username"
- const val CONFIG = "jwt-auth"
- private const val EXPIRE_IN_MILLIS = 60_000
-
- fun generateToken(username: String): JwtToken = JWT.create()
- .withAudience(AUDIENCE)
- .withIssuer(ISSUER)
- .withClaim(CLAIM, username)
- .withExpiresAt(Date(System.currentTimeMillis() + EXPIRE_IN_MILLIS))
- .sign(Algorithm.HMAC256(SECRET))
- .let(::JwtToken)
-
- data class User(val username: String) : Principal
-}
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/Server.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/Server.kt
deleted file mode 100644
index 6627427e..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/Server.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package ml.dev.kotlin.minigames.server
-
-import io.ktor.server.application.*
-import io.ktor.server.engine.*
-import io.ktor.server.netty.*
-import ml.dev.kotlin.minigames.db.model.UsersTable
-import ml.dev.kotlin.minigames.db.txn
-import ml.dev.kotlin.minigames.server.routes.gameSockets
-import ml.dev.kotlin.minigames.server.routes.userRoutes
-import ml.dev.kotlin.minigames.server.routes.webRoutes
-import ml.dev.kotlin.minigames.util.envVar
-import ml.dev.kotlin.minigames.util.eprintln
-import org.jetbrains.exposed.sql.SchemaUtils.createMissingTablesAndColumns
-
-fun main() {
- try {
- txn { createMissingTablesAndColumns(UsersTable) }
-
- embeddedServer(
- factory = Netty,
- host = envVar("HOST"),
- port = envVar("PORT"),
- watchPaths = emptyList(),
- module = Application::gameServiceModule
- ).start(wait = true)
- } catch (e: Exception) {
- eprintln(e)
- }
-}
-
-private fun Application.gameServiceModule() {
- installCbor()
- installWebSockets()
- installJWTAuth()
-
- userRoutes()
- webRoutes()
- gameSockets()
-}
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/routes/GameRoutes.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/routes/GameRoutes.kt
deleted file mode 100644
index fef568ba..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/routes/GameRoutes.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package ml.dev.kotlin.minigames.server.routes
-
-import io.ktor.server.application.*
-import io.ktor.server.routing.*
-import io.ktor.server.websocket.*
-import io.ktor.util.*
-import io.ktor.websocket.*
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.decodeFromByteArray
-import ml.dev.kotlin.minigames.server.Jwt
-import ml.dev.kotlin.minigames.service.GameConnection
-import ml.dev.kotlin.minigames.service.GameServerName
-import ml.dev.kotlin.minigames.service.GameService
-import ml.dev.kotlin.minigames.shared.api.BIRD_GAME_WEBSOCKET
-import ml.dev.kotlin.minigames.shared.api.GamePath
-import ml.dev.kotlin.minigames.shared.api.SET_GAME_WEBSOCKET
-import ml.dev.kotlin.minigames.shared.api.SNAKE_GAME_WEBSOCKET
-import ml.dev.kotlin.minigames.shared.model.*
-import ml.dev.kotlin.minigames.shared.util.GameSerialization
-import ml.dev.kotlin.minigames.util.StringValuesKey
-import ml.dev.kotlin.minigames.util.authJwtWebSocket
-import ml.dev.kotlin.minigames.util.eprintln
-import ml.dev.kotlin.minigames.util.get
-
-private val SET_GAME_HANDLER = GameService { SetGameState.random() }.let(::GameHandler)
-private val SNAKE_GAME_HANDLER = GameService(updateDelay = 30) { SnakeGameState.empty() }.let(::GameHandler)
-private val BIRD_GAME_HANDLER = GameService(updateDelay = 30) { BirdGameState.empty() }.let(::GameHandler)
-
-fun Application.gameSockets() = routing {
- authJwtGameHandlerWebSockets(SET_GAME_WEBSOCKET, SET_GAME_HANDLER)
- authJwtGameHandlerWebSockets(SNAKE_GAME_WEBSOCKET, SNAKE_GAME_HANDLER)
- authJwtGameHandlerWebSockets(BIRD_GAME_WEBSOCKET, BIRD_GAME_HANDLER)
-}
-
-@KtorDsl
-private fun Route.authJwtGameHandlerWebSockets(
- gamePathSource: (String) -> GamePath,
- handler: GameHandler,
-) {
- val gamePath = gamePathSource("{$SERVER_NAME}")
- authJwtWebSocket(gamePath.statePath, handler::handleState)
- authJwtWebSocket(gamePath.dataPath, handler::handleData)
-}
-
-private class GameHandler(
- private val service: GameService,
-) {
- @OptIn(ExperimentalSerializationApi::class)
- suspend fun handleState(
- session: DefaultWebSocketServerSession,
- user: Jwt.User,
- ): Unit = with(session) {
- val connection = GameConnection.State(session, user.username)
- val serverName = call.parameters[SERVER_NAME]?.let(::GameServerName) ?: return
- service.addStateConnection(serverName, connection)
-
- try {
- val initialState = service.state(serverName)
- service.sendAllUpdatedGameState(serverName, initialState)
-
- for (frame in incoming) {
- frame as? Frame.Binary ?: continue
- val bytes = frame.readBytes()
- val clientMessage = GameSerialization.decodeFromByteArray(bytes)
- service.updateGameStateWithClientMessage(serverName, user.username, clientMessage)
- }
- } catch (e: Exception) {
- eprintln(e.localizedMessage)
- } finally {
- service
- .removeStateConnection(serverName, connection)
- ?.let { finalState -> service.sendAllUpdatedGameState(serverName, finalState) }
- }
- }
-
- @OptIn(ExperimentalSerializationApi::class)
- suspend fun handleData(
- session: DefaultWebSocketServerSession,
- user: Jwt.User,
- ): Unit = with(session) {
- val connection = GameConnection.Data(session, user.username)
- val serverName = call.parameters[SERVER_NAME]?.let(::GameServerName) ?: return
- service.addDataConnection(serverName, connection)
-
- try {
- for (frame in incoming) {
- frame as? Frame.Binary ?: continue
- val bytes = frame.readBytes()
- val clientMessage = GameSerialization.decodeFromByteArray(bytes)
- service.updateGameDataWithClientMessage(serverName, connection.username, clientMessage)
- }
- } catch (e: Exception) {
- eprintln(e.localizedMessage)
- } finally {
- service.removeDataConnection(serverName, connection)
- }
- }
-}
-
-private val SERVER_NAME = StringValuesKey("serverName")
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/routes/UserRoutes.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/routes/UserRoutes.kt
deleted file mode 100644
index a961f53d..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/routes/UserRoutes.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package ml.dev.kotlin.minigames.server.routes
-
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.request.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import ml.dev.kotlin.minigames.server.Jwt
-import ml.dev.kotlin.minigames.service.UserService
-import ml.dev.kotlin.minigames.shared.api.USER_CONFIRM_GET
-import ml.dev.kotlin.minigames.shared.api.USER_CREATE_POST
-import ml.dev.kotlin.minigames.shared.api.USER_LOGIN_POST
-import ml.dev.kotlin.minigames.shared.model.UserCreate
-import ml.dev.kotlin.minigames.shared.model.UserLogin
-import ml.dev.kotlin.minigames.shared.util.on
-import ml.dev.kotlin.minigames.util.RoutesCtx
-import ml.dev.kotlin.minigames.util.StringValuesKey
-import ml.dev.kotlin.minigames.util.get
-
-fun Application.userRoutes() = routing {
- post(USER_LOGIN_POST) { handleUserLogin() }
- post(USER_CREATE_POST) { handleUserCreate() }
- get(USER_CONFIRM_GET("{$CONFIRM_HASH}")) { handleUserConfirm() }
-}
-
-private suspend fun RoutesCtx.handleUserLogin() {
- val userLogin = call.receive()
- val user = UserService.loginUser(userLogin)
- val token = user.map { Jwt.generateToken(it.username) }
- token.on(
- ok = { call.respond(HttpStatusCode.OK, it) },
- err = { call.respond(HttpStatusCode.Unauthorized, it) },
- )
-}
-
-private suspend fun RoutesCtx.handleUserCreate() {
- val userCreate = call.receive()
- val user = UserService.createUser(userCreate)
- user.on(
- ok = {
- UserService.sendConfirmationEmail(it)
- call.respond(HttpStatusCode.Created)
- },
- err = { call.respond(HttpStatusCode.BadRequest, it) },
- )
-}
-
-private suspend fun RoutesCtx.handleUserConfirm() {
- val confirmHash = call.parameters[CONFIRM_HASH] ?: return
- val confirmed = UserService.confirmUser(confirmHash)
- confirmed.on(
- ok = { call.respondText("Confirmed") },
- err = { call.respondText("Not confirmed") },
- )
-}
-
-private val CONFIRM_HASH = StringValuesKey("confirmHash")
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/routes/WebRoutes.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/routes/WebRoutes.kt
deleted file mode 100644
index d14228fc..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/server/routes/WebRoutes.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package ml.dev.kotlin.minigames.server.routes
-
-import io.ktor.server.application.*
-import io.ktor.server.html.*
-import io.ktor.server.routing.*
-import kotlinx.html.*
-import ml.dev.kotlin.minigames.shared.api.MAIN_SITE
-import ml.dev.kotlin.minigames.util.RoutesCtx
-
-fun Application.webRoutes() = routing {
- get(MAIN_SITE) { respondMainSite() }
-}
-
-private suspend fun RoutesCtx.respondMainSite() {
- call.respondHtml {
- head {
- title { +"Mini Games" }
- }
- body {
- h1 {
- +"Download my applications at "
- a("https://play.google.com/store/apps/developer?id=Maciej+Procyk") { +"Google Play" }
- }
- }
- }
-}
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/service/EmailService.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/service/EmailService.kt
deleted file mode 100644
index 4b0e3f55..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/service/EmailService.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package ml.dev.kotlin.minigames.service
-
-import ml.dev.kotlin.minigames.util.envVar
-import net.axay.simplekotlinmail.delivery.mailerBuilder
-import net.axay.simplekotlinmail.delivery.send
-import net.axay.simplekotlinmail.email.emailBuilder
-
-object EmailService {
-
- private val from by lazy { envVar("EMAIL_USERNAME") }
-
- private val mailer by lazy {
- mailerBuilder(
- host = envVar("EMAIL_HOST"),
- port = envVar("EMAIL_PORT"),
- username = envVar("EMAIL_USERNAME"),
- password = envVar("EMAIL_PASSWORD"),
- )
- }
-
- suspend fun send(email: String, subject: String, text: String) {
- emailBuilder {
- from(from)
- to(email)
- withSubject(subject)
- withHTMLText(text)
- }.send(mailer)
- }
-}
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/service/GameService.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/service/GameService.kt
deleted file mode 100644
index 044cbd3b..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/service/GameService.kt
+++ /dev/null
@@ -1,259 +0,0 @@
-package ml.dev.kotlin.minigames.service
-
-import io.ktor.websocket.*
-import kotlinx.coroutines.*
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.encodeToByteArray
-import ml.dev.kotlin.minigames.service.GameServerLocks.Companion.GameServerGuard
-import ml.dev.kotlin.minigames.service.GameStateUpdateResult.Unapproved
-import ml.dev.kotlin.minigames.service.GameStateUpdateResult.Updated
-import ml.dev.kotlin.minigames.shared.model.*
-import ml.dev.kotlin.minigames.shared.util.ComputedMap
-import ml.dev.kotlin.minigames.shared.util.GameSerialization
-import ml.dev.kotlin.minigames.shared.util.now
-import ml.dev.kotlin.minigames.shared.util.tryOrNull
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.CopyOnWriteArrayList
-
-@JvmInline
-value class GameServerName(val name: String)
-
-class GameService(
- private val updateDelay: Long? = null,
- private val default: () -> GameState,
-) {
- private val locks = GameServerLocks()
-
- private suspend inline fun lockForGame(serverName: GameServerName, action: GameServerGuard.() -> T): T =
- locks.lockForGame(serverName, action)
-
- private val serverStateConnections = ConcurrentHashMap>()
-
- private val serverDataConnections = ConcurrentHashMap>()
-
- private val serverUpdateJobs = ConcurrentHashMap()
-
- private val serverGamesStates = ComputedMap { default() }
-
- suspend fun addStateConnection(serverName: GameServerName, connection: GameConnection.State) {
- val connections = serverStateConnections.safeGet(serverName)
- val role = if (connections.isEmpty()) {
- resetServerBackgroundUpdate(serverName)
- UserRole.Admin
- } else UserRole.Player
- connections += connection
- lockForGame(serverName) {
- val gameState = serverGamesStates[this].addUser(connection.username, role)
- updateGameState(gameState)
- }
- }
-
- fun addDataConnection(serverName: GameServerName, connection: GameConnection.Data) {
- serverDataConnections.safeGet(serverName) += connection
- }
-
- private fun stopServerBackgroundUpdate(serverName: GameServerName) {
- serverUpdateJobs.remove(serverName)?.cancel()
- }
-
- private fun resetServerBackgroundUpdate(serverName: GameServerName) {
- if (updateDelay == null) return
- val job = updateInBackground(updateDelay, serverName)
- serverUpdateJobs.put(serverName, job)?.cancel()
- }
-
- suspend fun removeStateConnection(
- serverName: GameServerName,
- connection: GameConnection.State,
- ): GameState? {
- val connections = serverStateConnections.safeGet(serverName)
- connections -= connection
-
- return if (connections.isEmpty()) {
- stopServerBackgroundUpdate(serverName)
- lockForGame(serverName) { serverGamesStates[this] = default() }
- null
- } else if (connections.none { it.username == connection.username }) lockForGame(serverName) {
- val gameState = serverGamesStates[this].removeUser(connection.username)
- updateGameState(gameState)
- } else null
- }
-
- fun removeDataConnection(serverName: GameServerName, connection: GameConnection.Data) {
- serverDataConnections.safeGet(serverName) -= connection
- }
-
- suspend fun updateGameDataWithClientMessage(
- serverName: GameServerName,
- username: Username,
- msg: GameDataClientMessage,
- ): Unit = when (msg) {
- is HeartBeatClientMessage -> sendAllUpdatedGameState(serverName, state(serverName))
-
- is UserActionClientMessage -> updateGameState(
- serverName = serverName,
- byUser = username,
- forUser = msg.forUsername,
- action = msg.action
- )?.let { gameState ->
- val message = UserActionServerMessage(action = msg.action, timestamp = now())
- val connections = serverDataConnections.safeGet(serverName)
- connections.forEach { if (it.username == msg.forUsername) it.sendSerialized(message) }
- sendAllUpdatedGameState(serverName, gameState)
- }
-
- is SendMessageClientMessage -> sendAllUserMessage(serverName, msg.message)
- } ?: Unit
-
- suspend fun updateGameStateWithClientMessage(
- serverName: GameServerName,
- username: Username,
- msg: GameStateUpdateClientMessage,
- ): Unit = when (val updateResult = updateGameState(serverName, username, msg.update)) {
- Unapproved -> sendUnapprovedGameStateUpdate(serverName, username)
- is Updated -> sendAllUpdatedGameState(serverName, updateResult.gameState)
- null -> Unit
- }
-
- private suspend fun updateGameState(
- serverName: GameServerName,
- username: Username,
- update: GameUpdate,
- ): GameStateUpdateResult? = lockForGame(serverName) update@{
- val oldGame = serverGamesStates[this]
- val userData = oldGame.users[username]
- if (userData?.state != UserState.Approved) return@update Unapproved
- val gameState = update.update(username, oldGame, currMillis = now())
- updateGameState(gameState)?.let { Updated(it) }
- }
-
- private suspend fun updateGameState(
- serverName: GameServerName,
- byUser: Username,
- forUser: Username,
- action: UserAction,
- ): GameState? = lockForGame(serverName) {
- val gameState = serverGamesStates[this].changeUserState(byUser, forUser, action)
- updateGameState(gameState)
- }
-
- private suspend fun timeUpdateGameState(
- serverName: GameServerName,
- ): GameState? = lockForGame(serverName) {
- val gameState = serverGamesStates[this].update(currMillis = now())
- updateGameState(gameState)
- }
-
- private fun GameServerGuard.updateGameState(gameState: GameState): GameState? {
- val oldState = serverGamesStates[this]
- return if (oldState == gameState) null
- else gameState.also { serverGamesStates[this] = it }
- }
-
- suspend fun state(serverName: GameServerName): GameState =
- lockForGame(serverName) { serverGamesStates[this] }
-
- suspend fun sendAllUpdatedGameState(
- serverName: GameServerName,
- gameState: GameState,
- ) {
- val snapshot = gameState.snapshot()
- supervisorScope {
- serverStateConnections.safeGet(serverName)
- .map { launchSendSnapshot(it, snapshot::get) }
- .joinAll()
- }
- }
-
- private suspend fun sendAllUserMessage(
- serverName: GameServerName,
- userMessage: UserMessage,
- ): Unit = supervisorScope {
- serverDataConnections.safeGet(serverName).map {
- launch { it.sendSerialized(ReceiveMessageServerMessage(userMessage, timestamp = now())) }
- }.joinAll()
- }
-
- private suspend fun sendUnapprovedGameStateUpdate(serverName: GameServerName, username: Username) {
- val message = UnapprovedGameStateUpdateServerMessage(timestamp = now())
- serverDataConnections.safeGet(serverName).forEach {
- if (it.username == username) it.sendSerialized(message)
- }
- }
-
- private fun updateInBackground(delayMillis: Long, serverName: GameServerName): Job =
- CoroutineScope(Dispatchers.IO).launch {
- var now = 0L
- while (isActive) tryOrNull update@{
- val updatedGameState = timeUpdateGameState(serverName) ?: return@update
- sendAllUpdatedGameState(serverName, updatedGameState)
-
- val last = now
- now = now()
- val passedMillis = now - last
- delay(delayMillis - passedMillis)
- }
- }
-}
-
-sealed class GameConnection(
- protected val session: WebSocketSession,
- val username: Username,
-) {
- override fun hashCode(): Int = session.hashCode()
- override fun equals(other: Any?): Boolean {
- if (other == null) return false
- if (this::class.java != other::class.java) return false
- return (other as? GameConnection)?.session == session
- }
-
- class State(session: WebSocketSession, username: Username) : GameConnection(session, username) {
- @OptIn(ExperimentalSerializationApi::class)
- suspend fun sendSerialized(content: GameStateSnapshotServerMessage): Unit =
- session.send(Frame.Binary(true, GameSerialization.encodeToByteArray(content)))
- }
-
- class Data(session: WebSocketSession, username: Username) : GameConnection(session, username) {
- @OptIn(ExperimentalSerializationApi::class)
- suspend fun sendSerialized(content: GameDataServerMessage): Unit =
- session.send(Frame.Binary(true, GameSerialization.encodeToByteArray(content)))
- }
-}
-
-private fun CoroutineScope.launchSendSnapshot(
- connection: GameConnection.State,
- snapshot: (Username) -> GameSnapshot?,
-): Job = launch {
- val userSnapshot = snapshot(connection.username) ?: return@launch
- val message = GameStateSnapshotServerMessage(userSnapshot, timestamp = now())
- connection.sendSerialized(message)
-}
-
-private class GameServerLocks {
- private val serverLocks: ConcurrentHashMap = ConcurrentHashMap()
-
- suspend inline fun lockForGame(serverName: GameServerName, action: GameServerGuard.() -> T): T =
- serverLocks.getOrPut(serverName) { Mutex() }
- .withLock {
- val guard = GameServerGuard(serverName)
- guard.action()
- }
-
- companion object {
- @JvmInline
- value class GameServerGuard(val name: GameServerName)
- }
-}
-
-private sealed interface GameStateUpdateResult {
- data object Unapproved : GameStateUpdateResult
-
- @JvmInline
- value class Updated(val gameState: GameState) : GameStateUpdateResult
-}
-
-@Suppress("NOTHING_TO_INLINE")
-private inline fun ConcurrentHashMap>.safeGet(key: K): CopyOnWriteArrayList =
- getOrPut(key) { CopyOnWriteArrayList() }
\ No newline at end of file
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/service/UserService.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/service/UserService.kt
deleted file mode 100644
index c4fd6efb..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/service/UserService.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package ml.dev.kotlin.minigames.service
-
-import at.favre.lib.crypto.bcrypt.BCrypt
-import ml.dev.kotlin.minigames.db.model.UserEntity
-import ml.dev.kotlin.minigames.db.model.UsersTable
-import ml.dev.kotlin.minigames.db.suspendedTxn
-import ml.dev.kotlin.minigames.shared.api.USER_CONFIRM_GET
-import ml.dev.kotlin.minigames.shared.model.UserCreate
-import ml.dev.kotlin.minigames.shared.model.UserError
-import ml.dev.kotlin.minigames.shared.model.UserError.Reason
-import ml.dev.kotlin.minigames.shared.model.UserLogin
-import ml.dev.kotlin.minigames.shared.util.Res
-import ml.dev.kotlin.minigames.shared.util.err
-import ml.dev.kotlin.minigames.shared.util.ok
-import ml.dev.kotlin.minigames.util.envVar
-import ml.dev.kotlin.minigames.util.sha256
-import java.util.*
-
-object UserService {
-
- private const val BCRYPT_COST = 12
-
- private val REQUIRE_EMAIL_VERIFY = envVar("REQUIRE_EMAIL_VERIFY")
-
- private val SCHEME_EMAIL_VERIFY = envVar("SCHEME_EMAIL_VERIFY")
-
- private val HOST_EMAIL_VERIFY = envVar("HOST_EMAIL_VERIFY")
-
- suspend fun loginUser(userLogin: UserLogin): Res = suspendedTxn {
- UserEntity.find { UsersTable.username eq userLogin.username }.singleOrNull()
- }?.let {
- val passwordMatch = userLogin.password matchesHash it.passwordHash
- when {
- passwordMatch && it.confirmed -> it.ok()
- passwordMatch && !it.confirmed -> UserError(it.username, Reason.NotConfirmed).err()
- else -> UserError(it.username, Reason.InvalidPassword).err()
- }
- } ?: UserError(userLogin.username, Reason.NotExists).err()
-
- suspend fun createUser(userCreate: UserCreate): Res = suspendedTxn {
- val existingUserName = UserEntity.find {
- UsersTable.username eq userCreate.username
- }.singleOrNull()
-
- val existingUserEmail = UserEntity.find {
- UsersTable.email eq userCreate.email
- }.singleOrNull()
-
- when {
- existingUserName == null && existingUserEmail == null -> UserEntity.new {
- this.email = userCreate.email
- this.username = userCreate.username
- this.passwordHash = userCreate.password.bcrypt()
- this.confirmed = !REQUIRE_EMAIL_VERIFY
- this.confirmHash = UUID.randomUUID().toString().sha256()
- }.ok()
-
- existingUserEmail != null && !existingUserEmail.confirmed ->
- UserError(userCreate.username, Reason.NotConfirmed).err()
-
- else -> UserError(userCreate.username, Reason.AlreadyExists).err()
- }
- }
-
- suspend fun confirmUser(confirmHash: String): Boolean = suspendedTxn {
- UserEntity.find { UsersTable.confirmHash eq confirmHash }.apply {
- forEach { it.confirmed = true }
- }.empty().not()
- }
-
- suspend fun sendConfirmationEmail(userEntity: UserEntity) {
- if (!REQUIRE_EMAIL_VERIFY) return
- val confirmUrl = createConfirmUrl(userEntity)
- EmailService.send(
- userEntity.email,
- subject = "[Mini Games] Confirm your email address",
- text = """If you have registered to Mini Games, please confirm your email address by using this link"""
- )
- }
-
- private fun createConfirmUrl(userEntity: UserEntity): String =
- "$SCHEME_EMAIL_VERIFY://$HOST_EMAIL_VERIFY/${USER_CONFIRM_GET(userEntity.confirmHash)}"
-
- private fun String.bcrypt(): String = BCrypt.withDefaults().hashToString(BCRYPT_COST, toCharArray())
-
- private infix fun String.matchesHash(hash: String): Boolean = BCrypt.verifyer().verify(toCharArray(), hash).verified
-}
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/util/AnyUtil.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/util/AnyUtil.kt
deleted file mode 100644
index 763d8a37..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/util/AnyUtil.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package ml.dev.kotlin.minigames.util
-
-import java.security.MessageDigest
-
-fun String.sha256(): String = MessageDigest
- .getInstance("SHA-256")
- .digest(toByteArray())
- .joinToString("") { "%02x".format(it) }
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/util/EnvUtil.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/util/EnvUtil.kt
deleted file mode 100644
index f62d6b92..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/util/EnvUtil.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-@file:Suppress("NOTHING_TO_INLINE")
-
-package ml.dev.kotlin.minigames.util
-
-inline fun envVar(name: String): T = when (T::class) {
- Int::class -> System.getenv(name)?.toInt() as? T
- String::class -> System.getenv(name) as? T
- Boolean::class -> System.getenv(name)?.toBoolean() as? T
- else -> throw IllegalStateException("Getting env variables for ${T::class} not defined")
-} ?: throw IllegalArgumentException("Env variable $name not defined")
-
-@Suppress("NOTHING_TO_INLINE")
-inline fun eprintln(s: Any?) = System.err.println(s)
diff --git a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/util/RoutesUtil.kt b/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/util/RoutesUtil.kt
deleted file mode 100644
index d85d0b16..00000000
--- a/server/src/jvmMain/kotlin/ml/dev/kotlin/minigames/util/RoutesUtil.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-@file:Suppress("NOTHING_TO_INLINE")
-
-package ml.dev.kotlin.minigames.util
-
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.auth.*
-import io.ktor.server.request.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import io.ktor.server.websocket.*
-import io.ktor.util.*
-import io.ktor.util.pipeline.*
-import ml.dev.kotlin.minigames.server.Jwt
-import org.slf4j.Logger
-
-typealias RoutesCtx = PipelineContext
-
-inline fun RoutesCtx.log(): Logger = this.application.environment.log
-
-@KtorDsl
-inline fun Route.authJwtPost(
- path: String,
- crossinline body: suspend RoutesCtx.(R, Jwt.User) -> Unit,
-): Route = authenticate(Jwt.CONFIG) {
- post(path) {
- val principal = call.principal()
- if (principal != null) body(call.receive(), principal)
- else call.respond(HttpStatusCode.Unauthorized)
- }
-}
-
-@KtorDsl
-inline fun Route.authJwtGet(
- path: String,
- crossinline body: suspend RoutesCtx.(Jwt.User) -> Unit,
-): Route = authenticate(Jwt.CONFIG) {
- get(path) {
- val principal = call.principal()
- if (principal != null) body(principal)
- else call.respond(HttpStatusCode.Unauthorized)
- }
-}
-
-@KtorDsl
-fun Route.authJwtWebSocket(
- path: String,
- handler: suspend (DefaultWebSocketServerSession, Jwt.User) -> Unit,
-): Route = authenticate(Jwt.CONFIG) {
- webSocket(path) {
- val principal = call.principal()
- if (principal != null) handler(this, principal)
- else call.respond(HttpStatusCode.Unauthorized)
- }
-}
-
-@JvmInline
-value class StringValuesKey(val key: String) {
- override fun toString(): String = key
-}
-
-operator fun StringValues.get(key: StringValuesKey): String? = this[key.key]
diff --git a/settings.gradle.kts b/settings.gradle.kts
deleted file mode 100644
index ec48bc36..00000000
--- a/settings.gradle.kts
+++ /dev/null
@@ -1,43 +0,0 @@
-pluginManagement {
- repositories {
- google()
- gradlePluginPortal()
- mavenCentral()
- maven("https://jitpack.io")
- maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
- }
-
- plugins {
- val kotlinVersion: String by System.getProperties()
- val agpVersion: String by System.getProperties()
- val composeVersion: String by System.getProperties()
- val buildkonfigVersion: String by System.getProperties()
- val shadowVersion: String by System.getProperties()
- val parcelizeDarwinVersion: String by System.getProperties()
-
- kotlin("multiplatform") version kotlinVersion
- kotlin("plugin.parcelize") version kotlinVersion
- kotlin("plugin.serialization") version kotlinVersion
- id("com.android.application") version agpVersion
- id("com.android.library") version agpVersion
- id("org.jetbrains.compose") version composeVersion
- id("com.codingfeline.buildkonfig") version buildkonfigVersion
- id("com.github.johnrengelman.shadow") version shadowVersion
- id("com.arkivanov.parcelize.darwin") version parcelizeDarwinVersion
- }
-}
-
-plugins {
- val foojayResolverVersion: String by System.getProperties()
- id("org.gradle.toolchains.foojay-resolver-convention") version foojayResolverVersion
-}
-
-rootProject.name = "mini-games"
-
-includeBuild("build-src")
-
-include(":shared")
-include(":shared-client")
-include(":android-app")
-include(":desktop-app")
-include(":server")
diff --git a/shared-client/.gitignore b/shared-client/.gitignore
deleted file mode 100644
index 42afabfd..00000000
--- a/shared-client/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/shared-client/build.gradle.kts b/shared-client/build.gradle.kts
deleted file mode 100644
index 904086b4..00000000
--- a/shared-client/build.gradle.kts
+++ /dev/null
@@ -1,189 +0,0 @@
-import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
-import com.codingfeline.buildkonfig.gradle.TargetConfigDsl
-import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
-import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
-
-plugins {
- kotlin("multiplatform")
- kotlin("native.cocoapods")
- id("com.android.library")
- id("org.jetbrains.compose")
- id("com.codingfeline.buildkonfig")
- kotlin("plugin.serialization")
- kotlin("plugin.parcelize")
- id("com.arkivanov.parcelize.darwin")
- id("build-src-plugin")
-}
-
-kotlin {
- jvmToolchain(17)
-
- androidTarget()
-
- iosX64()
- iosArm64()
- iosSimulatorArm64()
-
- jvm()
-
- @OptIn(ExperimentalKotlinGradlePluginApi::class)
- compilerOptions {
- freeCompilerArgs.add("-Xexpect-actual-classes")
- }
-
- cocoapods {
- homepage = "https://github.com/avan1235/mini-games"
- summary = "Mini Games shared client"
- version = VERSION
- ios.deploymentTarget = Constants.iOS.deploymentTarget
- podfile = project.file("../ios-app/Podfile")
- framework {
- isStatic = true
- baseName = "shared_client"
- export(Dependencies.decompose)
- export(Dependencies.essenty)
- export(Dependencies.stateKeeper)
- export(Dependencies.parcelizeDarwinRuntime)
- }
- }
-
- sourceSets {
- commonMain.dependencies {
- implementation(project(":shared"))
-
- api(compose.runtime)
- api(compose.foundation)
- api(compose.material3)
- api(compose.materialIconsExtended)
- api(compose.ui)
- api(compose.animation)
- api(compose.animationGraphics)
-
- @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
- implementation(compose.components.resources)
-
- implementation(Dependencies.decompose)
- implementation(Dependencies.decomposeExtensions)
-
- implementation(Dependencies.composeUtil)
-
- implementation(Dependencies.kotlinxSerializationCbor)
- implementation(Dependencies.kotlinxSerializationJson)
- implementation(Dependencies.ktorClientCore)
- implementation(Dependencies.ktorClientWebsockets)
- implementation(Dependencies.ktorClientSerialization)
- implementation(Dependencies.ktorClientContentNegotiation)
-
- implementation(Dependencies.napierLogger)
- implementation(Dependencies.multiplatformSettings)
- implementation(Dependencies.multiplatformSettingsCoroutines)
-
- implementation(Dependencies.kotlinxAtomicFu)
- }
- androidMain {
- resources.srcDirs("src/commonMain/res")
-
- dependencies {
- implementation(Dependencies.androidXActivity)
- implementation(Dependencies.androidXActivityCompose)
- implementation(Dependencies.decompose)
-
- implementation(Dependencies.ktorClientAndroid)
-
- implementation(Dependencies.androidXDataStorePreferences)
- implementation(Dependencies.multiplatformSettings)
- implementation(Dependencies.multiplatformSettingsCoroutines)
- implementation(Dependencies.multiplatformSettingsDatastore)
- implementation(Dependencies.kotlinxCoroutinesAndroid)
- }
- }
- jvmMain {
- resources.srcDirs("src/commonMain/res")
-
- dependencies {
- implementation(compose.desktop.common)
- implementation(compose.desktop.currentOs)
-
- implementation(Dependencies.ktorClientDesktop)
- implementation(Dependencies.multiplatformSettings)
- implementation(Dependencies.multiplatformSettingsCoroutines)
- implementation(Dependencies.kotlinxCoroutinesSwing)
- }
- }
-
- iosMain {
- resources.srcDirs("src/commonMain/res")
-
- dependencies {
- implementation(Dependencies.ktorClientDarwin)
- api(Dependencies.decompose)
- api(Dependencies.essenty)
- api(Dependencies.stateKeeper)
- api(Dependencies.parcelizeDarwinRuntime)
- implementation(Dependencies.multiplatformSettings)
- implementation(Dependencies.multiplatformSettingsCoroutines)
- }
- }
- }
-}
-
-buildkonfig {
- packageName = "ml.dev.kotlin.minigames.shared"
- objectName = "BuildConfiguration"
-
- defaultConfigs {
- buildConfigField("REST_CLIENT_API_SCHEME")
- buildConfigField("WEBSOCKET_CLIENT_API_SCHEME")
- }
-
- targetConfigs {
- create("android") {
- buildConfigField("ANDROID_CLIENT_API_HOST")
- }
- listOf(
- "iosX64",
- "iosArm64",
- "iosSimulatorArm64",
- ).forEach {
- create(it) {
- buildConfigField("IOS_CLIENT_API_HOST")
- }
- }
- create("jvm") {
- buildConfigField("DESKTOP_CLIENT_API_HOST")
- }
- }
-}
-
-android {
- compileSdk = Constants.Android.compileSdk
- namespace = "ml.dev.kotlin.minigames.shared.client"
-
- defaultConfig {
- minSdk = Constants.Android.minSdk
- }
-
- sourceSets {
- named("main") {
- res.srcDirs(
- "src/androidMain/res",
- "src/commonMain/res",
- )
- }
- }
-}
-
-fun KotlinNativeTarget.configureBinary() = apply {
- binaries.framework {
- baseName = "shared_client"
- binaryOption("bundleId", "ml.dev.kotlin.shared.client")
- }
-}
-
-inline fun TargetConfigDsl.buildConfigField(name: String) {
- val value = ENV[name] ?: throw IllegalStateException("$name not defined")
- when (T::class) {
- String::class -> buildConfigField(STRING, name, value)
- else -> throw IllegalStateException("Not implemented for ${T::class.java.simpleName}")
- }
-}
diff --git a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/component/MiniGamesAppComponentContext.kt b/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/component/MiniGamesAppComponentContext.kt
deleted file mode 100644
index ba670c70..00000000
--- a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/component/MiniGamesAppComponentContext.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import android.content.Context
-import android.view.Window
-import android.view.WindowManager
-import androidx.compose.material3.SnackbarHostState
-
-actual class MiniGamesAppComponentContext(
- val applicationContext: Context,
- val window: Window,
-) {
- @Suppress("DEPRECATION")
- actual fun adjustResize(): Unit = window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
-
- actual fun adjustPan(): Unit = window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
-
- actual val snackbarHostState = SnackbarHostState()
-}
\ No newline at end of file
diff --git a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/main.android.kt b/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/main.android.kt
deleted file mode 100644
index 5b9334bc..00000000
--- a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/main.android.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package ml.dev.kotlin.minigames.shared
-
-import android.annotation.SuppressLint
-import android.app.Activity
-import android.content.Context
-import android.content.ContextWrapper
-import android.content.pm.ActivityInfo
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.ui.platform.LocalContext
-import ml.dev.kotlin.minigames.shared.component.MiniGamesAppComponent
-
-fun ComponentActivity.setMainAndroidApp(
- component: MiniGamesAppComponent,
-): Unit = setContent {
- LockScreenPortraitOrientation()
- MiniGamesApp(component)
-}
-
-@SuppressLint("SourceLockedOrientationActivity")
-@Composable
-private fun LockScreenPortraitOrientation() {
- val context = LocalContext.current
- DisposableEffect(Unit) {
- val activity = context.findActivity() ?: return@DisposableEffect onDispose { }
- val originalOrientation = activity.requestedOrientation
- activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
- onDispose { activity.requestedOrientation = originalOrientation }
- }
-}
-
-private tailrec fun Context.findActivity(): Activity? = when (this) {
- is Activity -> this
- is ContextWrapper -> baseContext.findActivity()
- else -> null
-}
\ No newline at end of file
diff --git a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/rest/RestApiConfig.kt b/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/rest/RestApiConfig.kt
deleted file mode 100644
index c8d969d8..00000000
--- a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/rest/RestApiConfig.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package ml.dev.kotlin.minigames.shared.rest
-
-import ml.dev.kotlin.minigames.shared.BuildConfiguration
-
-actual object RestApiConfig {
- actual val host = BuildConfiguration.ANDROID_CLIENT_API_HOST
- actual val scheme = BuildConfiguration.REST_CLIENT_API_SCHEME
-}
diff --git a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/rest/client/RestJsonApiClient.kt b/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/rest/client/RestJsonApiClient.kt
deleted file mode 100644
index 36d09aa8..00000000
--- a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/rest/client/RestJsonApiClient.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package ml.dev.kotlin.minigames.shared.rest.client
-
-import io.ktor.client.engine.*
-import io.ktor.client.engine.okhttp.*
-
-internal actual val CLIENT_ENGINE_FACTORY: HttpClientEngineFactory<*> = OkHttp
diff --git a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/DropdownMenuParts.kt b/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/DropdownMenuParts.kt
deleted file mode 100644
index c286bbf0..00000000
--- a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/DropdownMenuParts.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-
-@Composable
-internal actual fun DropdownMenu(
- expanded: Boolean,
- onDismissRequest: () -> Unit,
- modifier: Modifier,
- content: @Composable ColumnScope.() -> Unit,
-): Unit = androidx.compose.material3.DropdownMenu(
- expanded = expanded,
- onDismissRequest = onDismissRequest,
- modifier = modifier,
- content = content,
-)
-
-@Composable
-internal actual fun DropdownMenuItem(
- onClick: () -> Unit,
- content: @Composable () -> Unit,
-): Unit = androidx.compose.material3.DropdownMenuItem(
- onClick = onClick,
- text = { content() },
-)
diff --git a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/FormFieldParts.kt b/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/FormFieldParts.kt
deleted file mode 100644
index 8774344f..00000000
--- a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/FormFieldParts.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-internal actual fun moveDownOnTab(): Boolean = true
diff --git a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/ui/theme/Fonts.kt b/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/ui/theme/Fonts.kt
deleted file mode 100644
index 90717f2f..00000000
--- a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/ui/theme/Fonts.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.theme
-
-import androidx.compose.ui.text.font.Font
-import androidx.compose.ui.text.font.FontFamily
-import ml.dev.kotlin.minigames.shared.client.R
-
-internal actual suspend fun loadLeckerliOneFont(): FontFamily =
- loadFontFamily(R.font.leckerlione_regular)
-
-private fun loadFontFamily(id: Int): FontFamily =
- FontFamily(Font(id))
diff --git a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/viewmodel/ViewModelSettings.kt b/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/viewmodel/ViewModelSettings.kt
deleted file mode 100644
index 6b6cbeee..00000000
--- a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/viewmodel/ViewModelSettings.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package ml.dev.kotlin.minigames.shared.viewmodel
-
-import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.preferencesDataStore
-import com.russhwolf.settings.ExperimentalSettingsApi
-import com.russhwolf.settings.ExperimentalSettingsImplementation
-import com.russhwolf.settings.coroutines.SuspendSettings
-import com.russhwolf.settings.datastore.DataStoreSettings
-import ml.dev.kotlin.minigames.shared.component.MiniGamesAppComponentContext
-
-private val Context.USER_LOGIN_DATA_STORE: DataStore by preferencesDataStore("user_settings")
-
-@OptIn(ExperimentalSettingsApi::class, ExperimentalSettingsImplementation::class)
-internal actual fun getUserSettings(context: MiniGamesAppComponentContext): SuspendSettings =
- with(context.applicationContext) {
- DataStoreSettings(USER_LOGIN_DATA_STORE)
- }
diff --git a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/websocket/WebsocketApiConfig.kt b/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/websocket/WebsocketApiConfig.kt
deleted file mode 100644
index 950d877b..00000000
--- a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/websocket/WebsocketApiConfig.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package ml.dev.kotlin.minigames.shared.websocket
-
-import ml.dev.kotlin.minigames.shared.BuildConfiguration
-
-actual object WebsocketApiConfig {
- actual val host = BuildConfiguration.ANDROID_CLIENT_API_HOST
- actual val scheme = BuildConfiguration.WEBSOCKET_CLIENT_API_SCHEME
-}
diff --git a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/websocket/client/WebsocketApiClient.kt b/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/websocket/client/WebsocketApiClient.kt
deleted file mode 100644
index 85478033..00000000
--- a/shared-client/src/androidMain/kotlin/ml/dev/kotlin/minigames/shared/websocket/client/WebsocketApiClient.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package ml.dev.kotlin.minigames.shared.websocket.client
-
-import io.ktor.client.engine.*
-import io.ktor.client.engine.okhttp.*
-
-internal actual val CLIENT_ENGINE_FACTORY: HttpClientEngineFactory<*> = OkHttp
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/MiniGamesApp.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/MiniGamesApp.kt
deleted file mode 100644
index e6d1fcd0..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/MiniGamesApp.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package ml.dev.kotlin.minigames.shared
-
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import com.arkivanov.decompose.extensions.compose.stack.Children
-import com.arkivanov.decompose.extensions.compose.stack.animation.slide
-import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
-import io.github.aakira.napier.DebugAntilog
-import io.github.aakira.napier.Napier
-import ml.dev.kotlin.minigames.shared.component.MiniGamesAppComponent
-import ml.dev.kotlin.minigames.shared.component.MiniGamesAppComponent.Child
-import ml.dev.kotlin.minigames.shared.component.MiniGamesAppComponent.Child.Game
-import ml.dev.kotlin.minigames.shared.ui.component.bird.BirdGamePlay
-import ml.dev.kotlin.minigames.shared.ui.component.set.SetGamePlay
-import ml.dev.kotlin.minigames.shared.ui.component.snake.SnakeGamePlay
-import ml.dev.kotlin.minigames.shared.ui.screen.GameScreen
-import ml.dev.kotlin.minigames.shared.ui.screen.LogInScreen
-import ml.dev.kotlin.minigames.shared.ui.screen.RegisterScreen
-import ml.dev.kotlin.minigames.shared.ui.theme.Theme
-
-@Composable
-internal fun MiniGamesApp(component: MiniGamesAppComponent) {
- Napier.base(DebugAntilog())
- Theme {
- Scaffold(
- snackbarHost = {
- SnackbarHost(
- modifier = Modifier.padding(bottom = 72.dp),
- hostState = component.snackbarHostState
- )
- },
- modifier = Modifier.fillMaxSize()
- ) {
- Children(
- stack = component.stack,
- modifier = Modifier.fillMaxSize(),
- animation = stackAnimation(slide())
- ) { child ->
- when (val instance = child.instance) {
- is Game -> when (instance) {
- is Game.Bird -> GameScreen(instance.component) { snapshot, stateMessages ->
- BirdGamePlay(instance.component, snapshot, stateMessages)
- }
-
- is Game.Set -> GameScreen(instance.component) { snapshot, stateMessages ->
- SetGamePlay(instance.component, snapshot, stateMessages)
- }
-
- is Game.Snake -> GameScreen(instance.component) { snapshot, stateMessages ->
- SnakeGamePlay(instance.component, snapshot, stateMessages)
- }
- }
-
- is Child.LogIn -> LogInScreen(instance.component)
- is Child.Register -> RegisterScreen(instance.component)
- }
- }
- }
- }
-}
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/AbstractComponent.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/AbstractComponent.kt
deleted file mode 100644
index 95a79a76..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/AbstractComponent.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import androidx.compose.material3.SnackbarDuration
-import com.arkivanov.decompose.ComponentContext
-import com.arkivanov.decompose.value.Value
-import com.arkivanov.essenty.lifecycle.Lifecycle
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
-import ml.dev.kotlin.minigames.shared.ui.util.coroutineScope
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.flow.combine as coroutinesFlowCombine
-import kotlinx.coroutines.flow.map as coroutinesFlowMap
-import ml.dev.kotlin.minigames.shared.ui.util.asValue as asValueUtil
-
-interface Component {
- val appContext: MiniGamesAppComponentContext
-
- fun toast(
- message: String,
- actionLabel: String? = null,
- withDismissAction: Boolean = false,
- duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite,
- )
-}
-
-abstract class AbstractComponent(
- final override val appContext: MiniGamesAppComponentContext,
- componentContext: ComponentContext,
-) : ComponentContext by componentContext, Component {
-
- protected val scope: CoroutineScope = coroutineScope(Dispatchers.Main.immediate)
-
- override fun toast(
- message: String,
- actionLabel: String?,
- withDismissAction: Boolean,
- duration: SnackbarDuration,
- ) {
- scope.launch {
- appContext.snackbarHostState.showSnackbar(message, actionLabel, withDismissAction, duration)
- }
- }
-
- protected fun StateFlow.map(
- coroutineScope: CoroutineScope = scope,
- mapper: (value: T) -> M,
- ): StateFlow =
- coroutinesFlowMap(mapper)
- .stateIn(
- coroutineScope,
- SharingStarted.Eagerly,
- mapper(value),
- )
-
- protected fun combine(
- flow1: StateFlow,
- flow2: StateFlow,
- coroutineScope: CoroutineScope = scope,
- transform: (T1, T2) -> R,
- ): StateFlow =
- coroutinesFlowCombine(flow1, flow2, transform)
- .stateIn(
- coroutineScope,
- SharingStarted.Eagerly,
- transform(flow1.value, flow2.value)
- )
-
- protected fun combine(
- flow1: StateFlow,
- flow2: StateFlow,
- flow3: StateFlow,
- coroutineScope: CoroutineScope = scope,
- transform: (T1, T2, T3) -> R,
- ): StateFlow =
- coroutinesFlowCombine(flow1, flow2, flow3, transform)
- .stateIn(
- coroutineScope,
- SharingStarted.Eagerly,
- transform(flow1.value, flow2.value, flow3.value)
- )
-
- protected fun combine(
- flow1: StateFlow,
- flow2: StateFlow,
- flow3: StateFlow,
- flow4: StateFlow,
- coroutineScope: CoroutineScope = scope,
- transform: (T1, T2, T3, T4) -> R,
- ): StateFlow =
- coroutinesFlowCombine(flow1, flow2, flow3, flow4, transform)
- .stateIn(
- coroutineScope,
- SharingStarted.Eagerly,
- transform(flow1.value, flow2.value, flow3.value, flow4.value)
- )
-
- protected fun StateFlow.asValue(
- lifecycle: Lifecycle = this@AbstractComponent.lifecycle,
- context: CoroutineContext = Dispatchers.Main.immediate,
- ): Value = asValueUtil(lifecycle, context)
-}
\ No newline at end of file
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/BirdComponent.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/BirdComponent.kt
deleted file mode 100644
index c04465b5..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/BirdComponent.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import com.arkivanov.decompose.ComponentContext
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-import ml.dev.kotlin.minigames.shared.api.BIRD_GAME_WEBSOCKET
-import ml.dev.kotlin.minigames.shared.model.BirdGameSnapshot
-import ml.dev.kotlin.minigames.shared.model.BirdGameUpdate
-import ml.dev.kotlin.minigames.shared.model.GameStateUpdateClientMessage
-import ml.dev.kotlin.minigames.shared.util.now
-import ml.dev.kotlin.minigames.shared.websocket.client.GameAccessData
-import ml.dev.kotlin.minigames.shared.websocket.client.GameClient
-
-interface BirdComponent : GameComponent {
-
- fun emitFly(stateMessages: MutableStateFlow)
-}
-
-internal class BirdComponentImpl(
- appContext: MiniGamesAppComponentContext,
- componentContext: ComponentContext,
- gameAccessData: GameAccessData,
- onCloseGame: (String?) -> Unit,
-) : AbstractGameComponent(
- appContext,
- componentContext,
- gameAccessData,
- onCloseGame
-), BirdComponent {
-
- override val client: GameClient = GameClient(BIRD_GAME_WEBSOCKET)
-
- override fun emitFly(stateMessages: MutableStateFlow) {
- scope.launch {
- val message = GameStateUpdateClientMessage(BirdGameUpdate, timestamp = now())
- stateMessages.emit(message)
- }
- }
-}
\ No newline at end of file
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/ChatComponent.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/ChatComponent.kt
deleted file mode 100644
index c9d65bfe..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/ChatComponent.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import androidx.compose.ui.text.input.TextFieldValue
-import com.arkivanov.decompose.ComponentContext
-import com.arkivanov.decompose.value.Value
-import io.github.aakira.napier.Napier
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import ml.dev.kotlin.minigames.shared.model.GameDataClientMessage
-import ml.dev.kotlin.minigames.shared.model.SendMessageClientMessage
-import ml.dev.kotlin.minigames.shared.model.UserMessage
-import ml.dev.kotlin.minigames.shared.model.Username
-import ml.dev.kotlin.minigames.shared.util.now
-
-interface ChatComponent : CountingComponent {
- val username: Username
- val messages: Value>
-
- val userMessageText: Value
- fun onUserMessageTextChange(text: TextFieldValue)
-
- fun addMessage(message: UserMessage)
-
- fun send(clientMessages: MutableSharedFlow)
-}
-
-class ChatComponentImpl(
- appContext: MiniGamesAppComponentContext,
- componentContext: ComponentContext,
- countPredicate: () -> Boolean,
- override val username: Username,
-) : AbstractCountingComponent(appContext, componentContext, countPredicate), ChatComponent {
- private val _messages: MutableStateFlow> = MutableStateFlow(emptyList())
- override val messages: Value> = _messages.asValue()
-
- private val _userMessageText: MutableStateFlow = MutableStateFlow(TextFieldValue())
- override val userMessageText: Value = _userMessageText.asValue()
-
- override fun onUserMessageTextChange(text: TextFieldValue) {
- Napier.d { "onUserMessageTextChange: ${text.text}" }
- _userMessageText.value = text
- }
-
- override fun addMessage(message: UserMessage) {
- countNew()
- _messages.update {
- val idx = it.binarySearch(message, USER_MESSAGES_COMPARATOR)
- if (idx < 0) it.toMutableList().apply { add(-idx - 1, message) } else it
- }
-
- }
-
- override fun send(clientMessages: MutableSharedFlow) {
- val userMessageText = _userMessageText.value
- if (userMessageText.text.isBlank()) return
- val timestamp = now()
- val userMessage = UserMessage(userMessageText.text, username, timestamp)
- val message = SendMessageClientMessage(userMessage, timestamp)
- scope.launch {
- clientMessages.emit(message)
- _userMessageText.value = TextFieldValue()
- }
- }
-}
-
-private val USER_MESSAGES_COMPARATOR: Comparator =
- Comparator { a, b -> a.timestamp.compareTo(b.timestamp) }
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/CountingComponent.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/CountingComponent.kt
deleted file mode 100644
index f1b7952e..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/CountingComponent.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import com.arkivanov.decompose.ComponentContext
-import com.arkivanov.decompose.value.Value
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.update
-
-interface CountingComponent : Component {
- val count: Value
-
- fun clearNewCount()
-}
-
-abstract class AbstractCountingComponent(
- appContext: MiniGamesAppComponentContext,
- componentContext: ComponentContext,
- private val countPredicate: () -> Boolean,
-) : AbstractComponent(appContext, componentContext), CountingComponent {
-
- private val _count: MutableStateFlow = MutableStateFlow(0)
- override val count: Value = _count.asValue()
-
- protected fun countNew() {
- val isCountValid = countPredicate()
- if (!isCountValid) return
- _count.update { it + 1 }
- }
-
- override fun clearNewCount() {
- _count.update { 0 }
- }
-}
\ No newline at end of file
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/GameComponent.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/GameComponent.kt
deleted file mode 100644
index 36cc7a0b..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/GameComponent.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import com.arkivanov.decompose.ComponentContext
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.launch
-import ml.dev.kotlin.minigames.shared.model.*
-import ml.dev.kotlin.minigames.shared.util.now
-import ml.dev.kotlin.minigames.shared.viewmodel.CONNECT_ERROR_MESSAGE
-import ml.dev.kotlin.minigames.shared.viewmodel.RECEIVE_ERROR_MESSAGE
-import ml.dev.kotlin.minigames.shared.viewmodel.SEND_ERROR_MESSAGE
-import ml.dev.kotlin.minigames.shared.websocket.client.GameAccessData
-import ml.dev.kotlin.minigames.shared.websocket.client.GameClient
-
-interface GameComponent : Component, ComponentContext {
- val client: GameClient
- val gameAccessData: GameAccessData
- val username: Username get() = gameAccessData.userLogin.username
-
- fun closeGame()
-
- fun onErrorLogin()
-
- fun onErrorReceive(e: Exception)
-
- fun onErrorSend(e: Exception)
-
- fun points(snapshot: Snapshot): Int
-
- fun points(forUser: Username, snapshot: Snapshot): Int
-
- fun userRole(snapshot: Snapshot): UserRole
-
- fun canEditUser(username: Username, snapshot: Snapshot): Boolean
-
- fun heartBeat(): HeartBeatClientMessage
-
- fun approve(
- username: Username,
- clientMessages: MutableSharedFlow,
- )
-
- fun discard(
- username: Username,
- clientMessages: MutableSharedFlow,
- )
-}
-
-abstract class AbstractGameComponent(
- appContext: MiniGamesAppComponentContext,
- componentContext: ComponentContext,
- override val gameAccessData: GameAccessData,
- private val onCloseGame: (String?) -> Unit,
-) : AbstractComponent(appContext, componentContext), GameComponent {
-
- override fun closeGame() {
- onCloseGame(null)
- }
-
- override fun onErrorLogin() {
- onCloseGame(CONNECT_ERROR_MESSAGE)
- }
-
- override fun onErrorReceive(e: Exception) {
- onCloseGame(RECEIVE_ERROR_MESSAGE)
- }
-
- override fun onErrorSend(e: Exception) {
- onCloseGame(SEND_ERROR_MESSAGE)
- }
-
- override fun points(snapshot: Snapshot): Int = points(username, snapshot)
-
- override fun points(forUser: Username, snapshot: Snapshot): Int = snapshot.points[forUser] ?: 0
-
- override fun userRole(snapshot: Snapshot): UserRole = snapshot.users[username]?.role ?: DEFAULT_USER.role
-
- override fun canEditUser(username: Username, snapshot: Snapshot): Boolean =
- userRole(snapshot) == UserRole.Admin && username != this.username
-
- override fun heartBeat(): HeartBeatClientMessage = HeartBeatClientMessage(timestamp = now())
-
- override fun approve(
- username: Username,
- clientMessages: MutableSharedFlow,
- ) {
- toast("Approving $username")
- userAction(username, UserAction.Approve, clientMessages)
- }
-
- override fun discard(
- username: Username,
- clientMessages: MutableSharedFlow,
- ) {
- toast("Discarding $username")
- userAction(username, UserAction.Discard, clientMessages)
- }
-
- private fun userAction(
- username: Username,
- action: UserAction,
- clientMessages: MutableSharedFlow,
- ) {
- scope.launch {
- val message = UserActionClientMessage(username, action, timestamp = now())
- clientMessages.emit(message)
- }
- }
-}
-
-const val HEARTBEAT_DELAY_MILLIS: Long = 30_000
-
-private val DEFAULT_USER: UserData = UserData.player()
\ No newline at end of file
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/LogInComponent.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/LogInComponent.kt
deleted file mode 100644
index f4ab2999..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/LogInComponent.kt
+++ /dev/null
@@ -1,189 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import com.arkivanov.decompose.ComponentContext
-import com.arkivanov.decompose.value.Value
-import com.russhwolf.settings.ExperimentalSettingsApi
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-import ml.dev.kotlin.minigames.shared.model.UserLogin
-import ml.dev.kotlin.minigames.shared.rest.client.UserClient
-import ml.dev.kotlin.minigames.shared.ui.util.set
-import ml.dev.kotlin.minigames.shared.util.on
-import ml.dev.kotlin.minigames.shared.viewmodel.CONNECT_ERROR_MESSAGE
-import ml.dev.kotlin.minigames.shared.viewmodel.getUserSettings
-import ml.dev.kotlin.minigames.shared.viewmodel.message
-import kotlin.random.Random
-
-interface LogInComponent : Component {
- val serverName: Value
- fun onServerNameChanged(serverName: String)
-
- val username: Value
- fun onUsernameChanged(username: String)
-
- val password: Value
- fun onPasswordChanged(password: String)
-
- val serverNameError: Value
- fun onServerNameErrorChanged(error: Boolean)
-
- val usernameError: Value
- fun onUsernameErrorChanged(error: Boolean)
-
- val passwordError: Value
- fun onPasswordErrorChanged(error: Boolean)
-
- val game: Value
- fun onGameChanged(game: Game)
-
- val rememberUserLogin: Value
- fun onRememberUserLoginChanged(remember: Boolean)
-
- fun loginUser(onError: () -> Unit)
-
- fun verifyInputFields(): Boolean
-
- fun navigatePlayGame()
-
- fun navigateRegister()
-
- fun shuffleGameName()
-}
-
-internal class LogInComponentImpl(
- appContext: MiniGamesAppComponentContext,
- private val componentContext: ComponentContext,
- private val onNavigateRegister: () -> Unit,
- private val onNavigatePlayGame: (game: Game, serverName: String, username: String, password: String) -> Unit,
-) : AbstractComponent(appContext, componentContext), LogInComponent {
- private val client: UserClient = UserClient()
-
- private val _serverName: MutableStateFlow = MutableStateFlow("")
- override val serverName: Value = _serverName.asValue()
- override fun onServerNameChanged(serverName: String) {
- _serverName.value = serverName
- }
-
- private val _username: MutableStateFlow = MutableStateFlow("")
- override val username: Value = _username.asValue()
- override fun onUsernameChanged(username: String) {
- _username.value = username
- }
-
- private val _password: MutableStateFlow = MutableStateFlow("")
- override val password: Value = _password.asValue()
- override fun onPasswordChanged(password: String) {
- _password.value = password
- }
-
- private val _serverNameError: MutableStateFlow = MutableStateFlow(false)
- override val serverNameError: Value = _serverNameError.asValue()
- override fun onServerNameErrorChanged(error: Boolean) {
- _serverNameError.value = error
- }
-
- private val _usernameError: MutableStateFlow = MutableStateFlow(false)
- override val usernameError: Value = _usernameError.asValue()
- override fun onUsernameErrorChanged(error: Boolean) {
- _usernameError.value = error
- }
-
- private val _passwordError: MutableStateFlow = MutableStateFlow(false)
- override val passwordError: Value = _passwordError.asValue()
- override fun onPasswordErrorChanged(error: Boolean) {
- _passwordError.value = error
- }
-
- private val _game: MutableStateFlow = MutableStateFlow(Game.entries.first())
- override val game: Value = _game.asValue()
- override fun onGameChanged(game: Game) {
- _game.value = game
- }
-
- private val _rememberUserLogin: MutableStateFlow = MutableStateFlow(false)
- override val rememberUserLogin: Value = _rememberUserLogin.asValue()
- override fun onRememberUserLoginChanged(remember: Boolean) {
- _rememberUserLogin.value = remember
- }
-
- private val userLogin: UserLogin get() = UserLogin(username.value, password.value)
-
- private val storableServerName: String?
- get() = serverName.value.takeIf { shouldStoreServerName(it) }
-
- init {
- scope.launch {
- loadLoginScreenData()
- if (storableServerName == null) shuffleGameName()
- }
- }
-
- override fun navigateRegister() {
- onNavigateRegister()
- }
-
- override fun verifyInputFields(): Boolean = when {
- _serverName.value.isEmpty() -> true.set(_serverNameError).let { false }
- _username.value.isEmpty() -> true.set(_usernameError).let { false }
- _password.value.isEmpty() -> true.set(_passwordError).let { false }
- else -> false.set(_serverNameError, _usernameError, _passwordError).let { true }
- }
-
- override fun navigatePlayGame() {
- scope.launch(Dispatchers.Main) {
- onNavigatePlayGame(_game.value, _serverName.value, _username.value, _password.value)
- }
- }
-
- override fun shuffleGameName() {
- _serverName.value = _game.value.name + "-" + Random.nextInt(0, 1000)
- }
-
- override fun loginUser(onError: () -> Unit) {
- scope.launch {
- storeLoginScreenData()
- client.loginUser(userLogin).on(
- ok = {
- toast("Logged in")
- navigatePlayGame()
- },
- err = {
- toast(it.reason.message())
- onError()
- },
- empty = {
- toast(CONNECT_ERROR_MESSAGE)
- onError()
- }
- )
- }
- }
-
- @OptIn(ExperimentalSettingsApi::class)
- private suspend fun loadLoginScreenData(): Unit = getUserSettings(appContext).run {
- getStringOrNull(USERNAME_KEY)?.let { _username.value = it }
- getStringOrNull(PASSWORD_KEY)?.let { _password.value = it }
- getStringOrNull(SERVER_NAME_KEY)?.let { _serverName.value = it }
- getBooleanOrNull(REMEMBER_KEY)?.let { _rememberUserLogin.value = it }
- }
-
- @OptIn(ExperimentalSettingsApi::class)
- private suspend fun storeLoginScreenData(): Unit = getUserSettings(appContext).run {
- putString(USERNAME_KEY, if (rememberUserLogin.value) username.value else "")
- putString(PASSWORD_KEY, if (rememberUserLogin.value) password.value else "")
- putString(SERVER_NAME_KEY, storableServerName ?: "")
- putBoolean(REMEMBER_KEY, rememberUserLogin.value)
- }
-}
-
-private fun shouldStoreServerName(name: String): Boolean {
- if (name.isBlank()) return false
-
- return Game.entries.all { !name.startsWith("${it.name}-") }
-}
-
-private const val SERVER_NAME_KEY: String = "serverName"
-private const val REMEMBER_KEY: String = "remember"
-private const val USERNAME_KEY: String = "username"
-private const val PASSWORD_KEY: String = "password"
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/MiniGamesAppComponent.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/MiniGamesAppComponent.kt
deleted file mode 100644
index ff8b3fc5..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/MiniGamesAppComponent.kt
+++ /dev/null
@@ -1,148 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import androidx.compose.material3.SnackbarHostState
-import com.arkivanov.decompose.ComponentContext
-import com.arkivanov.decompose.router.stack.*
-import com.arkivanov.decompose.value.Value
-import kotlinx.serialization.Serializable
-import ml.dev.kotlin.minigames.shared.component.MiniGamesAppComponent.Child
-import ml.dev.kotlin.minigames.shared.model.UserLogin
-import ml.dev.kotlin.minigames.shared.model.Username
-import ml.dev.kotlin.minigames.shared.util.Named
-import ml.dev.kotlin.minigames.shared.websocket.client.GameAccessData
-
-interface MiniGamesAppComponent : Component {
-
- val stack: Value>
-
- val snackbarHostState: SnackbarHostState
-
- fun onBackClicked(toIndex: Int)
-
- sealed interface Child {
- class LogIn(val component: LogInComponent) : Child
- class Register(val component: RegisterComponent) : Child
- sealed interface Game : Child {
- val component: GameComponent<*>
-
- class Snake(override val component: SnakeComponent) : Game
- class Set(override val component: SetComponent) : Game
- class Bird(override val component: BirdComponent) : Game
- }
- }
-}
-
-class MiniGamesAppComponentImpl(
- appContext: MiniGamesAppComponentContext,
- componentContext: ComponentContext,
-) : AbstractComponent(appContext, componentContext), MiniGamesAppComponent {
-
- private val navigation: StackNavigation = StackNavigation()
-
- private val navigationPopAndToastMessage: (String?) -> Unit
- get() = { message -> navigation.pop { message?.let(::toast) } }
-
- override val stack: Value> = childStack(
- source = navigation,
- serializer = Config.serializer(),
- initialConfiguration = Config.LogIn,
- handleBackButton = true,
- childFactory = ::child,
- )
-
- override val snackbarHostState: SnackbarHostState = appContext.snackbarHostState
-
- private fun child(config: Config, childComponentContext: ComponentContext): Child = when (config) {
- is Config.LogIn -> Child.LogIn(
- LogInComponentImpl(
- appContext = appContext,
- componentContext = childComponentContext,
- onNavigateRegister = { navigation.push(Config.Register) },
- onNavigatePlayGame = { game, serverName, username, password ->
- navigation.push(
- when (game) {
- Game.Bird -> Config.Game.Bird(serverName, username, password)
- Game.SnakeIO -> Config.Game.Snake(serverName, username, password)
- Game.Set -> Config.Game.Set(serverName, username, password)
- }
- )
- },
- )
- )
-
- is Config.Register -> Child.Register(
- RegisterComponentImpl(
- appContext = appContext,
- componentContext = childComponentContext,
- onNavigateBack = navigationPopAndToastMessage,
- )
- )
-
- is Config.Game -> {
- val gameAccessData = GameAccessData(config.serverName, UserLogin(config.username, config.password))
- when (config) {
- is Config.Game.Set -> Child.Game.Set(
- SetComponentImpl(
- appContext = appContext,
- componentContext = childComponentContext,
- gameAccessData = gameAccessData,
- onCloseGame = navigationPopAndToastMessage,
- )
- )
-
- is Config.Game.Snake -> Child.Game.Snake(
- SnakeComponentImpl(
- appContext = appContext,
- componentContext = childComponentContext,
- gameAccessData = gameAccessData,
- onCloseGame = navigationPopAndToastMessage,
- )
- )
-
- is Config.Game.Bird -> Child.Game.Bird(
- BirdComponentImpl(
- appContext = appContext,
- componentContext = childComponentContext,
- gameAccessData = gameAccessData,
- onCloseGame = navigationPopAndToastMessage,
- )
- )
- }
- }
- }
-
- override fun onBackClicked(toIndex: Int) {
- navigation.popTo(index = toIndex)
- }
-
- @Serializable
- private sealed interface Config {
- @Serializable
- data object LogIn : Config
-
- @Serializable
- data object Register : Config
-
- @Serializable
- sealed interface Game : Config {
- val serverName: String
- val username: Username
- val password: String
-
- @Serializable
- data class Set(override val serverName: String, override val username: Username, override val password: String) : Game
-
- @Serializable
- data class Snake(override val serverName: String, override val username: Username, override val password: String) : Game
-
- @Serializable
- data class Bird(override val serverName: String, override val username: Username, override val password: String) : Game
- }
- }
-}
-
-enum class Game : Named {
- Bird,
- SnakeIO,
- Set,
-}
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/MiniGamesAppComponentContext.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/MiniGamesAppComponentContext.kt
deleted file mode 100644
index 9039856b..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/MiniGamesAppComponentContext.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import androidx.compose.material3.SnackbarHostState
-
-expect class MiniGamesAppComponentContext {
- fun adjustResize()
- fun adjustPan()
-
- val snackbarHostState: SnackbarHostState
-}
\ No newline at end of file
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/NotificationsComponent.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/NotificationsComponent.kt
deleted file mode 100644
index f656a571..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/NotificationsComponent.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import com.arkivanov.decompose.ComponentContext
-import com.arkivanov.decompose.value.Value
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.update
-
-interface NotificationsComponent : CountingComponent {
- val notifications: Value>
-
- fun addNotification(message: String)
- fun removeNotification(notification: IndexedNotification)
-}
-
-class NotificationsComponentImpl(
- appContext: MiniGamesAppComponentContext,
- componentContext: ComponentContext,
- countPredicate: () -> Boolean,
-) : AbstractCountingComponent(appContext, componentContext, countPredicate), NotificationsComponent {
- private val _notifications: MutableStateFlow> = MutableStateFlow(emptyList())
- override val notifications: Value> = _notifications.asValue()
-
- override fun addNotification(message: String) {
- countNew()
- _notifications.update { it + IndexedNotification(message, it.size) }
- }
-
- override fun removeNotification(notification: IndexedNotification) {
- _notifications.update { it - notification }
- }
-}
-
-data class IndexedNotification(val message: String, val idx: Int)
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/RegisterComponent.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/RegisterComponent.kt
deleted file mode 100644
index 686d61eb..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/RegisterComponent.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import com.arkivanov.decompose.ComponentContext
-import com.arkivanov.decompose.value.Value
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-import ml.dev.kotlin.minigames.shared.model.UserCreate
-import ml.dev.kotlin.minigames.shared.rest.client.UserClient
-import ml.dev.kotlin.minigames.shared.ui.util.set
-import ml.dev.kotlin.minigames.shared.util.on
-import ml.dev.kotlin.minigames.shared.viewmodel.CONNECT_ERROR_MESSAGE
-import ml.dev.kotlin.minigames.shared.viewmodel.message
-
-interface RegisterComponent : Component {
- val email: Value
- fun onEmailChanged(email: String)
-
- val username: Value
- fun onUsernameChanged(username: String)
-
- val password: Value
- fun onPasswordChanged(password: String)
-
- val confirmPassword: Value
- fun onConfirmPasswordChanged(password: String)
-
- val emailError: Value
- fun onEmailErrorChanged(error: Boolean)
-
- val usernameError: Value
- fun onUsernameErrorChanged(error: Boolean)
-
- val passwordError: Value
- fun onPasswordErrorChanged(error: Boolean)
-
- val confirmPasswordError: Value
- fun onConfirmPasswordErrorChanged(error: Boolean)
-
- fun navigateBack()
-
- fun verifyInputFields(): Boolean
-
- fun createUser(onError: () -> Unit)
-}
-
-internal class RegisterComponentImpl(
- appContext: MiniGamesAppComponentContext,
- componentContext: ComponentContext,
- private val onNavigateBack: (message: String?) -> Unit,
-) : AbstractComponent(appContext, componentContext), RegisterComponent {
- private val client: UserClient = UserClient()
-
- private val _email: MutableStateFlow = MutableStateFlow("")
- override val email: Value = _email.asValue()
- override fun onEmailChanged(email: String) {
- _email.value = email
- }
-
- private val _username: MutableStateFlow = MutableStateFlow("")
- override val username: Value = _username.asValue()
- override fun onUsernameChanged(username: String) {
- _username.value = username
- }
-
- private val _password: MutableStateFlow = MutableStateFlow("")
- override val password: Value = _password.asValue()
- override fun onPasswordChanged(password: String) {
- _password.value = password
- }
-
- private val _confirmPassword: MutableStateFlow = MutableStateFlow("")
- override val confirmPassword: Value = _confirmPassword.asValue()
- override fun onConfirmPasswordChanged(password: String) {
- _confirmPassword.value = password
- }
-
- private val _emailError: MutableStateFlow = MutableStateFlow(false)
- override val emailError: Value = _emailError.asValue()
- override fun onEmailErrorChanged(error: Boolean) {
- _emailError.value = error
- }
-
- private val _usernameError: MutableStateFlow = MutableStateFlow(false)
- override val usernameError: Value = _usernameError.asValue()
- override fun onUsernameErrorChanged(error: Boolean) {
- _usernameError.value = error
- }
-
- private val _passwordError: MutableStateFlow = MutableStateFlow(false)
- override val passwordError: Value = _passwordError.asValue()
- override fun onPasswordErrorChanged(error: Boolean) {
- _passwordError.value = error
- }
-
- private val _confirmPasswordError: MutableStateFlow = MutableStateFlow(false)
- override val confirmPasswordError: Value = _confirmPasswordError.asValue()
- override fun onConfirmPasswordErrorChanged(error: Boolean) {
- _confirmPasswordError.value = error
- }
-
- override fun navigateBack() {
- onNavigateBack(null)
- }
-
- override fun verifyInputFields(): Boolean = when {
- _email.value.isEmpty() -> true.set(_emailError).let { false }
- _username.value.isEmpty() -> true.set(_usernameError).let { false }
- _password.value.isEmpty() -> true.set(_passwordError).let { false }
- _confirmPassword.value.isEmpty() -> true.set(_confirmPasswordError).let { false }
- _password.value != _confirmPassword.value -> {
- true.set(_passwordError, _confirmPasswordError)
- toast("Passwords don't match").let { false }
- }
-
- else -> false.set(_emailError, _usernameError, _passwordError, _confirmPasswordError).let { true }
- }
-
- override fun createUser(onError: () -> Unit) {
- scope.launch {
- client.createUser(UserCreate(_email.value, _username.value, _password.value))?.on(
- ok = {
- onNavigateBack("Verify your email and check for spam messages")
- },
- err = {
- toast(it.reason.message())
- onError()
- },
- empty = {
- toast(CONNECT_ERROR_MESSAGE)
- onError()
- }
- )
- }
- }
-}
\ No newline at end of file
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/SetComponent.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/SetComponent.kt
deleted file mode 100644
index 24c74a15..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/SetComponent.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import com.arkivanov.decompose.ComponentContext
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-import ml.dev.kotlin.minigames.shared.api.SET_GAME_WEBSOCKET
-import ml.dev.kotlin.minigames.shared.model.GameStateUpdateClientMessage
-import ml.dev.kotlin.minigames.shared.model.SetGameSnapshot
-import ml.dev.kotlin.minigames.shared.model.SetGameUpdate
-import ml.dev.kotlin.minigames.shared.model.SetProposal
-import ml.dev.kotlin.minigames.shared.util.now
-import ml.dev.kotlin.minigames.shared.websocket.client.GameAccessData
-import ml.dev.kotlin.minigames.shared.websocket.client.GameClient
-
-interface SetComponent : GameComponent {
- fun emitSetProposal(
- cardsIds: Set,
- stateMessages: MutableStateFlow,
- )
-}
-
-internal class SetComponentImpl(
- appContext: MiniGamesAppComponentContext,
- componentContext: ComponentContext,
- gameAccessData: GameAccessData,
- onCloseGame: (String?) -> Unit,
-) : AbstractGameComponent(
- appContext,
- componentContext,
- gameAccessData,
- onCloseGame
-), SetComponent {
-
- override val client: GameClient = GameClient(SET_GAME_WEBSOCKET)
-
- override fun emitSetProposal(
- cardsIds: Set,
- stateMessages: MutableStateFlow,
- ) {
- scope.launch {
- val proposal = SetProposal(cardsIds)
- val update = SetGameUpdate(proposal)
- val message = GameStateUpdateClientMessage(update, timestamp = now())
- stateMessages.emit(message)
- }
- }
-}
\ No newline at end of file
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/SnakeComponent.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/SnakeComponent.kt
deleted file mode 100644
index 7c00e477..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/component/SnakeComponent.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package ml.dev.kotlin.minigames.shared.component
-
-import com.arkivanov.decompose.ComponentContext
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-import ml.dev.kotlin.minigames.shared.api.SNAKE_GAME_WEBSOCKET
-import ml.dev.kotlin.minigames.shared.model.*
-import ml.dev.kotlin.minigames.shared.util.V2
-import ml.dev.kotlin.minigames.shared.util.now
-import ml.dev.kotlin.minigames.shared.websocket.client.GameAccessData
-import ml.dev.kotlin.minigames.shared.websocket.client.GameClient
-
-interface SnakeComponent : GameComponent {
- fun emitDirectionChange(
- dir: V2,
- stateMessages: MutableStateFlow,
- )
-
- fun userSnake(snapshot: SnakeGameSnapshot): Snake?
-}
-
-internal class SnakeComponentImpl(
- appContext: MiniGamesAppComponentContext,
- componentContext: ComponentContext,
- gameAccessData: GameAccessData,
- onCloseGame: (String?) -> Unit,
-) : AbstractGameComponent(
- appContext,
- componentContext,
- gameAccessData,
- onCloseGame,
-), SnakeComponent {
-
- override val client: GameClient = GameClient(SNAKE_GAME_WEBSOCKET)
-
- override fun emitDirectionChange(
- dir: V2,
- stateMessages: MutableStateFlow,
- ) {
- scope.launch {
- val direction = SnakeDirection(dir)
- val update = SnakeGameUpdate(direction)
- val message = GameStateUpdateClientMessage(update, timestamp = now())
- stateMessages.emit(message)
- }
- }
-
- override fun userSnake(snapshot: SnakeGameSnapshot): Snake? = snapshot.snakes[username]
-}
\ No newline at end of file
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/rest/RestApiConfig.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/rest/RestApiConfig.kt
deleted file mode 100644
index eee8adb7..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/rest/RestApiConfig.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package ml.dev.kotlin.minigames.shared.rest
-
-expect object RestApiConfig {
- val host: String
- val scheme: String
-}
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/rest/client/RestApiClient.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/rest/client/RestApiClient.kt
deleted file mode 100644
index cf4c0f3a..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/rest/client/RestApiClient.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package ml.dev.kotlin.minigames.shared.rest.client
-
-import io.ktor.client.*
-import io.ktor.client.call.*
-import io.ktor.client.engine.*
-import io.ktor.client.plugins.*
-import io.ktor.client.plugins.contentnegotiation.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import io.ktor.serialization.kotlinx.cbor.*
-import io.ktor.utils.io.core.*
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.decodeFromByteArray
-import ml.dev.kotlin.minigames.shared.rest.RestApiConfig
-import ml.dev.kotlin.minigames.shared.util.*
-
-class RestApiClient : Closeable {
-
- @OptIn(ExperimentalSerializationApi::class)
- val httpClient = HttpClient(CLIENT_ENGINE_FACTORY) {
- install(ContentNegotiation) { cbor() }
- followRedirects = true
- expectSuccess = true
- }
-
- @OptIn(ExperimentalSerializationApi::class)
- suspend inline fun post(
- path: String,
- block: HttpRequestBuilder.() -> Unit = {},
- ): Res? = try {
- httpClient.post {
- url {
- protocol = URLProtocol.byName[RestApiConfig.scheme]!!
- host = RestApiConfig.host
- contentType(ContentType.Application.Cbor)
- path(path)
- }
- block()
- }
- .body()
- .ok()
- } catch (e: ClientRequestException) {
- tryOrNull {
- val errorData = e.response.readBytes()
- GameSerialization.decodeFromByteArray(errorData).err()
- }
- } catch (_: Exception) {
- null
- }
-
- @OptIn(ExperimentalSerializationApi::class)
- suspend inline fun get(
- path: String,
- block: HttpRequestBuilder.() -> Unit = {},
- ): Res? = try {
- httpClient.get {
- url {
- protocol = URLProtocol.byName[RestApiConfig.scheme]!!
- host = RestApiConfig.host
- contentType(ContentType.Application.Cbor)
- path(path)
- }
- block()
- }
- .body()
- .ok()
- } catch (e: ClientRequestException) {
- tryOrNull {
- val errorData = e.response.readBytes()
- GameSerialization.decodeFromByteArray(errorData).err()
- }
- } catch (_: Exception) {
- null
- }
-
- override fun close(): Unit = httpClient.close()
-}
-
-internal expect val CLIENT_ENGINE_FACTORY: HttpClientEngineFactory<*>
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/rest/client/UserClient.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/rest/client/UserClient.kt
deleted file mode 100644
index 7cd4fd48..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/rest/client/UserClient.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package ml.dev.kotlin.minigames.shared.rest.client
-
-import com.arkivanov.essenty.instancekeeper.InstanceKeeper
-import io.ktor.client.request.*
-import io.ktor.utils.io.core.*
-import ml.dev.kotlin.minigames.shared.api.USER_CREATE_POST
-import ml.dev.kotlin.minigames.shared.api.USER_LOGIN_POST
-import ml.dev.kotlin.minigames.shared.model.JwtToken
-import ml.dev.kotlin.minigames.shared.model.UserCreate
-import ml.dev.kotlin.minigames.shared.model.UserError
-import ml.dev.kotlin.minigames.shared.model.UserLogin
-import ml.dev.kotlin.minigames.shared.util.Res
-
-class UserClient : Closeable, InstanceKeeper.Instance {
-
- private val client = RestApiClient()
-
- suspend fun loginUser(userLogin: UserLogin): Res? =
- client.post(USER_LOGIN_POST) {
- setBody(userLogin)
- }
-
- suspend fun createUser(userCreate: UserCreate): Res? =
- client.post(USER_CREATE_POST) {
- setBody(userCreate)
- }
-
- override fun close(): Unit = client.close()
- override fun onDestroy(): Unit = close()
-}
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Button.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Button.kt
deleted file mode 100644
index d16393d9..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Button.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material3.*
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-
-@Composable
-internal fun CircleButton(
- icon: ImageVector? = null,
- contentDescription: String? = null,
- text: String? = null,
- color: Color = MaterialTheme.colorScheme.inversePrimary,
- onClick: () -> Unit,
-) {
- Surface(
- shape = CircleShape,
- modifier = Modifier
- .padding(16.dp)
- .size(56.dp),
- shadowElevation = 8.dp,
- color = color
- ) {
- Box(
- modifier = Modifier
- .size(52.dp)
- .clickable(onClick = onClick),
- contentAlignment = Alignment.Center
- ) {
- if (icon != null) {
- Icon(
- imageVector = icon,
- contentDescription = contentDescription,
- tint = MaterialTheme.colorScheme.onPrimary,
- )
- }
- if (text != null) {
- Text(
- text = text,
- fontSize = 12.sp,
- fontWeight = FontWeight.Bold
- )
- }
- }
- }
-}
-
-@Composable
-internal fun BackButton(onClick: () -> Unit) {
- IconButton(onClick, modifier = Modifier.size(56.dp)) {
- Icon(Icons.Default.ArrowBack, contentDescription = "back")
- }
-}
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Chat.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Chat.kt
deleted file mode 100644
index fea7dd94..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Chat.kt
+++ /dev/null
@@ -1,209 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.scaleIn
-import androidx.compose.animation.scaleOut
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.text.KeyboardActions
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Send
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.SolidColor
-import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.onKeyEvent
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import com.arkivanov.decompose.extensions.compose.subscribeAsState
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableSharedFlow
-import ml.dev.kotlin.minigames.shared.component.ChatComponent
-import ml.dev.kotlin.minigames.shared.model.GameDataClientMessage
-import ml.dev.kotlin.minigames.shared.model.UserMessage
-import ml.dev.kotlin.minigames.shared.model.Username
-import ml.dev.kotlin.minigames.shared.ui.theme.Shapes
-import ml.dev.kotlin.minigames.shared.ui.theme.Typography
-import ml.dev.kotlin.minigames.shared.ui.util.randomColor
-import ml.dev.kotlin.minigames.shared.util.ComputedMap
-import ml.dev.kotlin.minigames.shared.util.format
-import ml.dev.kotlin.minigames.shared.util.now
-import ml.dev.kotlin.minigames.shared.util.toPaddedString
-
-@Composable
-internal fun Chat(
- component: ChatComponent,
- clientMessages: MutableSharedFlow,
- size: Dp = 54.dp,
- bottomPadding: Dp = 16.dp,
-) {
- BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
- val messagesHeight = maxHeight - size - bottomPadding
- Column(
- modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Bottom
- ) {
- val messages by component.messages.subscribeAsState()
- Messages(messages, component.username, messagesHeight)
-
- val userMessageText by component.userMessageText.subscribeAsState()
- MessageInput(userMessageText, component::onUserMessageTextChange, size, onClick = { component.send(clientMessages) })
- BottomPadding(bottomPadding)
- }
- }
-}
-
-@Composable
-private fun BottomPadding(padding: Dp) {
- Box(modifier = Modifier.fillMaxWidth().height(padding).background(MaterialTheme.colorScheme.background))
-}
-
-@Composable
-private fun MessageInput(
- message: TextFieldValue,
- onMessageChange: (TextFieldValue) -> Unit,
- size: Dp,
- onClick: () -> Unit,
- padding: Dp = 8.dp,
-) {
- BoxWithConstraints(
- modifier = Modifier
- .fillMaxWidth()
- .height(size)
- ) {
- val messageWidth = maxWidth - size
- Row(
- modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.background),
- verticalAlignment = Alignment.CenterVertically
- ) {
- BasicTextField(
- value = message,
- onValueChange = onMessageChange,
- textStyle = Typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary),
- singleLine = true,
- cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
- keyboardActions = KeyboardActions(
- onDone = { onClick() },
- ),
- modifier = Modifier
- .width(messageWidth)
- .padding(start = padding, top = padding, bottom = padding)
- .clip(Shapes.large)
- .background(MaterialTheme.colorScheme.surface)
- .padding(horizontal = padding * 2, vertical = padding)
- .onKeyEvent {
- if (it.key.keyCode == Key.Enter.keyCode) true.also { onClick() } else false
- }
- )
- Box(
- modifier = Modifier.fillMaxHeight().padding(padding)
- ) {
- IconButton(onClick) {
- Icon(imageVector = Icons.Default.Send, contentDescription = "send")
- }
- }
-
- }
- }
-}
-
-@Composable
-private fun Messages(messages: List, username: Username, height: Dp) {
- val state = rememberLazyListState()
- LazyColumn(
- modifier = Modifier.fillMaxWidth().height(height),
- state = state,
- verticalArrangement = Arrangement.Top,
- horizontalAlignment = Alignment.Start,
- ) {
- itemsIndexed(messages, key = { _, msg -> msg.uuid }) { idx, msg ->
- Message(msg, username, onVisible = { state.animateScrollToItem(idx) })
- }
- }
-}
-
-@Composable
-private fun Message(
- message: UserMessage,
- username: Username,
- contentPart: Float = 0.9f,
- animationDuration: Int = 300,
- paddingMultiply: Float = 1.5f,
- onVisible: suspend () -> Unit,
-) {
- val isAuthor = message.author == username
- val background = if (isAuthor) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.inversePrimary
- val align = if (isAuthor) Alignment.CenterEnd else Alignment.CenterStart
- val transformOrigin = if (isAuthor) TransformOrigin(1f, 0.5f) else TransformOrigin(0f, 0.5f)
- val animationSpec = tween(animationDuration, easing = LinearEasing)
- var visible by remember(message) { mutableStateOf(false) }
- LaunchedEffect(Unit) {
- delay(message.timestamp - now())
- visible = true
- onVisible()
- }
- AnimatedVisibility(
- visible,
- enter = scaleIn(transformOrigin = transformOrigin, animationSpec = animationSpec),
- exit = scaleOut(transformOrigin = transformOrigin, animationSpec = animationSpec)
- ) {
- Box(
- modifier = Modifier.fillMaxWidth(), contentAlignment = align
- ) {
- Box(
- modifier = Modifier.fillMaxWidth(contentPart), contentAlignment = align
- ) {
- Surface(
- shape = Shapes.large,
- color = background,
- shadowElevation = 4.dp,
- modifier = Modifier.padding(4.dp)
- ) {
- Box(modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) {
- if (!isAuthor) Text(
- text = message.author,
- style = Typography.titleSmall,
- color = USER_COLOR[message.author],
- modifier = Modifier.align(Alignment.TopStart)
- )
- Text(
- message.message,
- style = Typography.bodyLarge,
- modifier = Modifier
- .align(Alignment.CenterEnd)
- .padding(
- start = 0.dp,
- end = 0.dp,
- top = if (!isAuthor) with(LocalDensity.current) { Typography.titleSmall.fontSize.toDp() * paddingMultiply } else 0.dp,
- bottom = with(LocalDensity.current) { Typography.bodySmall.fontSize.toDp() * paddingMultiply },
- )
- )
- Text(
- text = message.timestamp.format { "${hour.toPaddedString()}:${minute.toPaddedString()}" },
- style = Typography.bodySmall,
- color = MaterialTheme.colorScheme.onPrimary,
- modifier = Modifier.align(Alignment.BottomEnd)
- )
- }
- }
- }
- }
- }
-}
-
-private val USER_COLOR: ComputedMap = ComputedMap { randomColor() }
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/DropdownMenu.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/DropdownMenu.kt
deleted file mode 100644
index 4304d894..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/DropdownMenu.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowDropDown
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.unit.dp
-import ml.dev.kotlin.minigames.shared.ui.theme.Shapes
-import ml.dev.kotlin.minigames.shared.ui.theme.Typography
-import ml.dev.kotlin.minigames.shared.util.Named
-
-@Composable
-internal fun DropdownMenu(
- selected: T,
- onChanged: (T) -> Unit,
- anyItems: Collection,
-) {
- val items = anyItems.toList()
- var expanded by remember { mutableStateOf(false) }
- BoxWithConstraints(
- modifier = Modifier
- .fillMaxWidth()
- .wrapContentSize(Alignment.TopStart)
- ) {
- val dropDownWidth = maxWidth
- Box(
- modifier = Modifier.fillMaxWidth(),
- contentAlignment = Alignment.CenterEnd
- ) {
- Text(
- text = selected.name,
- modifier = Modifier
- .fillMaxWidth()
- .clip(Shapes.medium)
- .clickable(onClick = { expanded = true })
- .background(MaterialTheme.colorScheme.inversePrimary)
- .padding(16.dp),
- color = MaterialTheme.colorScheme.onPrimary,
- style = Typography.titleMedium,
- )
- Icon(
- imageVector = Icons.Default.ArrowDropDown,
- contentDescription = "dropdown",
- modifier = Modifier.padding(16.dp),
- tint = MaterialTheme.colorScheme.onPrimary
- )
- }
- DropdownMenu(
- expanded = expanded,
- onDismissRequest = { expanded = false },
- modifier = Modifier
- .background(MaterialTheme.colorScheme.inversePrimary)
- .width(dropDownWidth)
- ) {
- items.forEach {
- DropdownMenuItem(onClick = {
- onChanged(it)
- expanded = false
- }) {
- Text(
- text = it.name,
- color = MaterialTheme.colorScheme.onPrimary,
- style = Typography.titleMedium,
- )
- }
- }
- }
- }
-}
-
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/DropdownMenuInternal.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/DropdownMenuInternal.kt
deleted file mode 100644
index f8f954c9..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/DropdownMenuInternal.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-
-@Composable
-internal expect fun DropdownMenu(
- expanded: Boolean,
- onDismissRequest: () -> Unit,
- modifier: Modifier,
- content: @Composable ColumnScope.() -> Unit,
-)
-
-@Composable
-internal expect fun DropdownMenuItem(
- onClick: () -> Unit,
- content: @Composable () -> Unit,
-)
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/FormField.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/FormField.kt
deleted file mode 100644
index 0ebf6674..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/FormField.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.text.KeyboardActions
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Visibility
-import androidx.compose.material.icons.filled.VisibilityOff
-import androidx.compose.material3.*
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.onKeyEvent
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.text.input.VisualTransformation
-
-@Composable
-internal fun FormField(
- text: String,
- textInput: String,
- onTextInputChange: (String) -> Unit,
- errorState: Boolean,
- onErrorStateChange: (Boolean) -> Unit,
- buttonType: FormFieldButtonType = FormFieldButtonType.Next,
- password: Boolean = false,
- inputRegex: Regex = Regex("[^\\s]*"),
- trailingIcon: @Composable (() -> Unit)? = null,
-) {
- val focusManager = LocalFocusManager.current
- val showPassword = remember { mutableStateOf(false) }
-
- OutlinedTextField(
- value = textInput,
- onValueChange = {
- onErrorStateChange(false)
- onTextInputChange(if (inputRegex.matches(it)) it else textInput)
- },
- isError = errorState,
- modifier = Modifier
- .fillMaxWidth()
- .onKeyEvent { handleTabKey(it, focusManager) },
- label = { Text(text = text) },
- shape = MaterialTheme.shapes.medium,
- singleLine = true,
- visualTransformation = if (password && !showPassword.value) PasswordVisualTransformation() else VisualTransformation.None,
- keyboardOptions = KeyboardOptions.Default.run {
- if (password) copy(keyboardType = KeyboardType.Password) else this
- }.copy(imeAction = buttonType.imeAction),
- keyboardActions = KeyboardActions(
- onNext = on(buttonType == FormFieldButtonType.Next) { focusManager.moveFocus(FocusDirection.Down) },
- onDone = on(buttonType == FormFieldButtonType.Done) { focusManager.clearFocus() },
- ),
- trailingIcon = trailingIcon ?: if (password) {
- { PasswordIcon(showPassword) }
- } else null,
- )
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-private fun handleTabKey(event: KeyEvent, focusManager: FocusManager): Boolean =
- if (event.key.keyCode == Key.Tab.keyCode && moveDownOnTab()) {
- focusManager.moveFocus(FocusDirection.Down)
- } else false
-
-enum class FormFieldButtonType(val imeAction: ImeAction) { Next(ImeAction.Next), Done(ImeAction.Done) }
-
-private fun on(
- condition: Boolean,
- action: (A) -> Unit,
-): (A) -> Unit = if (condition) action else fun(_: A) {}
-
-@Composable
-private fun PasswordIcon(showPassword: MutableState) {
- IconButton(onClick = { showPassword.value = !showPassword.value }) {
- Icon(
- imageVector = if (showPassword.value) Icons.Default.Visibility else Icons.Default.VisibilityOff,
- contentDescription = "password"
- )
- }
-}
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/FormFieldInternal.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/FormFieldInternal.kt
deleted file mode 100644
index 19e0d58a..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/FormFieldInternal.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-internal expect fun moveDownOnTab(): Boolean
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/GameTopBar.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/GameTopBar.kt
deleted file mode 100644
index 2cef02d6..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/GameTopBar.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import ml.dev.kotlin.minigames.shared.model.UserRole
-import ml.dev.kotlin.minigames.shared.ui.theme.Typography
-
-@Composable
-internal fun GameTopBar(
- points: Int,
- role: UserRole,
- onClose: () -> Unit,
-) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column {
- AnimatedContent(targetState = points) { targetPoints ->
- Text(
- text = "$targetPoints point${if (targetPoints == 1) "" else "s"}",
- style = Typography.headlineSmall.copy(fontWeight = FontWeight.Bold)
- )
- }
- Text(
- text = "Role: $role",
- style = Typography.titleSmall,
- modifier = Modifier.padding(top = 4.dp),
- )
- }
- IconButton(onClick = onClose, modifier = Modifier.size(36.dp)) {
- ShadowIcon(
- imageVector = Icons.Default.Close,
- contentDescription = "close",
- size = 36.dp,
- )
- }
- }
-}
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Icon.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Icon.kt
deleted file mode 100644
index b4973c6d..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Icon.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.size
-import androidx.compose.material3.Icon
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-
-@Composable
-internal fun ShadowIcon(
- imageVector: ImageVector,
- contentDescription: String,
- size: Dp,
- elevation: Dp = 1.dp,
-) {
- Box(modifier = Modifier.size(size + elevation)) {
- Icon(
- imageVector = imageVector,
- contentDescription = contentDescription,
- modifier = Modifier
- .size(size)
- .offset(
- x = elevation,
- y = elevation
- )
- .alpha(0.5f),
- tint = Color.Black,
- )
- Icon(
- imageVector = imageVector,
- contentDescription = contentDescription,
- modifier = Modifier.size(size),
- )
- }
-}
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Loading.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Loading.kt
deleted file mode 100644
index fa76a02e..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Loading.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.animation.core.*
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-
-@Composable
-internal fun DotsTyping(
- dotSize: Dp = 16.dp,
- spaceSize: Dp = 8.dp,
- delayMillis: Int = 300,
- maxOffset: Float = 10f,
-) {
- @Composable
- fun Dot(offset: Float) {
- Spacer(
- Modifier
- .size(dotSize)
- .offset(y = -offset.dp)
- .background(
- color = MaterialTheme.colorScheme.primary,
- shape = CircleShape
- )
- .shadow(dotSize)
- )
- }
-
- val infiniteTransition = rememberInfiniteTransition()
-
- @Composable
- fun animateOffsetWithDelay(delay: Int): State = infiniteTransition.animateFloat(
- initialValue = 0f,
- targetValue = 0f,
- animationSpec = infiniteRepeatable(
- animation = keyframes {
- durationMillis = delayMillis * 4
- 0f at delay with LinearEasing
- maxOffset at delay + delayMillis with LinearEasing
- 0f at delay + delayMillis * 2
- }
- )
- )
-
- val offset1 by animateOffsetWithDelay(0)
- val offset2 by animateOffsetWithDelay(delayMillis)
- val offset3 by animateOffsetWithDelay(delayMillis * 2)
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center,
- modifier = Modifier.padding(top = maxOffset.dp)
- ) {
- Dot(offset1)
- Spacer(Modifier.width(spaceSize))
- Dot(offset2)
- Spacer(Modifier.width(spaceSize))
- Dot(offset3)
- }
-}
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Notifications.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Notifications.kt
deleted file mode 100644
index 66e12fba..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Notifications.kt
+++ /dev/null
@@ -1,144 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.MutableTransitionState
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.scaleIn
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material3.*
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
-import com.arkivanov.decompose.extensions.compose.subscribeAsState
-import com.arkivanov.decompose.value.getValue
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import ml.dev.kotlin.minigames.shared.component.IndexedNotification
-import ml.dev.kotlin.minigames.shared.component.NotificationsComponent
-import ml.dev.kotlin.minigames.shared.ui.theme.DiscardColor
-import ml.dev.kotlin.minigames.shared.ui.theme.Shapes
-import ml.dev.kotlin.minigames.shared.ui.theme.Typography
-
-@Composable
-internal fun Notifications(
- component: NotificationsComponent,
-) {
- val notifications by component.notifications.subscribeAsState()
- LazyColumn(
- modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.Top
- ) {
- items(notifications.asReversed(), key = { it.idx }) { notification ->
- Notification(notification, onRemove = { component.removeNotification(notification) })
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun Notification(
- notification: IndexedNotification,
- onRemove: suspend () -> Unit,
- animationDuration: Int = 300,
-) {
- val visible = remember { MutableTransitionState(true) }
- val scope = rememberCoroutineScope()
- val removeAndHide = {
- scope.launch {
- visible.targetState = false
- delay(animationDuration.toLong())
- onRemove()
- }
- }
- val dismissState = rememberDismissState(confirmValueChange = {
- when (it) {
- DismissValue.Default -> Unit
- DismissValue.DismissedToEnd -> removeAndHide()
- DismissValue.DismissedToStart -> removeAndHide()
- }
- true
- })
-
- @Composable
- fun NotificationDataRaw() {
- Card(
- modifier = Modifier.fillMaxWidth(),
- elevation = CardDefaults.cardElevation(4.dp),
- shape = Shapes.small,
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .background(MaterialTheme.colorScheme.surface)
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = notification.message,
- style = Typography.bodyLarge,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.fillMaxWidth()
- )
- }
- }
- }
-
- val animationSpec = tween(animationDuration, easing = LinearEasing)
- AnimatedVisibility(
- visible,
- enter = scaleIn(transformOrigin = TransformOrigin(0.5f, 0f), animationSpec = animationSpec),
- exit = fadeOut(animationSpec = animationSpec)
- ) {
- SwipeToDismiss(
- state = dismissState,
- background = {
- val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
- val color by animateColorAsState(
- targetValue = when (dismissState.targetValue) {
- DismissValue.Default -> MaterialTheme.colorScheme.surface
- DismissValue.DismissedToEnd -> DiscardColor
- DismissValue.DismissedToStart -> DiscardColor
- }
- )
- val icon = when (direction) {
- DismissDirection.StartToEnd -> Icons.Default.Delete
- DismissDirection.EndToStart -> Icons.Default.Delete
- }
- val scale by animateFloatAsState(
- targetValue = if (dismissState.targetValue == DismissValue.Default) 0.8f else 1f
- )
- val alignment = when (direction) {
- DismissDirection.StartToEnd -> Alignment.CenterStart
- DismissDirection.EndToStart -> Alignment.CenterEnd
- }
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(color)
- .padding(start = 12.dp, end = 12.dp),
- contentAlignment = alignment
- ) {
- Icon(icon, contentDescription = "icon", modifier = Modifier.scale(scale))
- }
- },
- dismissContent = { NotificationDataRaw() }
- )
- }
-}
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Players.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Players.kt
deleted file mode 100644
index 799a50e5..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Players.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import kotlinx.coroutines.flow.MutableSharedFlow
-import ml.dev.kotlin.minigames.shared.component.GameComponent
-import ml.dev.kotlin.minigames.shared.model.GameDataClientMessage
-import ml.dev.kotlin.minigames.shared.model.GameSnapshot
-import ml.dev.kotlin.minigames.shared.model.UserData
-import ml.dev.kotlin.minigames.shared.model.Username
-
-@Composable
-internal fun Players(
- component: GameComponent,
- snapshot: Snapshot,
- clientMessages: MutableSharedFlow,
-) {
- val listState = rememberLazyListState()
- val users = snapshot.users.entries
- .sortedByDescending { component.points(it.key, snapshot) }
- .map { IndexedUserData(it.key, it.value) }.let {
- if (component.username in snapshot.users) it
- else it + IndexedUserData(component.username, UserData.player())
- }
-
- LazyColumn(
- modifier = Modifier.fillMaxWidth(),
- state = listState
- ) {
- items(items = users, key = { it.username }) { data ->
- UserDataRow(
- username = data.username,
- userData = data.userData,
- userPoints = component.points(data.username, snapshot),
- canEdit = component.canEditUser(data.username, snapshot),
- onApprove = { component.approve(data.username, clientMessages) },
- onDiscard = { component.discard(data.username, clientMessages) },
- )
- }
- }
-}
-
-private data class IndexedUserData(val username: Username, val userData: UserData)
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/ProportionKeeper.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/ProportionKeeper.kt
deleted file mode 100644
index 8fd3280b..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/ProportionKeeper.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.foundation.layout.*
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-
-@Composable
-internal fun ProportionKeeper(
- maxWidthToHeight: Float = 0.66f,
- content: @Composable BoxScope.() -> Unit,
-) {
- BoxWithConstraints(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- val proportion = maxWidth.value / maxHeight.value
- val innerWidth = if (proportion > maxWidthToHeight) maxHeight * maxWidthToHeight else maxWidth
- Box(modifier = Modifier.size(innerWidth, maxHeight), content = content)
- }
-}
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/RecompositionHighlighter.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/RecompositionHighlighter.kt
deleted file mode 100644
index b400ceeb..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/RecompositionHighlighter.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.draw.drawWithCache
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.SolidColor
-import androidx.compose.ui.graphics.drawscope.Fill
-import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.graphics.lerp
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.delay
-import kotlin.math.min
-
-/**
- * A [Modifier] that draws a border around elements that are recomposing. The border increases in
- * size and interpolates from red to green as more recompositions occur before a timeout.
- */
-@Stable
-fun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier)
-
-// Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations
-// Modifier.composed will still remember unique data per call site.
-private val recomposeModifier =
- Modifier.composed(inspectorInfo = debugInspectorInfo { name = "recomposeHighlighter" }) {
- // The total number of compositions that have occurred. We're not using a State<> here be
- // able to read/write the value without invalidating (which would cause infinite
- // recomposition).
- val totalCompositions = remember { arrayOf(0L) }
- totalCompositions[0]++
-
- // The value of totalCompositions at the last timeout.
- val totalCompositionsAtLastTimeout = remember { mutableStateOf(0L) }
-
- // Start the timeout, and reset everytime there's a recomposition. (Using totalCompositions
- // as the key is really just to cause the timer to restart every composition).
- LaunchedEffect(totalCompositions[0]) {
- delay(3000)
- totalCompositionsAtLastTimeout.value = totalCompositions[0]
- }
-
- Modifier.drawWithCache {
- onDrawWithContent {
- // Draw actual content.
- drawContent()
-
- // Below is to draw the highlight, if necessary. A lot of the logic is copied from
- // Modifier.border
- val numCompositionsSinceTimeout =
- totalCompositions[0] - totalCompositionsAtLastTimeout.value
-
- val hasValidBorderParams = size.minDimension > 0f
- if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) {
- return@onDrawWithContent
- }
-
- val (color, strokeWidthPx) =
- when (numCompositionsSinceTimeout) {
- // We need at least one composition to draw, so draw the smallest border
- // color in blue.
- 1L -> Color.Blue to 1f
- // 2 compositions is _probably_ okay.
- 2L -> Color.Green to 2.dp.toPx()
- // 3 or more compositions before timeout may indicate an issue. lerp the
- // color from yellow to red, and continually increase the border size.
- else -> {
- lerp(
- Color.Yellow.copy(alpha = 0.8f),
- Color.Red.copy(alpha = 0.5f),
- min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f)
- ) to numCompositionsSinceTimeout.toInt().dp.toPx()
- }
- }
-
- val halfStroke = strokeWidthPx / 2
- val topLeft = Offset(halfStroke, halfStroke)
- val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
-
- val fillArea = (strokeWidthPx * 2) > size.minDimension
- val rectTopLeft = if (fillArea) Offset.Zero else topLeft
- val size = if (fillArea) size else borderSize
- val style = if (fillArea) Fill else Stroke(strokeWidthPx)
-
- drawRect(
- brush = SolidColor(color),
- topLeft = rectTopLeft,
- size = size,
- style = style
- )
- }
- }
- }
\ No newline at end of file
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/ScrollScreen.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/ScrollScreen.kt
deleted file mode 100644
index d5c241cc..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/ScrollScreen.kt
+++ /dev/null
@@ -1,232 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.expandIn
-import androidx.compose.animation.shrinkOut
-import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.Orientation
-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.shadow
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.arkivanov.decompose.extensions.compose.subscribeAsState
-import com.arkivanov.decompose.value.Value
-import com.arkivanov.essenty.backhandler.BackCallback
-import com.arkivanov.essenty.backhandler.BackHandler
-import `in`.procyk.compose.util.SystemBarsScreen
-import kotlinx.coroutines.launch
-import ml.dev.kotlin.minigames.shared.ui.util.ConstantValue
-import ml.dev.kotlin.minigames.shared.util.unit
-import ml.dev.kotlin.minigames.shared.util.zip
-import kotlin.math.roundToInt
-
-internal class ScrollScreenSection private constructor(
- val icon: ImageVector,
- val iconSelected: ImageVector,
- val iconCount: Value,
- val onSelected: () -> Unit,
- val screen: @Composable (BoxScope.() -> Unit),
-) {
- companion object {
- fun section(
- icon: ImageVector,
- iconSelected: ImageVector,
- iconCount: Value = ConstantValue(0),
- onSelected: () -> Unit = {},
- screen: @Composable BoxScope.() -> Unit,
- ): ScrollScreenSection = ScrollScreenSection(icon, iconSelected, iconCount, onSelected, screen)
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-internal fun ScrollScreen(
- selectedScreen: MutableState,
- swipeState: SwipeableState,
- up: @Composable BoxScope.() -> Unit,
- left: ScrollScreenSection,
- center: ScrollScreenSection,
- right: ScrollScreenSection,
- backHandler: BackHandler,
- onUp: () -> Unit,
- onDown: () -> Unit,
- threshold: Float = 0.3f,
- scrollIconSize: Dp = 32.dp,
- iconPadding: Dp = 16.dp,
-) {
- with(LocalDensity.current) {
- SystemBarsScreen(
- top = MaterialTheme.colorScheme.surface,
- bottom = MaterialTheme.colorScheme.surface,
- ) {
- BoxWithConstraints {
- val fullHeight = maxHeight
- val fullWidth = maxWidth
- val height = fullHeight - scrollIconSize - (iconPadding * 2)
- val scope = rememberCoroutineScope()
- val sections = remember { listOf(left, center, right) }
- val iconSection = when (selectedScreen.value) {
- SelectedScreen.LEFT -> listOf(left.iconSelected, center.icon, right.icon)
- SelectedScreen.CENTER -> listOf(left.icon, center.iconSelected, right.icon)
- SelectedScreen.RIGHT -> listOf(left.icon, center.icon, right.iconSelected)
- }
- .let { icons -> SelectedScreen.values().zip(icons, sections) }
-
- val scrollOffset by animateDpAsState(targetValue = -fullWidth * selectedScreen.value.ordinal)
- val handler = remember {
- object : BackCallback() {
- override fun onBack() = scope.launch { swipeState.animateTo(ScreenLocation.UP) }.unit()
- }.also(backHandler::register)
- }
- when (swipeState.targetValue) {
- ScreenLocation.DOWN -> {
- handler.isEnabled = true
- onDown()
- }
-
- ScreenLocation.UP -> {
- handler.isEnabled = true
- onUp()
- }
- }
-
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(fullHeight)
- ) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(height)
- .background(MaterialTheme.colorScheme.surface),
- content = up
- )
-
- Box(
- modifier = Modifier
- .offset { IntOffset(0, swipeState.offset.value.roundToInt()) }
- .fillMaxWidth()
- .height(fullHeight)
- .background(MaterialTheme.colorScheme.surface)
- .swipeable(
- state = swipeState,
- anchors = mapOf(0f to ScreenLocation.DOWN, height.toPx() to ScreenLocation.UP),
- thresholds = { _, _ -> FractionalThreshold(threshold) },
- orientation = Orientation.Vertical
- )
- ) {
- Column(modifier = Modifier.fillMaxSize()) {
- Surface(modifier = Modifier.shadow(elevation = 2.dp)) {
- Row(
- modifier = Modifier
- .background(MaterialTheme.colorScheme.background)
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceEvenly
- ) {
- iconSection.forEach { (screen, icon, section) ->
- val iconCount by section.iconCount.subscribeAsState()
- BottomIcon(icon, iconPadding, scrollIconSize, iconCount, onClick = {
- selectedScreen.value = screen
- if (swipeState.targetValue == ScreenLocation.UP) scope.launch {
- swipeState.animateTo(ScreenLocation.DOWN)
- }
- section.onSelected()
- })
- }
- }
- }
- Row(
- modifier = Modifier
- .offset(scrollOffset)
- .wrapContentWidth(unbounded = true, align = Alignment.Start)
- ) {
- sections.forEach {
- Box(
- modifier = Modifier
- .width(fullWidth)
- .height(height),
- content = it.screen
- )
- }
- }
- }
- }
- }
- }
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun BottomIcon(
- icon: ImageVector,
- padding: Dp,
- iconsSize: Dp,
- badgeCount: Int = 0,
- onClick: () -> Unit,
-) {
- CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
- BadgedBox(
- badge = {
- AnimatedVisibility(
- visible = badgeCount > 0,
- enter = expandIn(
- animationSpec = tween(durationMillis = 200, easing = LinearEasing),
- expandFrom = Alignment.Center
- ),
- exit = shrinkOut(
- animationSpec = tween(durationMillis = 300, easing = LinearEasing),
- shrinkTowards = Alignment.Center
- ),
- ) {
- Badge(
- modifier = Modifier.offset(
- x = (-20).dp,
- y = 44.dp,
- ),
- containerColor = Color.Red,
- contentColor = Color.White,
- ) {
- val badgeText = if (badgeCount > 999) "999+" else "$badgeCount"
- Text(
- text = badgeText,
- fontSize = 10.sp,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(2.dp)
- )
- }
- }
- },
- ) {
- IconButton(
- onClick = onClick,
- modifier = Modifier.padding(padding)
- ) {
- ShadowIcon(
- imageVector = icon,
- contentDescription = "selectIcon",
- size = iconsSize
- )
- }
- }
- }
-}
-
-enum class SelectedScreen { LEFT, CENTER, RIGHT }
-
-enum class ScreenLocation { UP, DOWN }
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/SizedCanvas.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/SizedCanvas.kt
deleted file mode 100644
index 41d6241f..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/SizedCanvas.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.unit.Dp
-
-@Composable
-internal fun SizedCanvas(width: Dp, height: Dp, modifier: Modifier = Modifier, onDraw: DrawScope.() -> Unit) {
- Canvas(
- modifier = Modifier
- .height(height)
- .width(width)
- .then(modifier),
- onDraw
- )
-}
\ No newline at end of file
diff --git a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Swipeable.kt b/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Swipeable.kt
deleted file mode 100644
index b7467ce4..00000000
--- a/shared-client/src/commonMain/kotlin/ml/dev/kotlin/minigames/shared/ui/component/Swipeable.kt
+++ /dev/null
@@ -1,879 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package ml.dev.kotlin.minigames.shared.ui.component
-
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.SpringSpec
-import androidx.compose.foundation.gestures.DraggableState
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.draggable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.runtime.*
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.launch
-import ml.dev.kotlin.minigames.shared.ui.component.SwipeableDefaults.AnimationSpec
-import ml.dev.kotlin.minigames.shared.ui.component.SwipeableDefaults.StandardResistanceFactor
-import ml.dev.kotlin.minigames.shared.ui.component.SwipeableDefaults.VelocityThreshold
-import ml.dev.kotlin.minigames.shared.ui.component.SwipeableDefaults.resistanceConfig
-import kotlin.math.PI
-import kotlin.math.abs
-import kotlin.math.sign
-import kotlin.math.sin
-
-/**
- * State of the [swipeable] modifier.
- *
- * This contains necessary information about any ongoing swipe or animation and provides methods
- * to change the state either immediately or by starting an animation. To create and remember a
- * [SwipeableState] with the default animation clock, use [rememberSwipeableState].
- *
- * @param initialValue The initial value of the state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
- */
-@Stable
-@ExperimentalMaterial3Api
-internal open class SwipeableState(
- initialValue: T,
- internal val animationSpec: AnimationSpec = AnimationSpec,
- internal val confirmStateChange: (newValue: T) -> Boolean = { true },
-) {
- /**
- * The current value of the state.
- *
- * If no swipe or animation is in progress, this corresponds to the anchor at which the
- * [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds
- * the last anchor at which the [swipeable] was settled before the swipe or animation started.
- */
- var currentValue: T by mutableStateOf(initialValue)
- private set
-
- /**
- * Whether the state is currently animating.
- */
- var isAnimationRunning: Boolean by mutableStateOf(false)
- private set
-
- /**
- * The current position (in pixels) of the [swipeable].
- *
- * You should use this state to offset your content accordingly. The recommended way is to
- * use `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled.
- */
- val offset: State get() = offsetState
-
- /**
- * The amount by which the [swipeable] has been swiped past its bounds.
- */
- val overflow: State get() = overflowState
-
- // Use `Float.NaN` as a placeholder while the state is uninitialised.
- private val offsetState = mutableStateOf(0f)
- private val overflowState = mutableStateOf(0f)
-
- // the source of truth for the "real"(non ui) position
- // basically position in bounds + overflow
- private val absoluteOffset = mutableStateOf(0f)
-
- // current animation target, if animating, otherwise null
- private val animationTarget = mutableStateOf(null)
-
- internal var anchors by mutableStateOf(emptyMap())
-
- private val latestNonEmptyAnchorsFlow: Flow