diff --git a/.github/workflows/allpublish.yml b/.github/workflows/allpublish.yml new file mode 100644 index 0000000..805ec89 --- /dev/null +++ b/.github/workflows/allpublish.yml @@ -0,0 +1,14 @@ +name: All Publish +on: + workflow_dispatch: null +jobs: + call-kmmbridge-publish: + permissions: + contents: write + packages: write + uses: touchlab/KMMBridgeGithubWorkflow/.github/workflows/faktorybuild.yml@v0.9 + with: + publishTask: kmmBridgePublish + publishKotlinMultiplatformPublicationToGitHubPackagesRepository + publishAndroidReleasePublicationToGitHubPackagesRepository + jvmVersion: '17' diff --git a/.github/workflows/kmmbridgepnblish.yml b/.github/workflows/kmmbridgepnblish.yml new file mode 100644 index 0000000..1b743d4 --- /dev/null +++ b/.github/workflows/kmmbridgepnblish.yml @@ -0,0 +1,11 @@ +name: KMMBridge Publish Release +on: workflow_dispatch +permissions: + contents: write + packages: write + +jobs: + call-kmmbridge-publish: + uses: touchlab/KMMBridgeGithubWorkflow/.github/workflows/faktorybuild.yml@v0.9 + with: + jvmVersion: '17' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e510fa9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +.idea +.DS_Store +build +captures +.externalNativeBuild +.cxx +local.properties +xcuserdata \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..9ec69c4 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,29 @@ +@Suppress("DSL_SCOPE_VIOLATION") // Remove when fixed https://youtrack.jetbrains.com/issue/KTIJ-19369 +plugins { + alias(libs.plugins.compose.multiplatform) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.kmmbridge) apply false +} + +tasks.register("clean", Delete::class) { + delete(rootProject.buildDir) +} + +buildscript { + dependencies { + classpath(libs.atomicfu.gradle.plugin) + } +} + +subprojects { + val GROUP: String by project + val LIBRARY_VERSION: String by project + + group = GROUP + version = LIBRARY_VERSION +} + +allprojects { + apply(plugin = "kotlinx-atomicfu") +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1227cea --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +#Gradle +org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" + +#Kotlin +kotlin.code.style=official + +#Android +android.useAndroidX=true +android.nonTransitiveRClass=true + +#MPP +kotlin.mpp.enableCInteropCommonization=true +kotlin.mpp.androidSourceSetLayoutVersion=2 +org.jetbrains.compose.experimental.uikit.enabled=true +kotlin.native.cacheKind=none + +LIBRARY_VERSION=0.1-alpha1 +GROUP=github.abhi165.noober \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..444655b --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,34 @@ +[versions] +activity-compose = "1.7.2" +appcompat = "1.6.1" +atomicfu-gradle-plugin = "0.17.3" +core-ktx = "1.10.1" +kotlinx-coroutines-core = "1.6.4" +kotlinx-datetime = "0.4.1" +okhttp = "4.11.0" +okio = "3.2.0" +precompose = "1.5.0" +security-crypto = "1.1.0-alpha06" +kotlin-version = "1.9.0" +agp-version = "8.0.1" +compose-version = "1.4.3" +kmmbridge = "0.3.7" + +[libraries] +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } +appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +atomicfu-gradle-plugin = { module = "org.jetbrains.kotlinx:atomicfu-gradle-plugin", version.ref = "atomicfu-gradle-plugin" } +core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +okio = { module = "com.squareup.okio:okio", version.ref = "okio" } +precompose = { module = "moe.tlaster:precompose", version.ref = "precompose" } +security-crypto = { module = "androidx.security:security-crypto", version.ref = "security-crypto" } + +[plugins] +android-library = { id = "com.android.library", version.ref = "agp-version" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin-version" } +compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-version" } +jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" } +kmmbridge = { id = "co.touchlab.faktory.kmmbridge", version.ref = "kmmbridge" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8eb59ed --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Aug 26 11:44:49 IST 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + 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" + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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=. +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%" == "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%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/noober-no-op/build.gradle.kts b/noober-no-op/build.gradle.kts new file mode 100644 index 0000000..c9dfc19 --- /dev/null +++ b/noober-no-op/build.gradle.kts @@ -0,0 +1,76 @@ +@Suppress("DSL_SCOPE_VIOLATION") // Remove when fixed https://youtrack.jetbrains.com/issue/KTIJ-19369 +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.kmmbridge) + `maven-publish` +} + +@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) +kotlin { + targetHierarchy.default() + + android { + publishAllLibraryVariants() + compilations.all { + kotlinOptions { + jvmTarget = "17" + } + } + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "noober-no-op" + } + } + + sourceSets { + val commonMain by getting { + dependencies { + //put your multiplatform dependencies here + } + } + + val androidMain by getting { + dependencies { + implementation (libs.okhttp) + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } +// val iosX64Main by getting +// val iosArm64Main by getting +// val iosSimulatorArm64Main by getting +// val iosMain by creating { +// dependsOn(commonMain) +// iosX64Main.dependsOn(this) +// iosArm64Main.dependsOn(this) +// iosSimulatorArm64Main.dependsOn(this) +// } + } +} + +android { + namespace = "com.abhi165.noober" + compileSdk = 33 + defaultConfig { + minSdk = 21 + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(11) + } +} +addGithubPackagesRepository() \ No newline at end of file diff --git a/noober-no-op/src/androidMain/kotlin/com/abhi165/noober/NoobInterceptor.kt b/noober-no-op/src/androidMain/kotlin/com/abhi165/noober/NoobInterceptor.kt new file mode 100644 index 0000000..2ab3e8a --- /dev/null +++ b/noober-no-op/src/androidMain/kotlin/com/abhi165/noober/NoobInterceptor.kt @@ -0,0 +1,10 @@ +package com.abhi165.noober + +import okhttp3.Interceptor +import okhttp3.Response + +class NoobInterceptor: Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + return chain.proceed(chain.request()) + } +} \ No newline at end of file diff --git a/noober-no-op/src/androidMain/kotlin/com/abhi165/noober/Noober.kt b/noober-no-op/src/androidMain/kotlin/com/abhi165/noober/Noober.kt new file mode 100644 index 0000000..3eca5e5 --- /dev/null +++ b/noober-no-op/src/androidMain/kotlin/com/abhi165/noober/Noober.kt @@ -0,0 +1,7 @@ +package com.abhi165.noober + +import android.content.Context + +actual object Noober: NooberCommon { + fun start(context: Context) {} +} \ No newline at end of file diff --git a/noober-no-op/src/commonMain/kotlin/com/abhi165/noober/Noober.kt b/noober-no-op/src/commonMain/kotlin/com/abhi165/noober/Noober.kt new file mode 100644 index 0000000..d41935e --- /dev/null +++ b/noober-no-op/src/commonMain/kotlin/com/abhi165/noober/Noober.kt @@ -0,0 +1,18 @@ +package com.abhi165.noober + +import com.abhi165.noober.model.NoobUserProperties +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + +interface NooberCommon { + fun log(tag:String, value: Any, isError: Boolean = false) {} + + @OptIn(ExperimentalObjCName::class) + fun setUserProperties(@ObjCName(name = "_") prop: List) {} + + fun mock(url: String, json: Map) {} + fun intercept(url: String) {} +} + +expect object Noober: NooberCommon + diff --git a/noober-no-op/src/commonMain/kotlin/com/abhi165/noober/model/NoobUserProperties.kt b/noober-no-op/src/commonMain/kotlin/com/abhi165/noober/model/NoobUserProperties.kt new file mode 100644 index 0000000..340ebb1 --- /dev/null +++ b/noober-no-op/src/commonMain/kotlin/com/abhi165/noober/model/NoobUserProperties.kt @@ -0,0 +1,3 @@ +package com.abhi165.noober.model + +data class NoobUserProperties(val key: String = "") \ No newline at end of file diff --git a/noober-no-op/src/iosMain/kotlin/com/abhi165/noober/Noober.kt b/noober-no-op/src/iosMain/kotlin/com/abhi165/noober/Noober.kt new file mode 100644 index 0000000..bf79b0b --- /dev/null +++ b/noober-no-op/src/iosMain/kotlin/com/abhi165/noober/Noober.kt @@ -0,0 +1,3 @@ +package com.abhi165.noober + +actual object Noober: NooberCommon \ No newline at end of file diff --git a/noober/build.gradle.kts b/noober/build.gradle.kts new file mode 100644 index 0000000..6c55388 --- /dev/null +++ b/noober/build.gradle.kts @@ -0,0 +1,101 @@ +@Suppress("DSL_SCOPE_VIOLATION") // Remove when fixed https://youtrack.jetbrains.com/issue/KTIJ-19369 +plugins { + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kmmbridge) + `maven-publish` +} + +@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) +kotlin { + //targetHierarchy.default() + + android { + publishAllLibraryVariants() + compilations.all { + kotlinOptions { + jvmTarget = "17" + } + } + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework("Noober") { + isStatic = true + } + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.material) + implementation(compose.animation) + @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) + implementation(compose.components.resources) + api(libs.precompose) + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + + val androidMain by getting { + dependencies { + api(libs.activity.compose) + api(libs.appcompat) + api(libs.core.ktx) + implementation (libs.okhttp) + implementation (libs.okio) + implementation (libs.security.crypto) + } + } + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + } +} + +android { + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + sourceSets["main"].res.srcDirs("src/androidMain/res") + sourceSets["main"].resources.srcDirs("src/commonMain/resources") + + namespace = "com.abhi165.noober" + compileSdk = 34 + defaultConfig { + minSdk = 21 + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(11) + } +} + +addGithubPackagesRepository() + +kmmbridge { + mavenPublishArtifacts() + githubReleaseVersions() + spm() +} \ No newline at end of file diff --git a/noober/src/androidMain/AndroidManifest.xml b/noober/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..57c0186 --- /dev/null +++ b/noober/src/androidMain/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/noober/src/androidMain/kotlin/com/abhi165/noober/AccountManagerImpl.kt b/noober/src/androidMain/kotlin/com/abhi165/noober/AccountManagerImpl.kt new file mode 100644 index 0000000..34fa624 --- /dev/null +++ b/noober/src/androidMain/kotlin/com/abhi165/noober/AccountManagerImpl.kt @@ -0,0 +1,21 @@ +package com.abhi165.noober + +internal object AccountManagerImpl: AccountManager { + override suspend fun generateDeepLink(): String { + val prop = NoobRepository.userProperties + val deepLinkPrefValueMap = mutableMapOf() + val prefValues = NoobHelper.prefManager.getAllValuesWithPrefName() + + prefValues.forEach {prefModel -> + for ((key, value) in prefModel.data) { + if(prop.any { it.key == key }) + deepLinkPrefValueMap[key] = value.toString() + } + } + return DeepLinkHandler.generateDeepLink(deepLinkPrefValueMap) + } + + override fun restoreAccount() { + NoobHelper.prefManager.restoreValues() + } +} \ No newline at end of file diff --git a/noober/src/androidMain/kotlin/com/abhi165/noober/DeepLinkHandler.kt b/noober/src/androidMain/kotlin/com/abhi165/noober/DeepLinkHandler.kt new file mode 100644 index 0000000..1b9e2c2 --- /dev/null +++ b/noober/src/androidMain/kotlin/com/abhi165/noober/DeepLinkHandler.kt @@ -0,0 +1,38 @@ +package com.abhi165.noober + +import android.net.Uri +import com.abhi165.noober.util.Constants + +internal object DeepLinkHandler { + + fun generateDeepLink(queries: Map): String { + val uriBuilder = Uri.Builder() + .scheme(Constants.NOOB_SCHEME) + .authority(Constants.NOOB_HOST) + + + for ((key, value) in queries) { + uriBuilder.appendQueryParameter(key, value) + } + uriBuilder.appendQueryParameter(Constants.IS_FROM_ANDROID, "1") + return uriBuilder.build().toString() + } + + fun handleDeepLink(uri: Uri?): Boolean { + uri?.let {path -> + val newPrefValues = mutableMapOf() + val queries = path.queryParameterNames.toList() + + queries.forEach { key -> + val value = path.getQueryParameter(key) + value?.let { + newPrefValues[key] = it + } + } + NoobHelper.prefManager.importAccount(newPrefValues) + + return true + } + return false + } +} \ No newline at end of file diff --git a/noober/src/androidMain/kotlin/com/abhi165/noober/Extension.kt b/noober/src/androidMain/kotlin/com/abhi165/noober/Extension.kt new file mode 100644 index 0000000..193d482 --- /dev/null +++ b/noober/src/androidMain/kotlin/com/abhi165/noober/Extension.kt @@ -0,0 +1,49 @@ +package com.abhi165.noober + +import com.abhi165.noober.model.RequestModel +import com.abhi165.noober.model.ResponseModel +import com.abhi165.noober.util.DateUtil +import com.abhi165.noober.util.generateCurlCommand +import com.abhi165.noober.util.prettyPrint +import okhttp3.Request +import okhttp3.Response +import okio.Buffer +import java.util.concurrent.TimeUnit + +internal fun Request.toModel(timeout: Long): RequestModel { + var requestBody = "" + body?.let {body-> + val buffer = Buffer().also { + body.writeTo(it) + } + requestBody = buffer.readUtf8() + } + val header = headers.associate { it.first to it.second } + + return RequestModel( + baseURL = url.toString(), + curl = generateCurlCommand( + url = url.toString(), + method = method, + headers = header, + body = requestBody), + header = header, + body = prettyPrint(requestBody), + date = DateUtil.now(), + method = method, + timeout = TimeUnit.MILLISECONDS.toSeconds(timeout).toFloat(), + cachePolicy = cacheControl.toString() + ) +} +internal fun Response.toModel(requestDate: String): ResponseModel { + val responseBodyString = peekBody(Long.MAX_VALUE).string() + + return ResponseModel( + header = headers.associate { it.first to it.second }, + body = prettyPrint(responseBodyString), + date = DateUtil.now(), + statusCode = this.code, + timeInterval = DateUtil.calculateDiff(DateUtil.now(), requestDate) + ) +} + diff --git a/noober/src/androidMain/kotlin/com/abhi165/noober/NoobHelper.kt b/noober/src/androidMain/kotlin/com/abhi165/noober/NoobHelper.kt new file mode 100644 index 0000000..64dcf17 --- /dev/null +++ b/noober/src/androidMain/kotlin/com/abhi165/noober/NoobHelper.kt @@ -0,0 +1,54 @@ +package com.abhi165.noober + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import com.abhi165.noober.ui.components.NavigationRoute + +@SuppressLint("StaticFieldLeak") +internal object NoobHelper { + private var hasNooberStartedAlready = false + var isNooberVisible = false + + + lateinit var prefManager: NoobPrefManager + private set + + private lateinit var context: Context + + lateinit var notificationManager: NotificationManager + private set + + fun init(context: Context) { + if(hasNooberStartedAlready) return + + this.context = context.applicationContext + prefManager = NoobPrefManager(context) + notificationManager = NotificationManager(context) + hasNooberStartedAlready = true + + Thread.setDefaultUncaughtExceptionHandler { _, throwable -> + NoobRepository.log("Crash Detected :(", throwable.stackTraceToString(), true) + startNoobActivity(route = NavigationRoute.BottomNavItem.Logs.route) + } + } + + private fun startNoobActivity(route: String? = null) { + context.startActivity(Intent(context, NoobActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + route?.let { + putExtra("route", it) + } + }) + } + + fun share(data: String) { + val sendIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, data) + } + val shareIntent = Intent.createChooser(sendIntent, "Share to..") + shareIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(shareIntent) + } +} \ No newline at end of file diff --git a/noober/src/androidMain/kotlin/com/abhi165/noober/NoobInterceptor.kt b/noober/src/androidMain/kotlin/com/abhi165/noober/NoobInterceptor.kt new file mode 100644 index 0000000..4d9fa2c --- /dev/null +++ b/noober/src/androidMain/kotlin/com/abhi165/noober/NoobInterceptor.kt @@ -0,0 +1,27 @@ +package com.abhi165.noober + +import com.abhi165.noober.model.APIInfoModel +import okhttp3.Interceptor +import okhttp3.Response + +class NoobInterceptor: Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + if(NoobRepository.OLD_URL.isNotBlank() && NoobRepository.NEW_URL.isNotBlank()) { + request = request + .newBuilder() + .url(request.url.toString() + .replace(NoobRepository.OLD_URL, NoobRepository.NEW_URL)) + .build() + } + val requestModel = request.toModel(chain.readTimeoutMillis().toLong()) + + val response = chain.proceed(request) + val responseModel = response.toModel(requestModel.date) + val apiInfoModel = APIInfoModel(request = requestModel, response = responseModel, error = "") + NoobRepository.recordAPI(apiInfoModel) + NoobHelper.notificationManager.showNotification(requestModel.baseURL) + return response + } +} \ No newline at end of file diff --git a/noober/src/androidMain/kotlin/com/abhi165/noober/NoobPrefManager.kt b/noober/src/androidMain/kotlin/com/abhi165/noober/NoobPrefManager.kt new file mode 100644 index 0000000..9e539ab --- /dev/null +++ b/noober/src/androidMain/kotlin/com/abhi165/noober/NoobPrefManager.kt @@ -0,0 +1,234 @@ +package com.abhi165.noober + +import android.content.Context +import android.content.SharedPreferences +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import androidx.core.content.edit +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.abhi165.noober.model.AvailablePrefModel +import com.abhi165.noober.model.SharedPrefModel +import com.abhi165.noober.util.Constants +import com.abhi165.noober.util.Constants.NOOB_PLACEHOLDER +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File + +internal class NoobPrefManager(private val context: Context) : SharedPrefManager { + private val NOOB_PREF = "noob_pref" + private val availableSharedPref = mutableListOf() + private var activePref: SharedPreferences? = null + private var activePrefIdentifier = "" + private val _prefFlow = MutableStateFlow(SharedPrefModel()) + + private val listener = OnSharedPreferenceChangeListener { sharedPreferences, _ -> + val updatedValues = sharedPreferences.all.filterValues { it != null } as Map + _prefFlow.tryEmit( + SharedPrefModel( + prefName = activePrefIdentifier, + data = updatedValues + ) + ) + } + + init { + getAllSharedPref() + } + + private val noobSharedPref by lazy { + context.getSharedPreferences(NOOB_PREF, Context.MODE_PRIVATE) + } + + override fun hasPrefEncryptionData(): Boolean { + val prefData = noobSharedPref.all + availableSharedPref.forEach { identifier -> + if (prefData[identifier] !is Boolean) return false + } + return true + } + + override fun getPrefValues(prefName: String): Flow { + activePrefIdentifier = prefName + val sharedPreference = (if (noobSharedPref.getBoolean( + prefName, + false + ) + ) getEncryptedSharedPref(prefName) else getSharedPref(prefName)).also { + it.registerOnSharedPreferenceChangeListener(listener) + activePref = it + } + val prefData = sharedPreference.all.filterValues { it != null } as Map + _prefFlow.value = SharedPrefModel( + prefName = prefName, + data = prefData + ) + return _prefFlow + } + + override fun cleanResourcesIfNeeded() { + activePref?.unregisterOnSharedPreferenceChangeListener(listener) + activePref = null + activePrefIdentifier = "" + } + + override suspend fun getAllValuesWithPrefName(): List { + val sharedPrefList = mutableListOf() + availableSharedPref.forEach { prefName -> + val isEncrypted = noobSharedPref.getBoolean(prefName, false) + val sharedPreference = + if (isEncrypted) getEncryptedSharedPref(prefName) else getSharedPref(prefName) + val prefData = sharedPreference.all.filterValues { it != null } as Map + sharedPrefList.add( + SharedPrefModel( + prefName = prefName, + data = prefData + ) + ) + } + return sharedPrefList + } + + + override fun changeValueOf(key: String, newValue: String, prefName: String, oldValue: Any) { + val sharedPreference = if (noobSharedPref.getBoolean( + prefName, + false + ) + ) getEncryptedSharedPref(prefName) else getSharedPref(prefName) + sharedPreference.edit().apply { + when (oldValue) { + is String -> putString(key, newValue) + is Boolean -> putBoolean(key, newValue.toBoolean()) + is Int -> putInt(key, newValue.toInt()) + is Float -> putFloat(key, newValue.toFloat()) + is Long -> putLong(key, newValue.toLong()) + } + }.apply() + } + + private fun changeAndBackupValueOf( + key: String, + newValue: String, + prefName: String, + oldValue: Any + ) { + noobSharedPref.edit().apply { + val noobKey = key + NOOB_PLACEHOLDER + prefName + putString(noobKey, oldValue.toString()) + }.apply() + changeValueOf(key, newValue, prefName, oldValue) + } + + fun importAccount(queryParameter: Map) { + if (!hasPrefEncryptionData()) return + val oldValues = noobSharedPref.all.filterKeys { it.contains(NOOB_PLACEHOLDER) } + noobSharedPref.edit(true) { + oldValues.forEach { (key, _) -> + remove(key) + } + } + + GlobalScope.launch { + val userProperties = + NoobRepository.userProperties.associate { it.alternateKeyForCrossPlatform to it.key } + val oldPrefValues = getAllValuesWithPrefName() + val isFromAndroid = queryParameter[Constants.IS_FROM_ANDROID] == "1" + val deppLinkParameters = queryParameter.toMutableMap().also { + it.remove(Constants.IS_FROM_ANDROID) + } + + for ((key, value ) in deppLinkParameters) { + val mappedKey = if(isFromAndroid) key else userProperties[key] + oldPrefValues.forEach {prefModel -> + for ((oldKey, oldValue) in prefModel.data) { + if (oldKey == mappedKey) { + withContext(Dispatchers.Main) { + changeAndBackupValueOf( + key = oldKey, + newValue = value, + prefName = prefModel.prefName, + oldValue = oldValue + ) + } + } + } + } + } + } + } + + fun restoreValues() { + val restorationValues = noobSharedPref.all.filterKeys { it.contains(NOOB_PLACEHOLDER) } + val noobEdit = noobSharedPref.edit() + GlobalScope.launch { + val oldPrefs = getAllValuesWithPrefName() + for ((key, value) in restorationValues) { + val noobKey = key.split(NOOB_PLACEHOLDER) + val prefKey = noobKey[0] + val prefIdentifier = noobKey[1] + val oldPrefValue = oldPrefs.first { it.prefName == prefIdentifier }.data[prefKey] + withContext(Dispatchers.Main) { + changeValueOf( + key = prefKey, + newValue = value.toString(), + prefName = prefIdentifier, + oldValue = oldPrefValue ?: "" + ) + noobEdit.remove(key) + } + } + noobEdit.apply() + } + } + + override fun updateNoobPrefValue(data: List) { + noobSharedPref.edit().apply { + data.forEach { + putBoolean(it.prefIdentifier, it.isEncrypted) + } + + }.apply() + } + + private fun getSharedPref(identifier: String): SharedPreferences { + return context.getSharedPreferences(identifier, Context.MODE_PRIVATE) + } + + private fun getEncryptedSharedPref(identifier: String): SharedPreferences { + val key = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() + return EncryptedSharedPreferences.create( + context, + identifier, + key, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + + private fun getAllSharedPref() { + val dir = File(context.applicationInfo.dataDir, "shared_prefs") + + val prefFiles = dir.listFiles { _, name -> + (name.endsWith(".xml") && !name.contains(NOOB_PREF)) + }?.map { it.nameWithoutExtension } + + prefFiles?.let { + availableSharedPref += it + } + } + + override fun getAvailablePreferences(): List { + val noobPref = noobSharedPref.all + return availableSharedPref.map { + AvailablePrefModel( + prefIdentifier = it, + isEncrypted = (noobPref[it] as? Boolean) ?: false + ) + } + } + +} \ No newline at end of file diff --git a/noober/src/androidMain/kotlin/com/abhi165/noober/Noober.kt b/noober/src/androidMain/kotlin/com/abhi165/noober/Noober.kt new file mode 100644 index 0000000..1ad7016 --- /dev/null +++ b/noober/src/androidMain/kotlin/com/abhi165/noober/Noober.kt @@ -0,0 +1,17 @@ +package com.abhi165.noober + +import android.content.Context + +actual object Noober: NooberCommon { + fun start(context: Context) { + NoobHelper.init(context) + } +} + +internal actual fun getSharedPrefManager(): SharedPrefManager = NoobHelper.prefManager +internal actual fun isAndroid(): Boolean = true +internal actual fun share(data: String) { + NoobHelper.share(data) +} + +internal actual fun getAccountManager(): AccountManager = AccountManagerImpl diff --git a/noober/src/androidMain/kotlin/com/abhi165/noober/NotificationManager.kt b/noober/src/androidMain/kotlin/com/abhi165/noober/NotificationManager.kt new file mode 100644 index 0000000..5dedfa5 --- /dev/null +++ b/noober/src/androidMain/kotlin/com/abhi165/noober/NotificationManager.kt @@ -0,0 +1,64 @@ +package com.abhi165.noober + +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat + +internal class NotificationManager(private val context: Context) { + + companion object { + private const val CHANNEL_ID = "noob_notification" + private const val NOTIFICATION_ID = 165 + } + private val notificationManager = NotificationManagerCompat.from(context) + + init { + createNotificationChannel() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "Noob Notification" + val descriptionText = "Used for api calls" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + description = descriptionText + } + val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } + + @SuppressLint("MissingPermission") + fun showNotification(contentText: String) { + if(NoobHelper.isNooberVisible) + return + val pendingIntent = PendingIntent.getActivity( + context, + 0, + Intent(context, NoobActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(androidx.core.R.drawable.notification_bg) + .setContentTitle("Noob Notificaiton") + .setContentText(contentText) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setOngoing(true) + .setContentIntent(pendingIntent) + + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) + } + + fun cancelNotification() { + notificationManager.cancel(NOTIFICATION_ID) + } +} diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/AccountManager.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/AccountManager.kt new file mode 100644 index 0000000..44e1931 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/AccountManager.kt @@ -0,0 +1,6 @@ +package com.abhi165.noober + +internal interface AccountManager { + suspend fun generateDeepLink(): String + fun restoreAccount() +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/NoobRepository.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/NoobRepository.kt new file mode 100644 index 0000000..aa47add --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/NoobRepository.kt @@ -0,0 +1,125 @@ +package com.abhi165.noober + +import com.abhi165.noober.model.APIInfoModel +import com.abhi165.noober.model.AvailablePrefModel +import com.abhi165.noober.model.LogModel +import com.abhi165.noober.model.NoobUserProperties +import com.abhi165.noober.model.SharedPrefModel +import com.abhi165.noober.model.convertToState +import com.abhi165.noober.model.state.APIInfoState +import com.abhi165.noober.model.state.SearchState +import com.abhi165.noober.ui.components.SearchWidgetState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.IO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +internal object NoobRepository { + private val _requestList = MutableStateFlow>(listOf()) + val requestList = _requestList.asStateFlow() + + private val _logsList = MutableStateFlow>(listOf()) + val logsList = _logsList.asStateFlow() + + private val _searchState = MutableStateFlow(SearchWidgetState.CLOSED) + val searchState = _searchState.asStateFlow() + + private val _searchQuery = MutableStateFlow("") + val searchQuery = _searchQuery.asStateFlow() + + private val prefManager = getSharedPrefManager() + + private val _userProperties = mutableListOf() + val userProperties: List = _userProperties + + var OLD_URL = "" + private set + + var NEW_URL = "" + private set + + private val cachedPrefData = mutableListOf() + + val searchedData = searchQuery.combine(requestList) { query, apiCalls -> + val filteredCalls = apiCalls.filter { it.matchesQuery(query) } + val filteredPrefData = cachedPrefData.filter { it.matchesQuery(query) }.map { + val filteredData = it.data.filter { (key, value) -> + key.contains(query, ignoreCase = true) || value.toString() + .contains(query, ignoreCase = true) + } + SharedPrefModel(prefName = it.prefName, data = filteredData) + } + return@combine SearchState(prefData = filteredPrefData, apiCalls = filteredCalls) + }.combine(logsList) { searchState, logs -> + val query = searchState.queryString + val filteredLogs = logs.filter { it.matchesQuery(query) } + return@combine searchState.copy(logs = filteredLogs) + }.flowOn(Dispatchers.IO) + + fun recordAPI(apiCall: APIInfoModel) { + _requestList.update { apiList -> + apiList.toMutableList().apply { + add(0, apiCall.convertToState()) + } + } + } + + fun hasPrefEncryptionData() = prefManager.hasPrefEncryptionData() + fun getAvailablePreferences() = prefManager.getAvailablePreferences() + + fun getPrefData(prefIdentifier: String): Flow = + prefManager.getPrefValues(prefIdentifier).flowOn(Dispatchers.IO) + + fun addPrefData(key: String, newValue: String, prefIdentifier: String, oldValue: Any) { + prefManager.changeValueOf(key, newValue, prefIdentifier, oldValue) + } + + fun updateNoobPrefValue(data: List) { + prefManager.updateNoobPrefValue(data) + } + + fun log(key: String, value: Any, isError: Boolean) { + _logsList.update { logList -> + logList.toMutableList().apply { + add(0, LogModel(tag = key, value = value.toString(), isError = isError)) + } + } + } + + fun onSearchQueryCHanged(query: String) { + _searchQuery.value = query + } + + private fun updateCachedPrefData() { + cachedPrefData.clear() + GlobalScope.launch { + val data = prefManager.getAllValuesWithPrefName() + withContext(Dispatchers.Main) { + cachedPrefData.addAll(0, data) + } + } + } + + fun changeSearchWidgetState(state: SearchWidgetState) { + updateCachedPrefData() + _searchState.value = state + } + + fun changeURL(newURL: String, oldURL: String) { + OLD_URL = oldURL + NEW_URL = newURL + } + + fun addUserProperties(prop: List) { + _userProperties.clear() + _userProperties += prop + } + fun cleanResourcesIfNeeded() = prefManager.cleanResourcesIfNeeded() +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/Noober.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/Noober.kt new file mode 100644 index 0000000..d753d3d --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/Noober.kt @@ -0,0 +1,26 @@ +package com.abhi165.noober + +import com.abhi165.noober.model.NoobUserProperties +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + + +interface NooberCommon { + fun log(tag:String, value: Any, isError: Boolean = false) { + NoobRepository.log(tag, value, isError) + } + + @OptIn(ExperimentalObjCName::class) + fun setUserProperties(@ObjCName(name = "_") prop: List) { + NoobRepository.addUserProperties(prop) + } + + fun mock(url: String, json: Map) {} + fun intercept(url: String) {} +} + +expect object Noober: NooberCommon +internal expect fun getSharedPrefManager(): SharedPrefManager +internal expect fun isAndroid(): Boolean +internal expect fun share(data: String) +internal expect fun getAccountManager(): AccountManager diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/SharedPrefManager.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/SharedPrefManager.kt new file mode 100644 index 0000000..e3f58af --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/SharedPrefManager.kt @@ -0,0 +1,15 @@ +package com.abhi165.noober + +import com.abhi165.noober.model.AvailablePrefModel +import com.abhi165.noober.model.SharedPrefModel +import kotlinx.coroutines.flow.Flow + +internal interface SharedPrefManager { + fun hasPrefEncryptionData(): Boolean{return true} + fun getAvailablePreferences(): List = listOf() + fun getPrefValues(prefName: String): Flow + fun cleanResourcesIfNeeded(){} + fun changeValueOf(key: String, newValue: String, prefName: String = "", oldValue: Any) + suspend fun getAllValuesWithPrefName(): List + fun updateNoobPrefValue(data: List) {} +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/model/APIInfoModel.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/model/APIInfoModel.kt new file mode 100644 index 0000000..89d1ce2 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/model/APIInfoModel.kt @@ -0,0 +1,49 @@ +package com.abhi165.noober.model + +import androidx.compose.ui.graphics.Color +import com.abhi165.noober.model.state.APIInfoState +import com.abhi165.noober.model.state.APISummaryState +import com.abhi165.noober.model.state.HeaderBodyState +import com.abhi165.noober.util.DateUtil + +internal data class APIInfoModel( + val request: RequestModel, + val response: ResponseModel, + val error: String? +) { + + + val statusCodeColor: Color + get() = if (response.statusCode in 200..299) Color(0,100,0) else Color(139,0,0) +} + +internal fun APIInfoModel.convertToState(): APIInfoState { + val summary = APISummaryState( + cURL = request.curl, + statusCode = response.statusCode.toString(), + statusCodeColor = if (response.statusCode in 200..299) Color(0,100,0) else Color(139,0,0), + timeout = request.timeout.toString(), + timeInterval = response.timeInterval, + responseTime = DateUtil.formatTime(response.date), + requestTime = DateUtil.formatTime(request.date), + url = request.baseURL, + method = request.method, + error = error, + cache = request.cachePolicy + ) + + val responseState = HeaderBodyState( + headers = response.header, + body = response.body + ) + + val requestState = HeaderBodyState( + headers = request.header, + body = request.body + ) + return APIInfoState( + summary = summary, + requestState = requestState, + responseState = responseState + ) +} diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/model/AvailablePrefModel.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/model/AvailablePrefModel.kt new file mode 100644 index 0000000..98e2d5b --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/model/AvailablePrefModel.kt @@ -0,0 +1,6 @@ +package com.abhi165.noober.model + +internal data class AvailablePrefModel( + val prefIdentifier: String, + val isEncrypted: Boolean +) diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/model/LogModel.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/model/LogModel.kt new file mode 100644 index 0000000..c5e4960 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/model/LogModel.kt @@ -0,0 +1,18 @@ +package com.abhi165.noober.model + +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +internal data class LogModel( + val tag:String, + val value: String, + val isError: Boolean, + val date: String = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).toString() +) { + fun matchesQuery(query: String): Boolean { + return tag.contains(query, ignoreCase = true) || + value.contains(query, ignoreCase = true) || + date.contains(query, ignoreCase = true) + } +} diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/model/NoobUserProperties.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/model/NoobUserProperties.kt new file mode 100644 index 0000000..74ecdb3 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/model/NoobUserProperties.kt @@ -0,0 +1,6 @@ +package com.abhi165.noober.model + +data class NoobUserProperties( + val key: String, + val alternateKeyForCrossPlatform: String = key +) diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/model/RequestModel.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/model/RequestModel.kt new file mode 100644 index 0000000..7a5110f --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/model/RequestModel.kt @@ -0,0 +1,12 @@ +package com.abhi165.noober.model + +internal data class RequestModel( + val baseURL: String = "_", + val curl: String = "_", + val header: Map = mapOf(), + val body: String = "_", + val date: String = "_", + val method: String = "_", + val timeout: Float = 0f, + val cachePolicy: String = "_" +) diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/model/ResponseModel.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/model/ResponseModel.kt new file mode 100644 index 0000000..7815526 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/model/ResponseModel.kt @@ -0,0 +1,9 @@ +package com.abhi165.noober.model + +internal data class ResponseModel( + val header: Map = mapOf(), + val body: String = "_", + val date: String = "_", + val statusCode: Int = 0, + val timeInterval: String = "_", +) diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/model/SharedPrefModel.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/model/SharedPrefModel.kt new file mode 100644 index 0000000..f33eb82 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/model/SharedPrefModel.kt @@ -0,0 +1,12 @@ +package com.abhi165.noober.model + +internal data class SharedPrefModel( + val prefName: String = "", + val data: Map = mapOf(), +) { + fun matchesQuery(query: String): Boolean { + return (data.any {prefData -> + prefData.key.contains(query, ignoreCase = true) || (prefData.value.toString().contains(query, ignoreCase = true)) + }) + } +} diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/APIInfoState.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/APIInfoState.kt new file mode 100644 index 0000000..250117c --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/APIInfoState.kt @@ -0,0 +1,15 @@ +package com.abhi165.noober.model.state + + + +internal data class APIInfoState( + val summary: APISummaryState, + val requestState: HeaderBodyState, + val responseState: HeaderBodyState +) { + fun matchesQuery(query: String): Boolean { + return summary.matchesQuery(query) || + requestState.matchesQuery(query) || + responseState.matchesQuery(query) + } +} diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/APISummaryState.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/APISummaryState.kt new file mode 100644 index 0000000..caaad0d --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/APISummaryState.kt @@ -0,0 +1,22 @@ +package com.abhi165.noober.model.state + +import androidx.compose.ui.graphics.Color + +internal data class APISummaryState( + val cURL: String, + val statusCode: String, + val statusCodeColor: Color, + val timeout: String, + val timeInterval: String, + val responseTime: String, + val requestTime: String, + val url: String, + val method: String, + val cache: String, + val error:String? +) { + fun matchesQuery(query: String): Boolean { + return method.contains(query, ignoreCase = true) || + url.contains(query, ignoreCase = true) + } +} diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/HeaderBodyState.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/HeaderBodyState.kt new file mode 100644 index 0000000..c6df781 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/HeaderBodyState.kt @@ -0,0 +1,14 @@ +package com.abhi165.noober.model.state + + +internal data class HeaderBodyState( + val headers: Map, + val body: String +) { + fun matchesQuery(query: String): Boolean { + return body.contains(query, ignoreCase = true) || + headers.any { + it.key.contains(query, ignoreCase = true) || it.value.contains(query, ignoreCase = true) + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/SearchState.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/SearchState.kt new file mode 100644 index 0000000..5d2b85e --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/model/state/SearchState.kt @@ -0,0 +1,12 @@ +package com.abhi165.noober.model.state + +import com.abhi165.noober.model.LogModel +import com.abhi165.noober.model.SharedPrefModel + +internal data class SearchState( + val queryString: String = "", + val apiCalls: List = listOf(), + val prefData: List = listOf(), + val logs: List = listOf() + +) diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/APICallScreen.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/APICallScreen.kt new file mode 100644 index 0000000..2e2734c --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/APICallScreen.kt @@ -0,0 +1,34 @@ +package com.abhi165.noober.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.abhi165.noober.NoobRepository +import com.abhi165.noober.ui.components.APIInfoRow +import com.abhi165.noober.util.collectAsStateWithLifecycleOrCollectAsState + +@Composable +internal fun APICallScreen( + onClick: (Int) -> Unit +) { + val apiCallList by NoobRepository.requestList.collectAsStateWithLifecycleOrCollectAsState() + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 16.dp, horizontal = 4.dp) + ) { + itemsIndexed(apiCallList){index, state -> + APIInfoRow( + state = state.summary, + onClick = { + onClick(index) + } + ) + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/APISummeryScreen.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/APISummeryScreen.kt new file mode 100644 index 0000000..4ad80b8 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/APISummeryScreen.kt @@ -0,0 +1,113 @@ +package com.abhi165.noober.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.abhi165.noober.model.state.APISummaryState + + +@Composable +internal fun APISummeryScreen( + state: APISummaryState +) { + LazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(16.dp) + ) { + item { + + if(state.error?.isNotEmpty() == true) { + InfoCell( + title = "Error", + subtitle = state.error, + subtitleColor = Color.Red + ) + } + + InfoCell( + title = "Status Code", + subtitle = state.statusCode, + subtitleColor = state.statusCodeColor + ) + InfoCell( + title = "Method", + subtitle = state.method + ) + InfoCell( + title = "URL", + subtitle = state.url + ) + InfoCell( + title = "Request Time", + subtitle = state.requestTime + ) + InfoCell( + title = "Response Time", + subtitle = state.responseTime + ) + InfoCell( + title = "Time Taken", + subtitle = state.timeInterval + ) + InfoCell( + title = "Timeout", + subtitle = state.timeout + ) + InfoCell( + title = "cURL", + subtitle = state.cURL + ) + + InfoCell( + title = "Cache Policy", + subtitle = state.cache + ) + } + } +} + +@Composable +fun InfoCell( + modifier: Modifier = Modifier, + title: String, + subtitle: String, + subtitleColor: Color = Color.Black, + showDivider: Boolean = true +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + + Text( + text = subtitle, + color = subtitleColor, + style = MaterialTheme.typography.bodyLarge + ) + + if (showDivider) { + Divider( + modifier = modifier + .padding(top = 2.dp) + ) + } + } + +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/APITabScreen.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/APITabScreen.kt new file mode 100644 index 0000000..a20399c --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/APITabScreen.kt @@ -0,0 +1,39 @@ +package com.abhi165.noober.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.abhi165.noober.model.state.APIInfoState + +@Composable +internal fun APITabScreen( + state: APIInfoState +) { + var tabIndex by remember { mutableStateOf(0) } + + val tabs = listOf("Summary", "Request", "Response") + + Column(modifier = Modifier.fillMaxSize()) { + TabRow(selectedTabIndex = tabIndex) { + tabs.forEachIndexed { index, title -> + Tab(text = { Text(title) }, + selected = tabIndex == index, + onClick = { tabIndex = index } + ) + } + } + when (tabIndex) { + 0 -> APISummeryScreen(state = state.summary) + 1 -> RequestResponseInfoScreen(state = state.requestState) + 2 -> RequestResponseInfoScreen(state = state.responseState) + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/AvailableSharedPreferencesScreen.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/AvailableSharedPreferencesScreen.kt new file mode 100644 index 0000000..d8c0fda --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/AvailableSharedPreferencesScreen.kt @@ -0,0 +1,49 @@ +package com.abhi165.noober.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.abhi165.noober.NoobRepository +import com.abhi165.noober.isAndroid +import com.abhi165.noober.ui.components.AvailablePrefRow + +@Composable +internal fun AvailableSharedPreferencesScreen( + modifier: Modifier = Modifier, + onPrefClicked: (String) -> Unit +) { + + if (isAndroid()) { + var showPrefSetting by remember { mutableStateOf(!NoobRepository.hasPrefEncryptionData()) } + + if (showPrefSetting) { + SharedPrefSettingView { + showPrefSetting = !NoobRepository.hasPrefEncryptionData() + } + } else { + val availablePrefList by remember { mutableStateOf(NoobRepository.getAvailablePreferences()) } + + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 16.dp, horizontal = 4.dp) + ) { + items(availablePrefList) { + AvailablePrefRow(identifier = it.prefIdentifier) { + onPrefClicked(it.prefIdentifier) + } + } + } + + } + } else { + SharedPreferenceDataScreen() + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/LogsScreen.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/LogsScreen.kt new file mode 100644 index 0000000..98e7294 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/LogsScreen.kt @@ -0,0 +1,60 @@ +package com.abhi165.noober.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.abhi165.noober.AccountManager +import com.abhi165.noober.NoobRepository +import com.abhi165.noober.getAccountManager +import com.abhi165.noober.share +import com.abhi165.noober.ui.components.LogRow +import com.abhi165.noober.util.collectAsStateWithLifecycleOrCollectAsState +import kotlinx.coroutines.launch + +@Composable +internal fun LogsScreen( + accountManager: AccountManager = getAccountManager() +) { + val logs by NoobRepository.logsList.collectAsStateWithLifecycleOrCollectAsState() + val coroutineScope = rememberCoroutineScope() + + LazyColumn( + modifier = Modifier + .background(MaterialTheme.colorScheme.surface) + .fillMaxSize(), + contentPadding = PaddingValues(vertical = 16.dp, horizontal = 8.dp) + ) { + item { + Text( + text = "Logs", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + + items(logs) {model -> + LogRow(model) {stackTrace -> + coroutineScope.launch { + val accountUsed = accountManager.generateDeepLink() + val message = "Account Used -> $accountUsed \n\n $stackTrace" + share(message) + } + } + } + } +} + diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/MoreScreen.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/MoreScreen.kt new file mode 100644 index 0000000..4bf2c7a --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/MoreScreen.kt @@ -0,0 +1,150 @@ +package com.abhi165.noober.ui + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.sharp.Refresh +import androidx.compose.material.icons.sharp.Share +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Button +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.abhi165.noober.AccountManager +import com.abhi165.noober.NoobRepository +import com.abhi165.noober.getAccountManager +import com.abhi165.noober.share +import kotlinx.coroutines.launch + +@Composable +internal fun MoreScreen( + modifier: Modifier = Modifier, + accountManager: AccountManager = getAccountManager() +) { + val focusManager = LocalFocusManager.current + + val coroutineScope = rememberCoroutineScope() + + var replaceableText by remember { + mutableStateOf(NoobRepository.OLD_URL) + } + + var replacedText by remember { + mutableStateOf(NoobRepository.NEW_URL) + } + + CompositionLocalProvider( + LocalIndication provides rememberRipple(color = Color.Transparent) + ) { + Column( + modifier = modifier + .clickable { + focusManager.clearFocus(true) + } + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp) + ) { + Text( + text = "Change URL", + style = MaterialTheme.typography.headlineMedium, + color = Color.Black, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace + ) + + OutlinedTextField( + value = replaceableText, + label = { Text(text = "Current URL") }, + onValueChange = { + replaceableText = it + }, + isError = replaceableText.isNotBlank() + ) + OutlinedTextField( + value = replacedText, + label = { Text(text = "Enter new URL") }, + onValueChange = { + replacedText = it + }, + isError = replacedText.isNotBlank() + ) + + Button(onClick = { + NoobRepository.changeURL(newURL = replacedText, oldURL = replaceableText) + focusManager.clearFocus(true) + }, + enabled = replacedText.isNotBlank() && replaceableText.isNotBlank() + ) { + Text(text = "Change") + } + Spacer(modifier = modifier.height(8.dp)) + Divider() + Spacer(modifier = modifier.height(16.dp)) + + Text( + text = "Account", + style = MaterialTheme.typography.headlineMedium, + color = Color.Black, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Button(onClick = { + coroutineScope.launch { + val link = accountManager.generateDeepLink() + share(link) + } }) { + Icon(imageVector = Icons.Sharp.Share, contentDescription = "") + Text(text = "Share") + } + + Button(onClick = { accountManager.restoreAccount()}) { + Icon(imageVector = Icons.Sharp.Refresh, contentDescription = "") + Text(text = "Restore") + } + } + Spacer(modifier = modifier.height(8.dp)) + Divider() + Spacer(modifier = modifier.height(16.dp)) + + Text( + text = "About", + style = MaterialTheme.typography.headlineMedium, + color = Color.Black, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace + ) + + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/NoobScreen.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/NoobScreen.kt new file mode 100644 index 0000000..54d1b1a --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/NoobScreen.kt @@ -0,0 +1,94 @@ +package com.abhi165.noober.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.abhi165.noober.NoobRepository +import com.abhi165.noober.isAndroid +import com.abhi165.noober.ui.components.BottomBar +import com.abhi165.noober.ui.components.NavigationRoute +import com.abhi165.noober.ui.components.NoobAppBar +import com.abhi165.noober.ui.components.RootNavGraph +import com.abhi165.noober.ui.components.SearchWidgetState +import com.abhi165.noober.util.collectAsStateWithLifecycleOrCollectAsState +import moe.tlaster.precompose.navigation.NavOptions +import moe.tlaster.precompose.navigation.PopUpTo +import moe.tlaster.precompose.navigation.rememberNavigator + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun NoobScreen( + route: String? = null, + topSafeArea: Float = 0f +) { + val items = listOf( + NavigationRoute.BottomNavItem.Home, + NavigationRoute.BottomNavItem.Properties, + NavigationRoute.BottomNavItem.Logs, + NavigationRoute.BottomNavItem.More + ) + val navigator = rememberNavigator() + val navBackStackEntry by navigator.currentEntry.collectAsState(null) + val currentEntry = navBackStackEntry?.route?.route + + val searchWidgetState by NoobRepository.searchState.collectAsStateWithLifecycleOrCollectAsState() + val searchState by NoobRepository.searchQuery.collectAsStateWithLifecycleOrCollectAsState() + + Scaffold( + modifier = Modifier + .fillMaxSize() + .padding(top = topSafeArea.dp), + topBar = { + NoobAppBar( + searchWidgetState = searchWidgetState, + canGoBack = !items.any { it.route == currentEntry }, + showPrefSetting = (currentEntry == NavigationRoute.BottomNavItem.Properties.route) && isAndroid() && NoobRepository.hasPrefEncryptionData(), + searchTextState =searchState, + onSettingClicked = { + navigator.navigate(NavigationRoute.SharedPrefSetting.route) + }, + onBackClicked = { + NoobRepository.changeSearchWidgetState(SearchWidgetState.CLOSED) + navigator.goBack() + }, + onTextChange = NoobRepository::onSearchQueryCHanged, + onSearchClicked = {}, + onSearchTriggered = { + NoobRepository.changeSearchWidgetState(SearchWidgetState.OPENED) + navigator.navigate(NavigationRoute.Search.route, options = NavOptions(launchSingleTop = true)) + } + ) + }, + bottomBar = { + if (items.any { it.route == currentEntry }) + BottomBar( + items, + selectedRoute = currentEntry ?: NavigationRoute.BottomNavItem.Home.route) {selectedRoute -> + navigator.navigate( + selectedRoute, + options = NavOptions( + launchSingleTop = true, + popUpTo = PopUpTo( + NavigationRoute.BottomNavItem.Home.route, + false + ) + ) + ) + } + } + ) { + RootNavGraph(navigator, modifier = Modifier.padding(it)) + } + LaunchedEffect(route) { + route?.let { + navigator.navigate(it, NavOptions(launchSingleTop = true)) + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/RequestResponseInfoScreen.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/RequestResponseInfoScreen.kt new file mode 100644 index 0000000..c7b0e9d --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/RequestResponseInfoScreen.kt @@ -0,0 +1,98 @@ +package com.abhi165.noober.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.sharp.Share +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.abhi165.noober.model.state.HeaderBodyState +import com.abhi165.noober.share + +@Composable +internal fun RequestResponseInfoScreen( + modifier: Modifier = Modifier, + state: HeaderBodyState +) { + + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + if(state.headers.isNotEmpty()) { + Text( + text = "Header", + style = MaterialTheme.typography.titleMedium, + color = Color.Black, + fontWeight = FontWeight.Bold, + modifier = modifier.padding(bottom = 16.dp) + ) + + for((key, value) in state.headers) { + InfoCell(title = key, subtitle = value) + } + } + + if(state.body.isNotEmpty()) { + Text( + text = "Body", + style = MaterialTheme.typography.titleMedium, + color = Color.Black, + fontWeight = FontWeight.Bold, + modifier = modifier.padding(bottom = 16.dp) + ) + + Button( + onClick = { share(state.body) }, + modifier = Modifier.align(Alignment.End) + ) { + Icon( + imageVector = Icons.Sharp.Share, + contentDescription = "Share JSON Data" + ) + + Text( + text = "Share", + fontWeight = FontWeight.SemiBold + ) + } + + SelectionContainer( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .border(1.dp, Color.Gray, MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.surface) + .padding(8.dp) + ) { + Text( + text = state.body, + style = MaterialTheme.typography.bodyLarge, + ) + + } + } + + if(state.headers.isEmpty() && state.body.isEmpty()) { + Text("Nothing to see here 😏") + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/SearchScreen.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/SearchScreen.kt new file mode 100644 index 0000000..24ee84a --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/SearchScreen.kt @@ -0,0 +1,79 @@ +package com.abhi165.noober.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.unit.dp +import com.abhi165.noober.AccountManager +import com.abhi165.noober.NoobRepository +import com.abhi165.noober.getAccountManager +import com.abhi165.noober.model.state.APIInfoState +import com.abhi165.noober.model.state.SearchState +import com.abhi165.noober.share +import com.abhi165.noober.ui.components.APIInfoRow +import com.abhi165.noober.ui.components.LogRow +import com.abhi165.noober.ui.components.SharedPrefRow +import com.abhi165.noober.util.collectAsStateWithLifecycleOrCollectAsState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +@Composable +internal fun SearchScreen( + accountManager: AccountManager = getAccountManager(), + searchState: Flow, + onAPICallTapped: (APIInfoState) -> Unit, + goBack: ()-> Unit +) { + val state by searchState.collectAsStateWithLifecycleOrCollectAsState(SearchState()) + val focusManager = LocalFocusManager.current + val coroutineScope = rememberCoroutineScope() + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .clickable { + focusManager.clearFocus(true) + } + , + contentPadding = PaddingValues(16.dp) + ) { + + items(state.apiCalls) { + APIInfoRow(state = it.summary) { + onAPICallTapped(it) + } + } + + state.prefData.forEach { + items(it.data.toList()) {pref -> + SharedPrefRow(key = pref.first, value = pref.second.toString()) {newValue -> + NoobRepository.addPrefData( + key = pref.first, + newValue = newValue, + prefIdentifier = it.prefName, + oldValue = pref.second + ) + goBack() + } + } + } + + items(state.logs) { + LogRow(it) {stackTrace -> + coroutineScope.launch { + val accountUsed = accountManager.generateDeepLink() + val message = "Account Used -> $accountUsed \n\n $stackTrace" + share(message) + } + } + } + + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/SharedPrefSettingsScreen.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/SharedPrefSettingsScreen.kt new file mode 100644 index 0000000..38a1e25 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/SharedPrefSettingsScreen.kt @@ -0,0 +1,94 @@ +package com.abhi165.noober.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.abhi165.noober.NoobRepository +import com.abhi165.noober.model.AvailablePrefModel + +@Composable +internal fun SharedPrefSettingView(onPrefSaved: ()-> Unit) { + val availablePref = NoobRepository.getAvailablePreferences().toMutableList() + + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.surface) + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { + + Text( + text = "Shared Preferences", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace + ) + + Divider(thickness = 0.5.dp) + + Text( + text = "Following shared preferences were found. Pleases check those which are stored using encrypted shared preference.", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .padding(vertical = 16.dp) + ) + PrefCheckMarkView(availablePref) { + NoobRepository.updateNoobPrefValue(availablePref) + onPrefSaved() + } + } + +} + +@Composable +private fun PrefCheckMarkView( + availablePref: MutableList, + onClick: () -> Unit +) { + Column(horizontalAlignment = Alignment.Start) { + availablePref.forEachIndexed { index, pref -> + var checked by remember { + mutableStateOf(pref.isEncrypted) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = checked, + onCheckedChange = { checked_ -> + checked = checked_ + availablePref[index] = pref.copy(isEncrypted = checked) + } + ) + + Text( + modifier = Modifier.padding(start = 2.dp), + text = pref.prefIdentifier + ) + } + } + + Button(onClick = onClick, modifier = Modifier.align(Alignment.End).padding(end = 16.dp)) { + Text("Save") + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/SharedPreferenceDataScreen.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/SharedPreferenceDataScreen.kt new file mode 100644 index 0000000..cc4431e --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/SharedPreferenceDataScreen.kt @@ -0,0 +1,62 @@ +package com.abhi165.noober.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.abhi165.noober.NoobRepository +import com.abhi165.noober.model.SharedPrefModel +import com.abhi165.noober.ui.components.SharedPrefRow +import com.abhi165.noober.util.collectAsStateWithLifecycleOrCollectAsState + + +@Composable +internal fun SharedPreferenceDataScreen( + prefIdentifier: String = "", + modifier: Modifier = Modifier +) { + val sharedPrefState by NoobRepository.getPrefData(prefIdentifier).collectAsStateWithLifecycleOrCollectAsState(SharedPrefModel()) + + DisposableEffect(Unit) { + onDispose { + NoobRepository.cleanResourcesIfNeeded() + } + } + + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 16.dp, horizontal = 4.dp) + ) { + item { + Text( + text = prefIdentifier, + style = MaterialTheme.typography.titleMedium, + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold + ) + } + + items(sharedPrefState.data.toList()) { + SharedPrefRow( + key = it.first, + value = it.second.toString() + ) { newValue -> + NoobRepository.addPrefData( + key = it.first, + newValue = newValue, + prefIdentifier = prefIdentifier, + oldValue = it.second.toString() + ) + } + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/APIInfoRow.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/APIInfoRow.kt new file mode 100644 index 0000000..e506860 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/APIInfoRow.kt @@ -0,0 +1,100 @@ +package com.abhi165.noober.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.abhi165.noober.model.state.APISummaryState + +@Composable +internal fun APIInfoRow( + modifier: Modifier = Modifier, + state: APISummaryState, + onClick: () -> Unit +) { + Card( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 8.dp, vertical = 8.dp) + .clip(RoundedCornerShape(16.dp)) + .clickable { onClick() }, + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = Color.LightGray.copy(0.2f)), + elevation = CardDefaults.cardElevation(), + border = BorderStroke(1.dp, state.statusCodeColor.copy(0.2f)) + ) { + Row( + modifier = modifier + .wrapContentHeight() + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + + Column(modifier = modifier + .padding(horizontal = 4.dp, vertical = 4.dp) + .background( + state.statusCodeColor.copy(0.1f), + RoundedCornerShape(16.dp) + ) + .padding(16.dp) + ) { + Text( + text = state.statusCode, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium, + color = state.statusCodeColor + ) + Text( + text = state.method, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleSmall, + color = state.statusCodeColor.copy(0.7f) + ) + } + Column { + Text( + text = state.url, + maxLines = 2, + style = MaterialTheme.typography.labelLarge + ) + + Divider( + thickness = 0.5.dp, + modifier = modifier.padding(end = 8.dp) + ) + + Text( + text = state.requestTime, + maxLines = 2, + color = Color.Black, + textAlign = TextAlign.End, + style = MaterialTheme.typography.bodyMedium, + modifier = modifier + .fillMaxWidth() + .padding(end = 8.dp, top = 12.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/AvailablePrefRow.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/AvailablePrefRow.kt new file mode 100644 index 0000000..b6a7bc6 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/AvailablePrefRow.kt @@ -0,0 +1,55 @@ +package com.abhi165.noober.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowForward +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +internal fun AvailablePrefRow(identifier: String, onClick: () -> Unit) { + Row( + modifier = Modifier + .padding(horizontal = 4.dp, vertical = 4.dp) + .clickable { + onClick() + } + .background( + Color.Blue.copy(0.01f), + RoundedCornerShape(8.dp) + ) + .border(width = 1.dp, shape = RoundedCornerShape(8.dp), color = Color.DarkGray) + .fillMaxWidth() + .wrapContentHeight() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + + Text( + text = identifier, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + + IconButton(onClick = onClick) { + Icon(Icons.Rounded.ArrowForward, contentDescription = "") + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/BottomBar.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/BottomBar.kt new file mode 100644 index 0000000..8be6b79 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/BottomBar.kt @@ -0,0 +1,68 @@ +package com.abhi165.noober.ui.components + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ExitToApp +import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.List +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector + + +internal sealed class NavigationRoute(val route: String) { + sealed class BottomNavItem(route: String, val icon: ImageVector, val title: String) : + NavigationRoute(route) { + object Home: BottomNavItem("api", Icons.Rounded.Home, "API") + object Properties : BottomNavItem("properties", Icons.Rounded.List, "Properties") + object More : BottomNavItem("more", Icons.Rounded.Info, "More") + object Logs : BottomNavItem("log", Icons.Rounded.ExitToApp, "Logs") + } + + object APIInfo : NavigationRoute("/info/{index}") + object SharedPrefSetting : NavigationRoute("pref_setting") + object Search : NavigationRoute("search") + object PrefData : NavigationRoute("/pref_data/{prefIdentifier}") +} + +@Composable +internal fun BottomBar( + items: List, + selectedRoute: String, + onclick: (String) -> Unit +) { + NavigationBar { + items.forEach { item -> + addNavItem( + item = item, + onClick = { onclick(item.route) }, + isSelected = item.route == selectedRoute + ) + } + } +} + +@Composable +internal fun RowScope.addNavItem( + item: NavigationRoute.BottomNavItem, + onClick: () -> Unit, + isSelected: Boolean +) { + NavigationBarItem( + selected = isSelected, + onClick = onClick, + icon = { + Icon(imageVector = item.icon, contentDescription = null) + }, + alwaysShowLabel = true, + label = { + Text(text = item.title) + }, + colors = NavigationBarItemDefaults.colors() + ) +} diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/InputDialog.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/InputDialog.kt new file mode 100644 index 0000000..20a32c1 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/InputDialog.kt @@ -0,0 +1,95 @@ +package com.abhi165.noober.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog + + +@Composable +internal fun InputDialogView( + heading: String, + onDismiss:(String?) -> Unit) { + + var newPrefValue by remember { + mutableStateOf("") + } + + Dialog(onDismissRequest = { onDismiss(null) }) { + Card( + shape = MaterialTheme.shapes.medium, + modifier = Modifier + .padding(8.dp) + , + ) { + Column( + Modifier + .background(MaterialTheme.colorScheme.surface) + ) { + Text( + text = heading, + modifier = Modifier.padding(8.dp), + fontSize = 20.sp, + style = MaterialTheme.typography.headlineSmall + ) + + OutlinedTextField( + value = newPrefValue, + onValueChange = { newPrefValue = it }, + modifier = Modifier.padding(8.dp), + label = { Text("Enter new value") }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + onDismiss(newPrefValue) + } + ) + ) + + Row { + OutlinedButton( + onClick = { onDismiss(null) }, + Modifier + .fillMaxWidth() + .padding(8.dp) + .weight(1F) + ) { + Text(text = "Cancel") + } + + Button( + onClick = { + onDismiss(newPrefValue) }, + Modifier + .fillMaxWidth() + .padding(8.dp) + .weight(1F) + ) { + Text(text = "Save") + } + } + } + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/LogRow.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/LogRow.kt new file mode 100644 index 0000000..38d1f3a --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/LogRow.kt @@ -0,0 +1,72 @@ +package com.abhi165.noober.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.sharp.Share +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.abhi165.noober.model.LogModel + + +@Composable +internal fun LogRow( + model: LogModel, + onShareClicked: (String) -> Unit +) { + SelectionContainer( + modifier = Modifier + .padding(bottom = 16.dp) + .fillMaxWidth() + ) { + Column( + modifier = Modifier + .background( + if(model.isError) Color.Red.copy(0.1f) else Color.LightGray.copy(0.1f), + MaterialTheme.shapes.medium + ) + .border(1.dp, Color.DarkGray.copy(0.6f), MaterialTheme.shapes.medium ) + .padding(8.dp) + ) { + if(model.isError) { + Button( + onClick = { onShareClicked(model.value) }, + modifier = Modifier.align(Alignment.End) + ) { + Icon( + imageVector = Icons.Sharp.Share, + contentDescription = "Share JSON Data" + ) + + Text( + text = "Share", + fontWeight = FontWeight.SemiBold + ) + } + } + + Text( + text = model.date, + style = MaterialTheme.typography.bodySmall, + ) + Text( + text = model.tag, + style = MaterialTheme.typography.headlineSmall, + ) + + Text(text = model.value) + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/MainAppBar.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/MainAppBar.kt new file mode 100644 index 0000000..896cbbe --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/MainAppBar.kt @@ -0,0 +1,177 @@ +package com.abhi165.noober.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.TextField +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.sharp.ArrowBack +import androidx.compose.material.icons.sharp.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp + +internal enum class SearchWidgetState { + OPENED, + CLOSED +} + +@Composable +internal fun NoobAppBar( + searchWidgetState: SearchWidgetState, + canGoBack: Boolean, + showPrefSetting: Boolean, + searchTextState: String, + onSettingClicked: () -> Unit, + onBackClicked: ()-> Unit, + onTextChange: (String) -> Unit, + onSearchClicked: (String) -> Unit, + onSearchTriggered: () -> Unit +) { + when (searchWidgetState) { + SearchWidgetState.CLOSED -> { + DefaultAppBar( + onSearchClicked = onSearchTriggered, + canGoBack = canGoBack, + onBackClicked = onBackClicked, + showPrefSetting = showPrefSetting, + onSettingClicked = onSettingClicked + ) + } + SearchWidgetState.OPENED -> { + SearchAppBar( + text = searchTextState, + onTextChange = onTextChange, + onCloseClicked = onBackClicked, + onSearchClicked = onSearchClicked + ) + } + } +} + +@Composable +fun DefaultAppBar( + canGoBack: Boolean, + showPrefSetting: Boolean, + onSearchClicked: () -> Unit, + onBackClicked: () -> Unit, + onSettingClicked: () -> Unit, +) { + TopAppBar( + title = { + Text( + text = "Noober", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + }, + + navigationIcon = { + AnimatedVisibility(canGoBack) { + IconButton(onBackClicked){ + Icon(Icons.Sharp.ArrowBack, contentDescription = null) + } + } + }, + actions = { + IconButton( + onClick = { onSearchClicked() } + ) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = "Search Icon", + tint = Color.Black + ) + } + AnimatedVisibility(showPrefSetting) { + IconButton(onSettingClicked) { + Icon(Icons.Sharp.Settings, contentDescription = null) + } + } + } + ) +} + +@Composable +fun SearchAppBar( + text: String, + onTextChange: (String) -> Unit, + onCloseClicked: () -> Unit, + onSearchClicked: (String) -> Unit, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + shadowElevation = 5.dp, + color = MaterialTheme.colorScheme.primary + ) { + TextField(modifier = Modifier + .fillMaxWidth(), + value = text, + onValueChange = { + onTextChange(it) + }, + placeholder = { + Text( + modifier = Modifier + .alpha(0.6f), + text = "Search here...", + color = Color.Black + ) + }, + textStyle = MaterialTheme.typography.titleSmall, + singleLine = true, + leadingIcon = { + IconButton( + onClick = {} + ) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search Icon", + tint = Color.Black + ) + } + }, + trailingIcon = { + IconButton( + onClick = { + if (text.isNotEmpty()) { + onTextChange("") + } else { + onCloseClicked() + } + } + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close Icon", + tint = Color.Black + ) + } + }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search + ), + keyboardActions = KeyboardActions( + onSearch = { + onSearchClicked(text) + } + )) + } +} + diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/RootNavGraph.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/RootNavGraph.kt new file mode 100644 index 0000000..72a6edc --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/RootNavGraph.kt @@ -0,0 +1,98 @@ +package com.abhi165.noober.ui.components + +import androidx.compose.animation.slideInHorizontally +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.abhi165.noober.NoobRepository +import com.abhi165.noober.ui.APICallScreen +import com.abhi165.noober.ui.APITabScreen +import com.abhi165.noober.ui.AvailableSharedPreferencesScreen +import com.abhi165.noober.ui.LogsScreen +import com.abhi165.noober.ui.MoreScreen +import com.abhi165.noober.ui.SearchScreen +import com.abhi165.noober.ui.SharedPrefSettingView +import com.abhi165.noober.ui.SharedPreferenceDataScreen +import moe.tlaster.precompose.navigation.NavHost +import moe.tlaster.precompose.navigation.NavOptions +import moe.tlaster.precompose.navigation.Navigator +import moe.tlaster.precompose.navigation.PopUpTo +import moe.tlaster.precompose.navigation.path +import moe.tlaster.precompose.navigation.transition.NavTransition + +@Composable +internal fun RootNavGraph(navigator: Navigator, modifier: Modifier) { + NavHost( + modifier = modifier, + navigator = navigator, + initialRoute = NavigationRoute.BottomNavItem.Home.route, + ) { + scene(NavigationRoute.BottomNavItem.Home.route) { + APICallScreen { + navigator.navigate(NavigationRoute.APIInfo.route.replace("{index}", it.toString())) + } + } + + scene( + NavigationRoute.BottomNavItem.Properties.route, + navTransition = NavTransition( + createTransition = slideInHorizontally(), + ) + ) { + AvailableSharedPreferencesScreen { + navigator.navigate(NavigationRoute.PrefData.route.replace("{prefIdentifier}", it)) + } + } + + scene( + NavigationRoute.BottomNavItem.Logs.route, + navTransition = NavTransition( + createTransition = slideInHorizontally(), + ) + ) { + LogsScreen() + } + + scene( + NavigationRoute.BottomNavItem.More.route, + navTransition = NavTransition( + createTransition = slideInHorizontally(), + ) + ) { + MoreScreen() + } + + scene(NavigationRoute.APIInfo.route) { + val index: Int = it.path("index") ?: -1 + val state = NoobRepository.requestList.value[index] + APITabScreen(state) + } + + scene(NavigationRoute.SharedPrefSetting.route) { + SharedPrefSettingView { + navigator.goBack() + } + } + + scene( + NavigationRoute.PrefData.route, + navTransition = NavTransition( + createTransition = slideInHorizontally(), + ) + ) { + val identifier: String = it.path("prefIdentifier") ?: "" + SharedPreferenceDataScreen(prefIdentifier = identifier) + } + + scene(NavigationRoute.Search.route) { + SearchScreen(searchState = NoobRepository.searchedData, onAPICallTapped = { + val index = NoobRepository.requestList.value.indexOf(it) + NoobRepository.changeSearchWidgetState(SearchWidgetState.CLOSED) + navigator.navigate(NavigationRoute.APIInfo.route.replace("{index}", index.toString()), + options = NavOptions(launchSingleTop = true, popUpTo = PopUpTo(NavigationRoute.BottomNavItem.Home.route, inclusive = false))) + }, goBack = { + navigator.goBack() + NoobRepository.changeSearchWidgetState(SearchWidgetState.CLOSED) + }) + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/SharedPrefRow.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/SharedPrefRow.kt new file mode 100644 index 0000000..57d5046 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/ui/components/SharedPrefRow.kt @@ -0,0 +1,84 @@ +package com.abhi165.noober.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +internal fun SharedPrefRow( + modifier: Modifier = Modifier, + key: String, + value: String, + onValueChanges: (String)-> Unit + ) { + var isSelected by remember { + mutableStateOf(false) + } + + val bgColour by animateColorAsState( + targetValue = if (isSelected) Color.White else Color.Blue.copy(0.01f)) + + val focusManager = LocalFocusManager.current + + Column( + modifier = modifier + .padding(horizontal = 4.dp, vertical = 4.dp) + .clickable { + isSelected = !isSelected + } + .background( + bgColour, + RoundedCornerShape(8.dp) + ) + .border(width = 1.dp, shape = RoundedCornerShape(8.dp), color = Color.DarkGray) + .fillMaxWidth() + .wrapContentHeight() + .padding(8.dp) + ) { + + Text( + text = key, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + + AnimatedVisibility(visible = isSelected) { + InputDialogView(key) { + it?.let { + onValueChanges(it) + } + isSelected = false + } + } + + AnimatedVisibility(visible = !isSelected) { + SelectionContainer { + Text( + text = value, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.labelMedium + ) + } + } + } +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/util/Constants.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/util/Constants.kt new file mode 100644 index 0000000..6ae2215 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/util/Constants.kt @@ -0,0 +1,10 @@ +package com.abhi165.noober.util + +internal object Constants { + const val PROTOCOL_KEY = "NoobProtocol" + const val NOOB_HOST = "open" + const val NOOB_SCHEME = "noober" + const val NOOB_PLACEHOLDER = "{Noob}" + const val IS_FROM_ANDROID = "is_from_android" + +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/util/DateUtill.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/util/DateUtill.kt new file mode 100644 index 0000000..635d0c2 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/util/DateUtill.kt @@ -0,0 +1,24 @@ +package com.abhi165.noober.util + +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime + +internal object DateUtil { + + fun now(): String = Clock.System.now().toString() + + + fun calculateDiff(from: String, to:String): String { + val responseTime = from.toInstant() + val requestTime = to.toInstant() + val difference = (responseTime - requestTime).absoluteValue + return difference.toString() + } + + fun formatTime(string: String): String{ + val time = string.toInstant().toLocalDateTime(TimeZone.currentSystemDefault()) + return "${time.hour}:${time.minute}:${time.second}" + } +} diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/util/Extension.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/util/Extension.kt new file mode 100644 index 0000000..76b8b3c --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/util/Extension.kt @@ -0,0 +1,39 @@ +package com.abhi165.noober.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import com.abhi165.noober.isAndroid +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import moe.tlaster.precompose.flow.collectAsStateWithLifecycle +import moe.tlaster.precompose.lifecycle.LocalLifecycleOwner +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + + +@Composable +internal fun StateFlow.collectAsStateWithLifecycleOrCollectAsState( + context: CoroutineContext = EmptyCoroutineContext, +): State { + if(isAndroid()) + return collectAsStateWithLifecycle(initial = this.value, context = context) + return collectAsState(context) +} + +@Composable +internal fun Flow.collectAsStateWithLifecycleOrCollectAsState( + initial: R, + context: CoroutineContext = EmptyCoroutineContext, +): State { + if(isAndroid()) { + val lifecycleOwner = checkNotNull(LocalLifecycleOwner.current) + return collectAsStateWithLifecycle( + initial = initial, + lifecycle = lifecycleOwner.lifecycle, + context = context, + ) + } + + return collectAsState(initial) +} \ No newline at end of file diff --git a/noober/src/commonMain/kotlin/com/abhi165/noober/util/Util.kt b/noober/src/commonMain/kotlin/com/abhi165/noober/util/Util.kt new file mode 100644 index 0000000..a5980f1 --- /dev/null +++ b/noober/src/commonMain/kotlin/com/abhi165/noober/util/Util.kt @@ -0,0 +1,72 @@ +package com.abhi165.noober.util + + +internal fun prettyPrint(jsonString: String): String { + val indentation = " " + val builder = StringBuilder() + var indentLevel = 0 + var inString = false + + for (char in jsonString) { + when (char) { + '{', '[' -> { + builder.append(char) + builder.append('\n') + indentLevel++ + builder.append(indentation.repeat(indentLevel)) + } + '}', ']' -> { + builder.append('\n') + indentLevel-- + builder.append(indentation.repeat(indentLevel)) + builder.append(char) + } + ',' -> { + builder.append(char) + if (!inString) { + builder.append('\n') + builder.append(indentation.repeat(indentLevel)) + } + } + '"' -> { + builder.append(char) + inString = !inString + } + else -> builder.append(char) + } + } + + return builder.toString() +} + +fun generateCurlCommand( + url: String, + method: String = "GET", + headers: Map = emptyMap(), + body: String? = null +): String { + val curlCommand = StringBuilder("curl") + + // Add method (-X) option + curlCommand.append(" -X $method") + + // Add headers (-H) options + for ((key, value) in headers) { + curlCommand.append(" -H \"$key: $value\"") + } + + // Add the URL + curlCommand.append(" \"$url\"") + + // Add request body if provided + if (!body.isNullOrBlank()) { + curlCommand.append(" -d \"$body\"") + } + + return curlCommand.toString() +} + +fun isURLValid(url: String): Boolean { + val regex = Regex("""(http|https)://[a-zA-Z0-9.-]+(/[a-zA-Z0-9.-]+)?""") + return regex.matches(url) +} \ No newline at end of file diff --git a/noober/src/iosMain/kotlin/com/abhi165/noober/AccountManagerImpl.kt b/noober/src/iosMain/kotlin/com/abhi165/noober/AccountManagerImpl.kt new file mode 100644 index 0000000..27c5f18 --- /dev/null +++ b/noober/src/iosMain/kotlin/com/abhi165/noober/AccountManagerImpl.kt @@ -0,0 +1,18 @@ +package com.abhi165.noober + +import com.abhi165.noober.util.DeepLinkHandler +import com.abhi165.noober.util.UserDefaultManager + +internal object AccountManagerImpl: AccountManager { + override suspend fun generateDeepLink(): String { + val userProp = mutableMapOf() + NoobRepository.userProperties.forEach {prop -> + userProp[prop.key] = UserDefaultManager.getValueFor(prop.key).toString() + } + return DeepLinkHandler.generateDeepLink(userProp) + } + + override fun restoreAccount() { + UserDefaultManager.restoreAccount() + } +} \ No newline at end of file diff --git a/noober/src/iosMain/kotlin/com/abhi165/noober/Noober.kt b/noober/src/iosMain/kotlin/com/abhi165/noober/Noober.kt new file mode 100644 index 0000000..2d3affe --- /dev/null +++ b/noober/src/iosMain/kotlin/com/abhi165/noober/Noober.kt @@ -0,0 +1,41 @@ +package com.abhi165.noober + +import com.abhi165.noober.util.DeepLinkHandler +import com.abhi165.noober.util.NoobHelper +import com.abhi165.noober.util.NoobProtocol +import com.abhi165.noober.util.NoobWindow +import com.abhi165.noober.util.UserDefaultManager +import kotlinx.cinterop.CValue +import kotlinx.cinterop.ExperimentalForeignApi +import platform.CoreGraphics.CGRect +import platform.Foundation.NSURL +import platform.UIKit.UIWindow +import platform.UIKit.UIWindowScene + + +actual object Noober: NooberCommon { + + fun start() { + NoobHelper.registerProtocol() + } + + fun getNoobProtocol() = NoobProtocol.`class`() + + @OptIn(ExperimentalForeignApi::class) + fun getNoobWindow(frame: CValue): UIWindow = NoobWindow(frame) + @OptIn(ExperimentalForeignApi::class) + fun getNoobWindow(scene: UIWindowScene): UIWindow = NoobWindow(scene) + + fun importAccountFromNoob(url: NSURL) { + DeepLinkHandler.handleDeepLink(url) + } + + fun toggle() { + NoobHelper.toggle() + } +} + +internal actual fun getSharedPrefManager(): SharedPrefManager = UserDefaultManager +internal actual fun isAndroid(): Boolean = false +internal actual fun share(data: String) = NoobHelper.share(data) +internal actual fun getAccountManager(): AccountManager = AccountManagerImpl diff --git a/noober/src/iosMain/kotlin/com/abhi165/noober/util/DeepLinkHandler.kt b/noober/src/iosMain/kotlin/com/abhi165/noober/util/DeepLinkHandler.kt new file mode 100644 index 0000000..ec6563f --- /dev/null +++ b/noober/src/iosMain/kotlin/com/abhi165/noober/util/DeepLinkHandler.kt @@ -0,0 +1,35 @@ +package com.abhi165.noober.util + +import platform.Foundation.NSURL +import platform.Foundation.NSURLComponents +import platform.Foundation.NSURLQueryItem + +internal object DeepLinkHandler { + + fun generateDeepLink(userProp: Map): String { + val urlComponent = NSURLComponents().apply { + setScheme(Constants.NOOB_SCHEME) + setHost(Constants.NOOB_HOST) + } + + + val queryItems = userProp.map { (key, value) -> + NSURLQueryItem(key, value) + }.toMutableList() + queryItems.add(NSURLQueryItem(Constants.IS_FROM_ANDROID, "0")) + urlComponent.queryItems = queryItems + + return urlComponent.URL?.absoluteString ?: "" + } + + fun handleDeepLink(url: NSURL) { + if(url.host() != Constants.NOOB_HOST) return + val parameters = mutableMapOf() + NSURLComponents(url, false).queryItems?.map { it as? NSURLQueryItem }?.forEach { + it?.let {query -> + parameters[query.name] = query.value ?: "" + } + } + UserDefaultManager.importAccount(parameters) + } +} \ No newline at end of file diff --git a/noober/src/iosMain/kotlin/com/abhi165/noober/util/Extension.kt b/noober/src/iosMain/kotlin/com/abhi165/noober/util/Extension.kt new file mode 100644 index 0000000..3b5640f --- /dev/null +++ b/noober/src/iosMain/kotlin/com/abhi165/noober/util/Extension.kt @@ -0,0 +1,123 @@ +package com.abhi165.noober.util + +import com.abhi165.noober.model.RequestModel +import com.abhi165.noober.model.ResponseModel +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.free +import kotlinx.cinterop.nativeHeap +import platform.Foundation.HTTPBodyStream +import platform.Foundation.HTTPMethod +import platform.Foundation.NSHTTPURLResponse +import platform.Foundation.NSInputStream +import platform.Foundation.NSJSONReadingFragmentsAllowed +import platform.Foundation.NSJSONSerialization +import platform.Foundation.NSJSONWritingPrettyPrinted +import platform.Foundation.NSMutableData +import platform.Foundation.NSString +import platform.Foundation.NSURLRequest +import platform.Foundation.NSURLResponse +import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.allHTTPHeaderFields +import platform.Foundation.appendBytes +import platform.Foundation.create +import platform.darwin.UInt8Var +import kotlin.to + + +internal fun NSURLRequest.toModel(withDate: String = DateUtil.now()): RequestModel { + val url = this.URL?.absoluteString ?: "" + val headers = this.allHTTPHeaderFields()?.filter { it.key != null && it.value != null } + ?.map { it.key.toString() to it.value.toString() } + ?.toMap() ?: mapOf() + val body = this.HTTPBodyStream?.toHttpBody() ?: "" + val method = this.HTTPMethod ?: "" + + return RequestModel( + baseURL = url, + curl = toCurlCommand(body), + header = headers, + body = body, + date = withDate, + method = method, + timeout = this.timeoutInterval.toFloat(), + cachePolicy = this.cachePolicy.toString() + ) +} + +@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) +internal fun NSMutableData.toModel(response: NSURLResponse?, requestDate: String): ResponseModel { + val httpResponse = response as? NSHTTPURLResponse + val headers = httpResponse?.allHeaderFields?.filter { it.key != null && it.value != null } + ?.map { it.key.toString() to it.value.toString() } + ?.toMap() ?: mapOf() + val status = httpResponse?.statusCode ?: 0 + val date = DateUtil.now() + var responseBody = "" + try { + val jsonData = NSJSONSerialization.JSONObjectWithData(this, options = NSJSONReadingFragmentsAllowed, error = null) + jsonData?.let { + val jsonString = NSJSONSerialization.dataWithJSONObject(jsonData, options = NSJSONWritingPrettyPrinted, error = null) + jsonString?.let { + responseBody = NSString.create(it, NSUTF8StringEncoding).toString() + } + } + } + finally { } +return ResponseModel( + header = headers, + body = responseBody, + date = date, + statusCode = status.toInt(), + timeInterval = DateUtil.calculateDiff(from = date, to = requestDate), +) +} + +fun NSURLRequest.toCurlCommand(body: String): String { + val curlCommand = StringBuilder("curl -i") + + this.HTTPMethod()?.let { method -> + curlCommand.append(" -X $method") + } + + this.allHTTPHeaderFields()?.let { headers -> + for ((key, value) in headers) { + curlCommand.append(" -H '$key: $value'") + } + } + + curlCommand.append(" -d '$body'") + + + this.URL()?.let { url -> + curlCommand.append(" '${url.absoluteString}'") + } + + return curlCommand.toString() +} + + +@OptIn(ExperimentalForeignApi::class) +fun NSInputStream.toHttpBody(): String { + this.let { inputStream -> + inputStream.open() + val bufferSize: Int = 16 + + val buffer = nativeHeap.allocArray(bufferSize) + + val data = NSMutableData() + + while (inputStream.hasBytesAvailable) { + val readBytes = inputStream.read(buffer, maxLength = bufferSize.toULong()) + if (readBytes <= 0) { + break + } + data.appendBytes(buffer, length = readBytes.toULong()) + } + + nativeHeap.free(buffer) + inputStream.close() + return NSString.create(data = data, encoding = NSUTF8StringEncoding).toString() + } +} diff --git a/noober/src/iosMain/kotlin/com/abhi165/noober/util/NoobHelper.kt b/noober/src/iosMain/kotlin/com/abhi165/noober/util/NoobHelper.kt new file mode 100644 index 0000000..bbea600 --- /dev/null +++ b/noober/src/iosMain/kotlin/com/abhi165/noober/util/NoobHelper.kt @@ -0,0 +1,60 @@ +package com.abhi165.noober.util + +import com.abhi165.noober.ui.NoobScreen +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.useContents +import moe.tlaster.precompose.PreComposeApplication +import platform.Foundation.NSURLProtocol +import platform.UIKit.UIActivityViewController +import platform.UIKit.UIApplication +import platform.UIKit.UIModalPresentationFullScreen +import platform.UIKit.UIViewController +import kotlin.native.concurrent.ThreadLocal + +@ThreadLocal +internal object NoobHelper { + private var hasAlreadyStarted = false + private var isNoobAlreadyShowing = false + + private val topViewController: UIViewController? + get() { + var rootController = UIApplication.sharedApplication.keyWindow?.rootViewController + while (rootController?.presentedViewController != null) { + rootController = rootController.presentedViewController + } + return rootController + } + + @OptIn(ExperimentalForeignApi::class) + private val noobController: UIViewController by lazy { + var topSafeArea = 0f + UIApplication.sharedApplication.keyWindow?.safeAreaInsets?.useContents { + topSafeArea = this.top.toFloat() + } + PreComposeApplication { NoobScreen(topSafeArea = topSafeArea) } + } + + + fun toggle() { + if(!isNoobAlreadyShowing) { + noobController.modalPresentationStyle = UIModalPresentationFullScreen + topViewController?.presentViewController(noobController, true, null) + } else { + noobController.dismissViewControllerAnimated(true, completion = null) + } + isNoobAlreadyShowing = !isNoobAlreadyShowing + } + + fun share(data: String) { + val activityViewController = UIActivityViewController(listOf(data), null) + topViewController?.presentViewController(activityViewController, true, null) + } + + @OptIn(BetaInteropApi::class) + fun registerProtocol() { + if(hasAlreadyStarted) return + hasAlreadyStarted = true + NoobProtocol.`class`()?.let { NSURLProtocol.registerClass(it) } + } +} \ No newline at end of file diff --git a/noober/src/iosMain/kotlin/com/abhi165/noober/util/NoobProtocol.kt b/noober/src/iosMain/kotlin/com/abhi165/noober/util/NoobProtocol.kt new file mode 100644 index 0000000..e3ca894 --- /dev/null +++ b/noober/src/iosMain/kotlin/com/abhi165/noober/util/NoobProtocol.kt @@ -0,0 +1,124 @@ +package com.abhi165.noober.util + +import com.abhi165.noober.NoobRepository +import com.abhi165.noober.model.APIInfoModel +import com.abhi165.noober.model.RequestModel +import platform.Foundation.NSCachedURLResponse +import platform.Foundation.NSData +import platform.Foundation.NSError +import platform.Foundation.NSMutableData +import platform.Foundation.NSMutableURLRequest +import platform.Foundation.NSURL +import platform.Foundation.NSURLCacheStoragePolicy +import platform.Foundation.NSURLProtocol +import platform.Foundation.NSURLProtocolClientProtocol +import platform.Foundation.NSURLProtocolMeta +import platform.Foundation.NSURLRequest +import platform.Foundation.NSURLResponse +import platform.Foundation.NSURLSession +import platform.Foundation.NSURLSessionConfiguration +import platform.Foundation.NSURLSessionDataDelegateProtocol +import platform.Foundation.NSURLSessionDataTask +import platform.Foundation.NSURLSessionResponseAllow +import platform.Foundation.NSURLSessionResponseDisposition +import platform.Foundation.NSURLSessionTask +import platform.Foundation.appendData + +class NoobProtocol: NSURLProtocol, NSURLSessionDataDelegateProtocol { + + @OverrideInit + constructor ( + request: NSURLRequest, + cachedResponse: NSCachedURLResponse?, + client: NSURLProtocolClientProtocol? + ) : super(request, cachedResponse, client) + + private var urlResponse: NSURLResponse? = null + private var responseData = NSMutableData() + private var requestDate = "" + + private val session = NSURLSession.sessionWithConfiguration( + configuration = NSURLSessionConfiguration.defaultSessionConfiguration, + this, + null + ) + + companion object : NSURLProtocolMeta() { + + override fun canInitWithRequest(request: NSURLRequest): Boolean { + val requestURL = request.URL?.absoluteString ?: "" + return ((requestURL.startsWith("http") || requestURL.startsWith("https")) && + NSURLProtocol.propertyForKey(Constants.PROTOCOL_KEY, request) == null) + } + + override fun canonicalRequestForRequest(request: NSURLRequest): NSURLRequest { + return request + } + } + + override fun startLoading() { + val request = request().mutableCopy() as NSMutableURLRequest + val oldURL = request.URL?.absoluteString() + val newURLString = oldURL?.replace(NoobRepository.OLD_URL, NoobRepository.NEW_URL) + newURLString?.let { + request.setURL(NSURL(string = newURLString)) + } + NSURLProtocol.setProperty(true, Constants.PROTOCOL_KEY, request) + requestDate = DateUtil.now() + session.dataTaskWithRequest(request).resume() + } + + override fun stopLoading() { + session.getTasksWithCompletionHandler { datatask, _, _ -> + datatask?.forEach { + (it as NSURLSessionDataTask).cancel() + } + } + } + + override fun URLSession( + session: NSURLSession, + dataTask: NSURLSessionDataTask, + didReceiveData: NSData + ) { + responseData.appendData(didReceiveData) + client?.URLProtocol(this, didLoadData = didReceiveData) + } + + override fun URLSession( + session: NSURLSession, + task: NSURLSessionTask, + didCompleteWithError: NSError? + ) { + val request = task.originalRequest?.toModel(withDate = requestDate) ?: RequestModel() + val response = responseData.toModel(response = urlResponse, requestDate = requestDate) + val apiModel = APIInfoModel( + request = request, + response = response, + error = didCompleteWithError?.localizedDescription + ) + NoobRepository.recordAPI(apiModel) + + didCompleteWithError?.let { + client?.URLProtocol(this, it) + } ?: kotlin.run { + client?.URLProtocolDidFinishLoading(this) + } + } + + override fun URLSession( + session: NSURLSession, + dataTask: NSURLSessionDataTask, + didReceiveResponse: NSURLResponse, + completionHandler: (NSURLSessionResponseDisposition) -> Unit + ) { + responseData = NSMutableData() + urlResponse = didReceiveResponse + client?.URLProtocol( + this, + didReceiveResponse, + NSURLCacheStoragePolicy.NSURLCacheStorageNotAllowed + ) + completionHandler(NSURLSessionResponseAllow) + } +} diff --git a/noober/src/iosMain/kotlin/com/abhi165/noober/util/NoobWindow.kt b/noober/src/iosMain/kotlin/com/abhi165/noober/util/NoobWindow.kt new file mode 100644 index 0000000..7c8b294 --- /dev/null +++ b/noober/src/iosMain/kotlin/com/abhi165/noober/util/NoobWindow.kt @@ -0,0 +1,31 @@ +package com.abhi165.noober.util + +import com.abhi165.noober.Noober +import kotlinx.cinterop.CValue +import kotlinx.cinterop.ExperimentalForeignApi +import platform.CoreGraphics.CGRect +import platform.UIKit.UIEvent +import platform.UIKit.UIEventSubtype +import platform.UIKit.UIEventSubtypeMotionShake +import platform.UIKit.UIWindow +import platform.UIKit.UIWindowScene + +@OptIn(ExperimentalForeignApi::class) +internal class NoobWindow : UIWindow { + @OverrideInit + constructor ( + frame: CValue + ) : super(frame) + + constructor ( + scene: UIWindowScene + ): super(scene) + + + override fun motionEnded(motion: UIEventSubtype, withEvent: UIEvent?) { + super.motionEnded(motion, withEvent) + if(motion == UIEventSubtypeMotionShake) { + Noober.toggle() + } + } +} \ No newline at end of file diff --git a/noober/src/iosMain/kotlin/com/abhi165/noober/util/UserDefaultManager.kt b/noober/src/iosMain/kotlin/com/abhi165/noober/util/UserDefaultManager.kt new file mode 100644 index 0000000..915467f --- /dev/null +++ b/noober/src/iosMain/kotlin/com/abhi165/noober/util/UserDefaultManager.kt @@ -0,0 +1,87 @@ +package com.abhi165.noober.util + +import com.abhi165.noober.NoobRepository +import com.abhi165.noober.SharedPrefManager +import com.abhi165.noober.model.SharedPrefModel +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import platform.Foundation.NSString +import platform.Foundation.NSUserDefaults +import platform.darwin.NSInteger + +internal object UserDefaultManager: SharedPrefManager { + private val userDefaults = NSUserDefaults.standardUserDefaults + private val noobUserDefault = NSUserDefaults(suiteName = "noob") + + private val _prefStateFlow = MutableStateFlow(SharedPrefModel()) + + override fun getPrefValues(prefName: String): Flow { + getLatestData() + return _prefStateFlow + } + + override fun changeValueOf(key: String, newValue: String, prefName: String, oldValue: Any) { + setNewValueWithSameType(newValue, oldValue, key) + getLatestData() + } + + private fun getLatestData() { + GlobalScope.launch { + val data = getAllValuesWithPrefName().first() + _prefStateFlow.emit(data) + } + } + + override suspend fun getAllValuesWithPrefName(): List { + val userDefaultData = userDefaults.dictionaryRepresentation() + val mappedData = userDefaultData.mapKeys { it.key.toString() }.mapValues { it.value.toString() } + return listOf(SharedPrefModel("", mappedData)) + } + + fun getValueFor(key: String) = userDefaults.objectForKey(key) + + private fun setNewValueWithSameType(newValue: String, oldValue: Any?, key: String) { + when (oldValue) { + is NSInteger -> userDefaults.setInteger(newValue.toLong(), key) + is NSString -> userDefaults.setObject(newValue, key) + is Boolean -> userDefaults.setBool(newValue.toBoolean(), key) + is Float -> userDefaults.setFloat(newValue.toFloat(), key) + is Double -> userDefaults.setDouble(newValue.toDouble(), key) + } + userDefaults.synchronize() + } + + fun importAccount(prop: Map) { + NSUserDefaults.standardUserDefaults.removeSuiteNamed("noob") + val isFromAndroid = prop[Constants.IS_FROM_ANDROID] == "1" + val deppLinkParameters = prop.toMutableMap().also { + it.remove(Constants.IS_FROM_ANDROID) + } + + val userProperties = NoobRepository.userProperties.associate { it.alternateKeyForCrossPlatform to it.key } + for((key, value ) in deppLinkParameters) { + val mappedKey = if(isFromAndroid) userProperties[key] else key + val oldValue = userDefaults.objectForKey(mappedKey ?: key) + setNewValueWithSameType( + newValue = value, + oldValue = oldValue ?: "", + key = mappedKey ?: key + ) + noobUserDefault.setObject(oldValue, mappedKey ?: key) + } + } + + fun restoreAccount() { + val values = noobUserDefault.dictionaryRepresentation() + for((key, oldValue) in values) { + noobUserDefault.removeObjectForKey(key.toString()) + setNewValueWithSameType( + newValue = oldValue.toString(), + oldValue = userDefaults.objectForKey(key.toString()), + key = key.toString() + ) + } + } +} \ No newline at end of file diff --git a/noober/src/main/java/com/abhi165/noober/NoobActivity.kt b/noober/src/main/java/com/abhi165/noober/NoobActivity.kt new file mode 100644 index 0000000..f7ff190 --- /dev/null +++ b/noober/src/main/java/com/abhi165/noober/NoobActivity.kt @@ -0,0 +1,38 @@ +package com.abhi165.noober + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.widget.Toast +import androidx.compose.material3.MaterialTheme +import com.abhi165.noober.ui.NoobScreen +import moe.tlaster.precompose.lifecycle.PreComposeActivity +import moe.tlaster.precompose.lifecycle.setContent + +class NoobActivity : PreComposeActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val route = intent.getStringExtra("route") + NoobHelper.isNooberVisible = true + NoobHelper.notificationManager.cancelNotification() + setContent { + MaterialTheme { + NoobScreen(route = route) + } + } + + val intent: Intent = intent + val action: String? = intent.action + val data: Uri? = intent.data + if (Intent.ACTION_VIEW == action) { + if (DeepLinkHandler.handleDeepLink(data)) + Toast.makeText(this, "Account Switched Successfully", Toast.LENGTH_SHORT).show() + } + } + + override fun onDestroy() { + super.onDestroy() + NoobHelper.isNooberVisible = false + NoobHelper.notificationManager.showNotification("") + } +} diff --git a/noober/src/main/res/values/strings.xml b/noober/src/main/res/values/strings.xml new file mode 100644 index 0000000..d2a1dfb --- /dev/null +++ b/noober/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + NoobActivity + \ No newline at end of file diff --git a/noober/src/main/res/values/themes.xml b/noober/src/main/res/values/themes.xml new file mode 100644 index 0000000..9e9b3ad --- /dev/null +++ b/noober/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +