This commit is contained in:
Joshua Perry 2024-07-11 10:17:50 +01:00
commit 53ba08f097
86 changed files with 80907 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
# https://editorconfig.org
root = true
[*]
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{java,scala,groovy,kt,kts}]
indent_size = 4
[*.gradle]
indent_size = 2
[*.md]
trim_trailing_whitespace = false

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
* text=auto eol=lf
*.bat text=auto eol=crlf

122
.gitignore vendored Normal file
View File

@ -0,0 +1,122 @@
/core/
/build/
/gradle/
/gradlew
/local.properties
/gradlew.bat
## Java
*.class
*.war
*.ear
hs_err_pid*
## Robovm
/ios/robovm-build/
## GWT
/html/war/
/html/gwt-unitCache/
.apt_generated/
.gwt/
gwt-unitCache/
www-test/
.gwt-tmp/
## Android Studio and Intellij and Android in general
/android/libs/armeabi-v7a/
/android/libs/arm64-v8a/
/android/libs/x86/
/android/libs/x86_64/
/android/gen/
.idea/
*.ipr
*.iws
*.iml
/android/out/
com_crashlytics_export_strings.xml
## Eclipse
.classpath
.project
.metadata/
/android/bin/
/core/bin/
/desktop/bin/
/html/bin/
/ios/bin/
*.tmp
*.bak
*.swp
*~.nib
.settings/
.loadpath
.externalToolBuilders/
*.launch
## NetBeans
/nbproject/private/
/android/nbproject/private/
/core/nbproject/private/
/desktop/nbproject/private/
/html/nbproject/private/
/ios/nbproject/private/
/build/
/android/build/
/core/build/
/desktop/build/
/html/build/
/ios/build/
/nbbuild/
/android/nbbuild/
/core/nbbuild/
/desktop/nbbuild/
/html/nbbuild/
/ios/nbbuild/
/dist/
/android/dist/
/core/dist/
/desktop/dist/
/html/dist/
/ios/dist/
/nbdist/
/android/nbdist/
/core/nbdist/
/desktop/nbdist/
/html/nbdist/
/ios/nbdist/
nbactions.xml
nb-configuration.xml
## Gradle
/local.properties
.gradle/
gradle-app.setting
/build/
/android/build/
/core/build/
/desktop/build/
/html/build/
/ios/build/
## OS Specific
.DS_Store
Thumbs.db
## iOS
/ios/xcode/*.xcodeproj/*
!/ios/xcode/*.xcodeproj/xcshareddata
!/ios/xcode/*.xcodeproj/project.pbxproj
/ios/xcode/native/
/ios/IOSLauncher.app
/ios/IOSLauncher.app.dSYM

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
<application
android:enableOnBackInvokedCallback="true"
android:usesCleartextTraffic="true"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Dermy_app"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

106
android/build.gradle Normal file
View File

@ -0,0 +1,106 @@
android {
namespace "xyz.r0r5chach.dermy_app"
compileSdk 35
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['../assets']
jniLibs.srcDirs = ['libs']
}
}
packagingOptions {
exclude 'META-INF/robovm/ios/robovm.xml'
exclude 'META-INF/native-image/org.mongodb/bson/native-image.properties'
}
defaultConfig {
applicationId "xyz.r0r5chach.dermy_app"
minSdkVersion 26
targetSdkVersion 35
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
renderScript true
aidl true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled false
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation "androidx.room:room-ktx:$roomVersion"
implementation "androidx.room:room-runtime:$roomVersion"
ksp "androidx.room:room-compiler:$roomVersion"
}
// called every time gradle gets executed, takes the native dependencies of
// the natives configuration, and extracts them to the proper libs/ folders
// so they get packed with the APK.
tasks.register('copyAndroidNatives') {
doFirst {
file("libs/armeabi-v7a/").mkdirs()
file("libs/arm64-v8a/").mkdirs()
file("libs/x86_64/").mkdirs()
file("libs/x86/").mkdirs()
configurations.natives.copy().files.each { jar ->
def outputDir = null
if (jar.name.endsWith("natives-arm64-v8a.jar")) outputDir = file("libs/arm64-v8a")
if (jar.name.endsWith("natives-armeabi-v7a.jar")) outputDir = file("libs/armeabi-v7a")
if (jar.name.endsWith("natives-x86_64.jar")) outputDir = file("libs/x86_64")
if (jar.name.endsWith("natives-x86.jar")) outputDir = file("libs/x86")
if (outputDir != null) {
copy {
from zipTree(jar)
into outputDir
include "*.so"
}
}
}
}
}
tasks.matching { it.name.contains("merge") && it.name.contains("JniLibFolders") }.configureEach { packageTask ->
packageTask.dependsOn 'copyAndroidNatives'
}
tasks.register('run', Exec) {
def path
def localProperties = project.file("../local.properties")
if (localProperties.exists()) {
Properties properties = new Properties()
localProperties.withInputStream { instr ->
properties.load(instr)
}
def sdkDir = properties.getProperty('sdk.dir')
if (sdkDir) {
path = sdkDir
} else {
path = "$System.env.ANDROID_HOME"
}
} else {
path = "$System.env.ANDROID_HOME"
}
def adb = path + "/platform-tools/adb"
commandLine "$adb", 'shell', 'am', 'start', '-n', 'xyz.r0r5chach.dermy_app/xyz.r0r5chach.dermy_app.MainActivity'
}
eclipse.project.name = appName + "-android"

30
android/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,30 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-verbose
-dontwarn com.badlogic.gdx.backends.android.AndroidFragmentApplication
# Required if using Gdx-Controllers extension
# Required if using Box2D extension
-keep class androidx.room.* { *; }
-dontwarn androidx.room.**

View File

@ -0,0 +1,9 @@
# This file is used by the Eclipse ADT plugin. It is unnecessary for IDEA and Android Studio projects, which
# configure Proguard and the Android target via the build.gradle file.
# To enable ProGuard to work with Eclipse ADT, uncomment this (available properties: sdk.dir, user.home)
# and ensure proguard.jar in the Android SDK is up to date (or alternately reduce the android target to 23 or lower):
# proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-rules.pro
# Project target.
target=android-19

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.0001 10.9999L22.0002 10.9997L22.0002 12.9997L13.0001 12.9999L13.0001 21.9998L11.0001 21.9998L11.0001 12.9999L2.00004 13.0001L2 11.0001L11.0001 10.9999L11 2.00025L13 2.00024L13.0001 10.9999Z"
android:fillColor="?attr/buttonIcon"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M16,2L21,7V21.008C21,21.556 20.555,22 20.007,22H3.993C3.445,22 3,21.545 3,21.008V2.992C3,2.444 3.445,2 3.993,2H16ZM11,11H8V13H11V16H13V13H16V11H13V8H11V11Z"
android:fillColor="?attr/buttonIcon"/>
</vector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#000000"/>
<size
android:width="200dp"
android:height="200dp"/>
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12 1L21.5 6.5V17.5L12 23L2.5 17.5V6.5L12 1ZM12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
android:fillColor="?attr/buttonIcon"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3 3H12.382C12.7607 3 13.107 3.214 13.2764 3.55279L14 5H20C20.5523 5 21 5.44772 21 6V17C21 17.5523 20.5523 18 20 18H13.618C13.2393 18 12.893 17.786 12.7236 17.4472L12 16H5V22H3V3Z"
android:fillColor="?attr/buttonIcon"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M21,20C21,20.552 20.552,21 20,21H4C3.448,21 3,20.552 3,20V9.489C3,9.18 3.142,8.889 3.386,8.7L11.386,2.477C11.747,2.197 12.253,2.197 12.614,2.477L20.614,8.7C20.858,8.889 21,9.18 21,9.489V20Z"
android:fillColor="?attr/buttonIcon"/>
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.0607 8.11097L14.4749 9.52518C17.2086 12.2589 17.2086 16.691 14.4749 19.4247L14.1214 19.7782C11.3877 22.5119 6.95555 22.5119 4.22188 19.7782C1.48821 17.0446 1.48821 12.6124 4.22188 9.87874L5.6361 11.293C3.68348 13.2456 3.68348 16.4114 5.6361 18.364C7.58872 20.3166 10.7545 20.3166 12.7072 18.364L13.0607 18.0105C15.0133 16.0578 15.0133 12.892 13.0607 10.9394L11.6465 9.52518L13.0607 8.11097ZM19.7782 14.1214L18.364 12.7072C20.3166 10.7545 20.3166 7.58872 18.364 5.6361C16.4114 3.68348 13.2456 3.68348 11.293 5.6361L10.9394 5.98965C8.98678 7.94227 8.98678 11.1081 10.9394 13.0607L12.3536 14.4749L10.9394 15.8891L9.52518 14.4749C6.79151 11.7413 6.79151 7.30911 9.52518 4.57544L9.87874 4.22188C12.6124 1.48821 17.0446 1.48821 19.7782 4.22188C22.5119 6.95555 22.5119 11.3877 19.7782 14.1214Z"
android:fillColor="?attr/buttonIcon"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M2 5L9 2L15 5L21.303 2.2987C21.5569 2.18992 21.8508 2.30749 21.9596 2.56131C21.9862 2.62355 22 2.69056 22 2.75827V19L15 22L9 19L2.69696 21.7013C2.44314 21.8101 2.14921 21.6925 2.04043 21.4387C2.01375 21.3765 2 21.3094 2 21.2417V5ZM16 19.3955L20 17.6812V5.03308L16 6.74736V19.3955ZM14 19.2639V6.73607L10 4.73607V17.2639L14 19.2639ZM8 17.2526V4.60451L4 6.31879V18.9669L8 17.2526Z"
android:fillColor="?attr/buttonIcon"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M15.224,15.508L13.011,20.158C12.869,20.457 12.511,20.584 12.212,20.442C12.145,20.41 12.085,20.367 12.034,20.313L8.492,16.574C8.397,16.474 8.271,16.41 8.134,16.392L3.028,15.724C2.7,15.681 2.468,15.38 2.511,15.052C2.521,14.978 2.544,14.908 2.579,14.843L5.04,10.319C5.106,10.198 5.128,10.058 5.103,9.923L4.16,4.86C4.099,4.534 4.314,4.221 4.64,4.16C4.713,4.147 4.787,4.147 4.86,4.16L9.922,5.103C10.058,5.129 10.198,5.106 10.319,5.04L14.842,2.579C15.134,2.421 15.498,2.528 15.656,2.819C15.692,2.884 15.715,2.955 15.724,3.028L16.392,8.135C16.41,8.271 16.474,8.398 16.574,8.492L20.313,12.034C20.553,12.262 20.564,12.642 20.336,12.882C20.285,12.936 20.225,12.979 20.158,13.011L15.508,15.224C15.383,15.283 15.283,15.384 15.224,15.508ZM16.021,17.435L17.435,16.021L21.677,20.263L20.263,21.678L16.021,17.435Z"
android:fillColor="?attr/buttonIcon"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18,21V13H6V21H4C3.448,21 3,20.552 3,20V4C3,3.448 3.448,3 4,3H17L21,7V20C21,20.552 20.552,21 20,21H18ZM16,21H8V15H16V21Z"
android:fillColor="?attr/buttonIcon"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4 3H20C20.5523 3 21 3.44772 21 4V11H3V4C3 3.44772 3.44772 3 4 3ZM3 13H21V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V13ZM7 16V18H10V16H7ZM7 6V8H10V6H7Z"
android:fillColor="?attr/buttonIcon"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4 22C4 17.5817 7.58172 14 12 14C16.4183 14 20 17.5817 20 22H4ZM12 13C8.685 13 6 10.315 6 7C6 3.685 8.685 1 12 1C15.315 1 18 3.685 18 7C18 10.315 15.315 13 12 13Z"
android:fillColor="?attr/buttonIcon"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageButton
android:id="@+id/home_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="25dp"
android:background="@drawable/home_icon"
android:contentDescription="@string/string_home"
app:layout_constraintBottom_toBottomOf="@+id/fragment_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/fragment_container" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<EditText
android:id="@+id/username_field"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@null"
android:autofillHints="username"
android:hint="@string/string_username"
android:inputType="text"
app:layout_constrainedHeight="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/password_field"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_min="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_min="250dp"
app:layout_constraintWidth_percent="0.25" />
<EditText
android:id="@+id/password_field"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@null"
android:autofillHints="password"
android:hint="@string/string_password"
android:inputType="textPassword"
app:layout_constrainedHeight="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/login_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_min="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username_field"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_min="250dp"
app:layout_constraintWidth_percent="0.25" />
<Button
android:id="@+id/login_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/string_login"
app:layout_constrainedHeight="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/register_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_min="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password_field"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_min="250dp"
app:layout_constraintWidth_percent="0.25" />
<Button
android:id="@+id/register_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/string_register"
app:layout_constrainedHeight="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/backup_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_min="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/login_button"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_min="250dp"
app:layout_constraintWidth_percent="0.25" />
<Button
android:id="@+id/backup_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/string_backup"
app:layout_constrainedHeight="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="@id/restore_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_min="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/login_button"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_min="250dp"
app:layout_constraintWidth_percent="0.25"
android:visibility="gone"/>
<Button
android:id="@+id/restore_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/string_restore"
app:layout_constrainedHeight="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/logout_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_min="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/backup_button"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_min="250dp"
app:layout_constraintWidth_percent="0.25"
android:visibility="gone"/>
<Button
android:id="@+id/logout_button"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/string_logout"
app:layout_constrainedHeight="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_min="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/restore_button"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_min="250dp"
app:layout_constraintWidth_percent="0.25"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/camera_preview"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</androidx.camera.view.PreviewView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,69 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageButton
android:id="@+id/add_mole_button"
android:background="@drawable/add_icon"
android:contentDescription="@string/string_add_mole"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/links_button"
app:layout_constraintEnd_toStartOf="@+id/map_button"
app:layout_constraintHeight_percent="0.15"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_percent="0.28" />
<ImageButton
android:id="@+id/map_button"
android:background="@drawable/map_icon"
android:contentDescription="@string/string_open_body_map"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/settings_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.15"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/add_mole_button"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_percent="0.28" />
<ImageButton
android:id="@+id/links_button"
android:background="@drawable/links_icon"
android:contentDescription="@string/string_open_useful_links"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/settings_button"
app:layout_constraintHeight_percent="0.15"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/add_mole_button"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_percent="0.28" />
<ImageButton
android:id="@+id/settings_button"
android:background="@drawable/cog_icon"
android:contentDescription="@string/string_open_settings"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.15"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/links_button"
app:layout_constraintTop_toBottomOf="@+id/map_button"
app:layout_constraintWidth_max="320dp"
app:layout_constraintWidth_percent="0.28" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/links_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/location_name_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/location_empty_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/string_you_have_no_moles_here"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/location_name_text" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/moles_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/location_name_text">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,292 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="48dp">
<ImageView
android:id="@+id/mole_image"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="100dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="100dp"
android:contentDescription="@string/string_mole_image"
app:layout_constraintBottom_toTopOf="@+id/mole_details"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/circle_button" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/mole_details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="25dp"
android:layout_marginEnd="25dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@+id/mole_logs"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/mole_image">
<LinearLayout
android:id="@+id/mole_location"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="25dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/mole_predictions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/mole_location_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/string_location"
android:textSize="20sp" />
<Space
android:layout_width="match_parent"
android:layout_height="10dp"
tools:layout_editor_absoluteY="27dp" />
<LinearLayout
android:id="@+id/mole_side"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_editor_absoluteY="37dp">
<TextView
android:id="@+id/side_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/string_side" />
<EditText
android:id="@+id/side_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autofillHints=""
android:ems="10"
android:inputType="text"
android:minHeight="48dp"
tools:layout_editor_absoluteY="19dp"
tools:ignore="LabelFor" />
</LinearLayout>
<LinearLayout
android:id="@+id/mole_body_part"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_editor_absoluteY="101dp">
<TextView
android:id="@+id/body_part_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/string_body_part" />
<EditText
android:id="@+id/body_part_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autofillHints=""
android:ems="10"
android:inputType="text"
tools:ignore="LabelFor"
tools:layout_editor_absoluteY="20dp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/mole_predictions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="25dp"
android:layout_marginBottom="25dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/mole_location"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/mole_predictions_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/string_prediction"
android:textSize="20sp" />
<Space
android:layout_width="match_parent"
android:layout_height="10dp"
tools:layout_editor_absoluteY="240dp" />
<LinearLayout
android:id="@+id/mole_benign_prediction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_editor_absoluteY="250dp">
<TextView
android:id="@+id/benign_prediction_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/string_benign_probability" />
<EditText
android:id="@+id/benign_prediction_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autofillHints=""
android:ems="10"
android:inputType="text"
tools:ignore="LabelFor"
tools:layout_editor_absoluteY="100dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/mole_malignant_prediction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_editor_absoluteY="350dp">
<TextView
android:id="@+id/malignant_prediction_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/string_malignant_probability" />
<EditText
android:id="@+id/malignant_prediction_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autofillHints=""
android:ems="10"
android:inputType="text"
tools:ignore="LabelFor" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/mole_logs"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="25dp"
android:layout_marginEnd="25dp"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/mole_buttons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/mole_details">
<TextView
android:id="@+id/log_empty_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/string_this_mole_has_no_logs"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/mole_logs_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:text="@string/string_logs"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@+id/log_list"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/log_list"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/mole_logs_header">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/mole_buttons"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="25dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="25dp"
android:layout_marginBottom="25dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/mole_logs">
<ImageButton
android:id="@+id/mole_add_log_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="25dp"
android:layout_marginBottom="10dp"
android:background="@drawable/add_log_icon"
android:contentDescription="@string/string_add_log"
android:minHeight="48dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/mole_predict_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/mole_save_button" />
<ImageButton
android:id="@+id/mole_predict_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="25dp"
android:layout_marginBottom="10dp"
android:background="@drawable/predict_icon"
android:contentDescription="@string/string_new_prediction"
android:minHeight="48dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/mole_add_log_button"
app:layout_constraintTop_toBottomOf="@+id/mole_save_button" />
<ImageButton
android:id="@+id/mole_save_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/save_icon"
android:contentDescription="@string/save_mole"
android:minHeight="48dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/link_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginBottom="50dp"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/mole_list_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/circle_button" />
</LinearLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,17 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Dermy_app" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="buttonIcon">@color/white</item>
</style>
</resources>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -0,0 +1,48 @@
<resources>
<string name="app_name">Dermy</string>
<string name="string_add_mole">Add Mole</string>
<string name="string_open_body_map">Open Body Map</string>
<string name="string_open_useful_links">Open Useful Links</string>
<string name="string_open_settings">Open Settings</string>
<string name="string_login">Login</string>
<string name="string_username">username</string>
<string name="string_password">password</string>
<string name="string_backup">Backup</string>
<string name="string_restore">Restore</string>
<string name="string_logout">Logout</string>
<string name="string_register">Register</string>
<string name="disclaimer"><!-- TODO: Remove or change this placeholder text -->
1. General Information
\nThe information provided by Dermy (“we,” “us” or “our”) on The Dermy App (the “App”) is for general informational purposes only and is based on a deep learning algorithm. While our model, based on MobileNetV3, attempts to predict the likelihood of a mole being malignant or benign, it has an accuracy rating of approximately 80%. All information on the App is provided in good faith; however, we make no representation or warranty of any kind, express or implied, regarding the accuracy, adequacy, validity, reliability, availability, or completeness of any information on the App.
\n\n2. Not Medical Advice
\nThe App cannot and does not provide medical advice. The health information provided is for general informational and educational purposes only and is not a substitute for professional medical advice, diagnosis, or treatment. Always seek the advice of your physician or other qualified health provider with any questions you may have regarding a medical condition. Never disregard professional medical advice or delay in seeking it because of something you have read on this App.
\n\n3. Limitation of Accuracy
\nOur model\'s predictions should not be taken as definitive or conclusive. While our algorithm uses advanced techniques, it is not infallible and its accuracy is limited to approximately 80%. The results generated by the App should not be used as a sole basis for making health decisions. Users should follow up with a qualified healthcare professional for any concerns regarding their health.
\n\n4. Personal Responsibility
\nYou acknowledge you are using our App voluntarily and that any choices, actions, and results now and in the future are solely your responsibility. We strongly encourage you to consult with a healthcare professional before making any health-related decisions based on the information provided by the App. We will not be liable to you or any other party for any decision made or action taken in reliance on the information given in the App.
\n\n5. External Links Disclaimer
\nThe App may contain (or you may be sent through the App) links to other websites or content belonging to or originating from third parties or links to websites and features in banners or other advertising. Such external links are not investigated, monitored, or checked for accuracy, adequacy, validity, reliability, availability, or completeness by us. We do not warrant, endorse, guarantee, or assume responsibility for the accuracy or reliability of any information offered by third-party websites linked through the App or any website or feature linked in any banner or other advertising. We will not be a party to or in any way be responsible for monitoring any transaction between you and third-party providers of products or services.</string>
<string name="string_home">Home</string>
<string name="string_you_have_no_moles_here">You have No Moles Here!</string>
<string name="string_mole_image">Mole Image</string>
<string name="string_location">Location</string>
<string name="string_side">Side</string>
<string name="string_name">Name</string>
<string name="string_body_part">Body Part</string>
<string name="string_prediction">Prediction</string>
<string name="string_benign_probability">Benign Probability</string>
<string name="string_malignant_probability">Malignant Probability</string>
<string name="string_logs">Logs</string>
<string name="string_add_log">Add Log</string>
<string name="string_new_prediction">New Prediction</string>
<string name="save_mole">Save Mole</string>
<string name="string_this_mole_has_no_logs">This Mole Has No Logs!</string>
<string-array name="links">
<item>https://www.nhs.uk/service-search/other-health-services/dermatology</item>
<item>https://www.britishskinfoundation.org.uk/news/find-your-nearest-dermatology-clinic</item>
<item>https://www.privatehealth.co.uk/dermatology/specialists/</item>
<item>https://www.sknclinics.co.uk/treatments-and-pricing/dermatology</item>
</string-array>
</resources>

View File

@ -0,0 +1,17 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Dermy_app" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="buttonIcon">@color/black</item>
</style>
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -0,0 +1,31 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:title="Account"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:icon="@drawable/user_icon">
<xyz.r0r5chach.dermy_app.AccountPreference
app:key="api_key"
app:layout="@layout/fragment_account" />
</PreferenceCategory>
<PreferenceCategory
android:title="Server"
app:icon="@drawable/server_icon"
app:allowDividerAbove="false"
app:allowDividerBelow="false">
<EditTextPreference
app:defaultValue="https://dermy.r0r-5chach.xyz/"
app:key="instance_url"
app:title="Instance URL" />
<Preference
app:key="disclaimer"
app:title="Disclaimer"
app:icon="@drawable/flag_icon"/>
</PreferenceCategory>
</PreferenceScreen>

View File

@ -0,0 +1,24 @@
package xyz.r0r5chach.dermy_app
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("xyz.r0r5chach.dermy_app", appContext.packageName)
}
}

View File

@ -0,0 +1,17 @@
package xyz.r0r5chach.dermy_app
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -0,0 +1,181 @@
package xyz.r0r5chach.dermy_app
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.widget.EditText
import android.widget.Toast
import androidx.fragment.app.findFragment
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.PreferenceViewHolder
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okhttp3.MediaType
import okhttp3.RequestBody
import org.json.JSONObject
import xyz.r0r5chach.dermy_app.api.DermyApi
import xyz.r0r5chach.dermy_app.fragments.SettingsFragment
@DelicateCoroutinesApi
class AccountPreference @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
): Preference(context, attrs, defStyleAttr) {
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
private lateinit var usernameField: EditText
private lateinit var passwordField: EditText
private lateinit var loggedIn: List<View>
private lateinit var loggedOut: List<View>
init {
widgetLayoutResource = R.layout.fragment_account
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
usernameField = holder.findViewById(R.id.username_field) as EditText
passwordField = holder.findViewById(R.id.password_field) as EditText
val loginButton = holder.findViewById(R.id.login_button)
val registerButton = holder.findViewById(R.id.register_button)
val logOutButton = holder.findViewById(R.id.logout_button)
val backupButton = holder.findViewById(R.id.backup_button)
val restoreButton = holder.findViewById(R.id.restore_button)
loggedIn = listOf(backupButton, restoreButton, logOutButton)
loggedOut = listOf(usernameField, passwordField, loginButton, registerButton)
checkSet()
loginButton.setOnClickListener {
val api = DermyApi(preferences.getString("instance_url", null)!!).getInstance()
val toast = Toast.makeText(context, "User does not exist", Toast.LENGTH_SHORT)
GlobalScope.launch {
try {
val saltRes = api.getSalt(usernameField.text.toString())
if (saltRes.containsKey("_salt")) {
val salt = saltRes["_salt"]
val hashedPassword = "abc" //TODO: Hash password
val body =
RequestBody.create(
MediaType.get("application/json"),JSONObject()
.put("username", usernameField.text.toString())
.put("_auth", JSONObject().put("_hash", hashedPassword)).toString())
val loginRes = api.login(body)
if (loginRes.containsKey("_api_key")) {
preferences.edit()
.putString("api_key", loginRes["_api_key"])
.apply()
toast.setText("Login successful")
toast.show()
back()
} else {
toast.show()
}
} else {
toast.show()
}
}
catch(e: Exception) {
Log.d("$e", "$e")
toast.setText("Login request has failed")
toast.show()
}
}
}
registerButton.setOnClickListener {
val api = DermyApi(preferences.getString("instance_url", null)!!).getInstance()
val toast = Toast.makeText(context, "User already exists", Toast.LENGTH_SHORT)
GlobalScope.launch {
try {
val salt = "1234" //TODO: Make salt
val hashedPassword = "abc" //TODO: hash password
val body =
RequestBody.create(MediaType.get("application/json"),
JSONObject()
.put("username", usernameField.text.toString())
.put("_auth", JSONObject().put("_salt", salt).put("_hash", hashedPassword))
.toString())
val registerRes = api.register(body)
if (registerRes.containsKey("_success")) {
toast.setText("Registration successful")
toast.show()
} else {
toast.show()
}
}
catch (e: Exception) {
Log.d("$e", "$e")
toast.setText("Registration request failed")
toast.show()
}
}
}
logOutButton.setOnClickListener {
val api = DermyApi(preferences.getString("instance_url", null)!!).getInstance()
val toast = Toast.makeText(context, "Your API key does not belong to any user", Toast.LENGTH_SHORT)
GlobalScope.launch {
try {
val key = preferences.getString("api_key", null)!!
preferences.edit().remove("api_key").apply()
val logoutRes = api.logout(key)
if (logoutRes.containsKey("_success")) {
toast.setText("Logout successful")
toast.show()
back()
} else {
toast.show()
}
}
catch (e: Exception) {
Log.d("$e", "$e")
toast.setText("Logout request failed")
toast.show()
}
}
}
backupButton.setOnClickListener {
//TODO: Handle Backup
Toast.makeText(context, "Backup Button Pressed", Toast.LENGTH_SHORT).show() //TODO: Change text to relevant message
}
restoreButton.setOnClickListener {
//TODO: Handle Restore
Toast.makeText(context, "Restore Button Pressed", Toast.LENGTH_SHORT).show() //TODO: Change text to relevant message
}
}
private fun checkSet() {
if(preferences.getString("api_key", null) != null) {
changeView(loggedIn, loggedOut)
}
else {
changeView(loggedOut, loggedIn)
}
}
private fun changeView(show: List<View>, hide: List<View>) {
show.forEach { it.visibility = View.VISIBLE }
hide.forEach { it.visibility = View.GONE }
}
private fun back() {
usernameField.findFragment<SettingsFragment>().parentFragmentManager.popBackStack()
}
}

View File

@ -0,0 +1,66 @@
package xyz.r0r5chach.dermy_app
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
class LinksAdapter(private val dataSet: Array<String>): RecyclerView.Adapter<LinksAdapter.ViewHolder>() {
class ViewHolder(view: View): RecyclerView.ViewHolder(view) {
private val title: TextView = view.findViewById(R.id.link_item_title)
fun bind(url: String) {
CoroutineScope(Dispatchers.Main).launch {
try {
updateItem(getInfo(url))
}
catch (_: Exception) {
updateItem(url)
}
}
}
private suspend fun getInfo(url: String): Document {
return withContext(Dispatchers.IO) {
Jsoup.connect(url).get()
}
}
private fun updateItem(result: Document) {
title.text = result.title()
title.setOnClickListener {
title.context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(result.location()))
)
}
}
private fun updateItem(result: String) {
title.text = result
title.setOnClickListener {
title.context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(result))
)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.links_list_item, parent, false)
return ViewHolder(view)
}
override fun getItemCount() = dataSet.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(dataSet[position])
}
}

View File

@ -0,0 +1,43 @@
package xyz.r0r5chach.dermy_app
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
import androidx.recyclerview.widget.RecyclerView
import xyz.r0r5chach.dermy_app.db.DermyDatabase
import xyz.r0r5chach.dermy_app.db.Entities
import xyz.r0r5chach.dermy_app.fragments.MoleFragment
import java.io.File
class LogsAdapter(private val dataSet: List<Entities.Entity?>?, private val filesDir: File): RecyclerView.Adapter<LogsAdapter.ViewHolder>() {
class ViewHolder(view: View): RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.mole_list_image)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.mole_list_item, parent, false)
return ViewHolder(view)
}
override fun getItemCount() = dataSet!!.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val log = (dataSet!![position]!! as Entities.LogEntry)
holder.textView.setOnClickListener {
//TODO: item displays date created
//TODO: On log item pressed: Popup -> show contents
}
holder.textView.text = log.dateCreated.toString()
}
}

View File

@ -0,0 +1,50 @@
package xyz.r0r5chach.dermy_app
import android.os.Bundle
import android.view.View
import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.add
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import androidx.room.Room
import com.badlogic.gdx.backends.android.AndroidFragmentApplication
import kotlinx.coroutines.DelicateCoroutinesApi
import xyz.r0r5chach.dermy_app.db.DermyDatabase
import xyz.r0r5chach.dermy_app.fragments.HomeFragment
class MainActivity: AppCompatActivity(R.layout.activity_main), AndroidFragmentApplication.Callbacks {
val db = DermyDatabase(this)
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val homeButton = findViewById<ImageButton>(R.id.home_button)
homeButton.visibility = View.GONE
homeButton.setOnClickListener {
goHome()
homeButton.visibility = View.GONE
}
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add(R.id.fragment_container, HomeFragment(db!!), null)
}
}
}
override fun exit() {}
@OptIn(DelicateCoroutinesApi::class)
fun goHome() {
supportFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container, HomeFragment(db!!), null)
addToBackStack(null)
}
}
}

View File

@ -0,0 +1,47 @@
package xyz.r0r5chach.dermy_app
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
import androidx.recyclerview.widget.RecyclerView
import xyz.r0r5chach.dermy_app.db.DermyDatabase
import xyz.r0r5chach.dermy_app.db.Entities
import xyz.r0r5chach.dermy_app.fragments.MoleFragment
import java.io.File
class MolesAdapter(private val dataSet: List<Entities.Entity?>?, private val filesDir: File): RecyclerView.Adapter<MolesAdapter.ViewHolder>() {
class ViewHolder(view: View): RecyclerView.ViewHolder(view) {
val imageView: ImageView = view.findViewById(R.id.mole_list_image)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.mole_list_item, parent, false)
return ViewHolder(view)
}
override fun getItemCount() = dataSet!!.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val mole = (dataSet!![position]!! as Entities.Mole)
val db = DermyDatabase(holder.imageView.context)
holder.imageView.setOnClickListener {
(holder.imageView.context as AppCompatActivity).supportFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container, MoleFragment(mole, PredictionMode.NONE, db), null)
addToBackStack(null)
}
}
holder.imageView.setImageBitmap(BitmapFactory.decodeFile(
File(filesDir, "${mole.id}.jpg").absolutePath
))
}
}

View File

@ -0,0 +1,7 @@
package xyz.r0r5chach.dermy_app
enum class PredictionMode {
NEW,
UPDATE,
NONE
}

View File

@ -0,0 +1,13 @@
package xyz.r0r5chach.dermy_app.api
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
class DermyApi(private val url: String) {
fun getInstance(): DermyApiService {
return Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(JacksonConverterFactory.create())
.build().create(DermyApiService::class.java)
}
}

View File

@ -0,0 +1,31 @@
package xyz.r0r5chach.dermy_app.api
import okhttp3.RequestBody
import org.json.JSONObject
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
interface DermyApiService {
@GET("/account/sign-in/{username}")
suspend fun getSalt(@Path("username") username: String): Map<String, String>
@POST("/account/sign-in")
suspend fun login(@Body loginForm: RequestBody): Map<String, String>
@POST("/account/sign-up")
suspend fun register(@Body loginForm: RequestBody): Map<String,String>
@GET("/account/sign-out/{api}")
suspend fun logout(@Path("api") apiKey: String): Map<String,String>
@GET("/account/restore/{api}")
suspend fun restore(@Path("api") apiKey: String): Map<String,String>
@POST("/account/backup/{api}")
suspend fun backup(@Path("api") apiKey: String, @Body backup: RequestBody): Map<String, String>
@GET("/predict/{api}")
suspend fun predict(@Path("api") apiKey: String, @Body image: RequestBody): Map<String, String>
}

View File

@ -0,0 +1,8 @@
package xyz.r0r5chach.dermy_app.api
class Prediction(var benign: Float, val malignant: Float) {
constructor(string: String): this(
string.split("/")[0].toFloat(),
string.split("/")[1].toFloat()
)
}

View File

@ -0,0 +1,9 @@
package xyz.r0r5chach.dermy_app.db
enum class DBProcess {
CREATE,
READ,
READ_ALL,
UPDATE,
DELETE
}

View File

@ -0,0 +1,37 @@
package xyz.r0r5chach.dermy_app.db
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class DermyDatabase(context: Context?) :
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
override fun onCreate(sqLiteDatabase: SQLiteDatabase) {
sqLiteDatabase.execSQL("CREATE TABLE ${Tables.MOLES} (" +
"id TEXT PRIMARY KEY," +
"userId TEXT," +
"dateCreated TEXT," +
"location TEXT," +
"benign TEXT," +
"malignant TEXT)")
sqLiteDatabase.execSQL("CREATE TABLE ${Tables.LOGS} (" +
"id TEXT PRIMARY KEY," +
"moleId TEXT," +
"dateCreated TEXT," +
"contents TEXT)")
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
for(table in Tables.values()) {
db!!.execSQL("DROP TABLE IF EXISTS $table")
}
onCreate(db!!)
}
companion object {
private const val DB_VERSION = 1
private const val DB_NAME = "dermy"
}
}

View File

@ -0,0 +1,23 @@
package xyz.r0r5chach.dermy_app.db
import xyz.r0r5chach.dermy_app.api.Prediction
import xyz.r0r5chach.dermy_app.map.locations.Location
import java.time.Instant
class Entities {
interface Entity
data class LogEntry(
val id: String,
val moleId: String,
val dateCreated: Instant,
val contents: String
): Entity
data class Mole(
val id: String?,
val userId: String?,
val dateCreated: Instant?,
val location: Location?,
var prediction: Prediction?
): Entity
}

View File

@ -0,0 +1,6 @@
package xyz.r0r5chach.dermy_app.db
enum class Tables {
MOLES,
LOGS
}

View File

@ -0,0 +1,127 @@
package xyz.r0r5chach.dermy_app.fragments
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.OutputFileOptions
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import com.google.common.util.concurrent.ListenableFuture
import org.bson.types.ObjectId
import xyz.r0r5chach.dermy_app.PredictionMode
import xyz.r0r5chach.dermy_app.R
import xyz.r0r5chach.dermy_app.db.DermyDatabase
import xyz.r0r5chach.dermy_app.db.Entities
import xyz.r0r5chach.dermy_app.map.locations.Location
import java.io.File
import java.time.Instant
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class CameraFragment(val location: Location, val db: DermyDatabase, val mode: PredictionMode, var mole: Entities.Mole?) : Fragment(R.layout.fragment_camera) {
private lateinit var previewView: PreviewView
private lateinit var imageCapture: ImageCapture
private lateinit var cameraExecutor: ExecutorService
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) {isGranted: Boolean ->
if (isGranted) {
startCamera()
}
else {
Toast.makeText(context, "Camera permission is required", Toast.LENGTH_SHORT).show()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
previewView = requireView().findViewById(R.id.camera_preview)!!
if (context?.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
else {
startCamera()
}
previewView.setOnClickListener { takePhoto() }
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun startCamera() {
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
imageCapture = ImageCapture.Builder().build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
} catch (exc: Exception) {
exc.printStackTrace()
}
}, ContextCompat.getMainExecutor(requireContext()))
}
private fun takePhoto() {
val id = when(mole) {
null -> ObjectId().toString()
else -> mole!!.id
}
val photoFile = File(context?.filesDir, "$id.jpg")
val outputFileOptions: OutputFileOptions = OutputFileOptions.Builder(photoFile).build()
imageCapture.takePicture(
outputFileOptions,
ContextCompat.getMainExecutor(requireContext()),
object : ImageCapture.OnImageSavedCallback {
@SuppressLint("NewApi")
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
if (mole == null) {
mole = Entities.Mole(id, null, Instant.now(), location, null)
}
parentFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container, MoleFragment(mole!!, mode, db), null)
addToBackStack(null)
}
}
override fun onError(exception: ImageCaptureException) {
Toast.makeText(
requireContext(),
"Error saving photo: ${exception.message}",
Toast.LENGTH_SHORT
).show()
}
})
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
}

View File

@ -0,0 +1,62 @@
package xyz.r0r5chach.dermy_app.fragments
import android.os.Bundle
import android.view.View
import android.widget.ImageButton
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import kotlinx.coroutines.DelicateCoroutinesApi
import xyz.r0r5chach.dermy_app.R
import xyz.r0r5chach.dermy_app.db.DermyDatabase
import xyz.r0r5chach.dermy_app.map.MapFragment
import xyz.r0r5chach.dermy_app.map.Mode
@DelicateCoroutinesApi
class HomeFragment(val db: DermyDatabase) : Fragment(R.layout.fragment_home) {
private var buttons: IntArray = intArrayOf(R.id.add_mole_button, R.id.map_button, R.id.links_button, R.id.settings_button)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
for (button in buttons) {
view.findViewById<ImageButton>(button).setOnClickListener {
initButton(button)
}
}
}
private fun initButton(button: Int) {
requireActivity().findViewById<ImageButton>(R.id.home_button).visibility = View.VISIBLE
when (button) {
R.id.add_mole_button -> {
parentFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container, MapFragment(Mode.ADD, db), null)
addToBackStack(null)
}
}
R.id.map_button -> {
parentFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container, MapFragment(Mode.VIEW, db), null)
addToBackStack(null)
}
}
R.id.links_button -> {
parentFragmentManager.commit {
setReorderingAllowed(true)
replace<LinksFragment>(R.id.fragment_container, null)
addToBackStack(null)
}
}
R.id.settings_button -> {
parentFragmentManager.commit {
setReorderingAllowed(true)
replace<SettingsFragment>(R.id.fragment_container, null)
addToBackStack(null)
}
}
}
}
}

View File

@ -0,0 +1,20 @@
package xyz.r0r5chach.dermy_app.fragments
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import xyz.r0r5chach.dermy_app.LinksAdapter
import xyz.r0r5chach.dermy_app.R
class LinksFragment : Fragment(R.layout.fragment_links) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val list = view.findViewById<RecyclerView>(R.id.links_list)
val links = resources.getStringArray(R.array.links)
val adapter = LinksAdapter(links)
list.layoutManager = LinearLayoutManager(context)
list.adapter = adapter
}
}

View File

@ -0,0 +1,58 @@
package xyz.r0r5chach.dermy_app.fragments
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import xyz.r0r5chach.dermy_app.MolesAdapter
import xyz.r0r5chach.dermy_app.R
import xyz.r0r5chach.dermy_app.api.Prediction
import xyz.r0r5chach.dermy_app.db.Tables
import xyz.r0r5chach.dermy_app.db.DermyDatabase
import xyz.r0r5chach.dermy_app.db.Entities
import xyz.r0r5chach.dermy_app.map.locations.Location
import java.time.Instant
class LocationFragment(private val location: Location, val db: DermyDatabase): Fragment(R.layout.fragment_location) {
@RequiresApi(Build.VERSION_CODES.O)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val list = view.findViewById<RecyclerView>(R.id.moles_list)
val emptyList = view.findViewById<TextView>(R.id.location_empty_text)
val moles = mutableListOf<Entities.Mole>()
with(db.readableDatabase.query(Tables.MOLES.toString(), null, null, null, null, null, null)) {
while (moveToNext()) {
moles.add(
Entities.Mole(
getString(getColumnIndexOrThrow("id")),
getString(getColumnIndexOrThrow("userId")),
Instant.parse(getString(getColumnIndexOrThrow("dateCreated"))),
Location(getString(getColumnIndexOrThrow("location"))),
Prediction(getString(getColumnIndexOrThrow("prediction")))
)
)
}
close()
}
val adapter = MolesAdapter(moles, requireContext().filesDir)
list.layoutManager = GridLayoutManager(context, 3)
list.adapter = adapter
if (adapter.itemCount == 0) {
list.visibility = View.GONE
emptyList.visibility = View.VISIBLE
}
else {
list.visibility = View.VISIBLE
emptyList.visibility = View.GONE
}
val name = "${location.side.toString()} ${location.bodyPart.toString()}"
view.findViewById<TextView>(R.id.location_name_text).text = name
}
}

View File

@ -0,0 +1,142 @@
package xyz.r0r5chach.dermy_app.fragments
import android.content.SharedPreferences
import android.database.sqlite.SQLiteDatabase
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.fragment.app.Fragment
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okhttp3.MediaType
import okhttp3.RequestBody
import xyz.r0r5chach.dermy_app.LogsAdapter
import xyz.r0r5chach.dermy_app.MolesAdapter
import xyz.r0r5chach.dermy_app.PredictionMode
import xyz.r0r5chach.dermy_app.R
import xyz.r0r5chach.dermy_app.api.DermyApi
import xyz.r0r5chach.dermy_app.api.DermyApiService
import xyz.r0r5chach.dermy_app.api.Prediction
import xyz.r0r5chach.dermy_app.db.DBProcess
import xyz.r0r5chach.dermy_app.db.DermyDatabase
import xyz.r0r5chach.dermy_app.db.Entities
import xyz.r0r5chach.dermy_app.db.Tables
import xyz.r0r5chach.dermy_app.map.locations.Location
import java.io.ByteArrayOutputStream
import java.io.File
import java.time.Instant
class MoleFragment(private val mole: Entities.Mole, private val mode: PredictionMode, val db: DermyDatabase) : Fragment(R.layout.fragment_mole) {
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(DelicateCoroutinesApi::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val api = DermyApi(preferences.getString("instance_url", null)!!).getInstance()
val apiKey = preferences.getString("api_key", null)
val moleImage = view.findViewById<ImageView>(R.id.mole_image)
val image = File(requireContext().filesDir, "${mole.id}.jpg").absolutePath
val moleSide = view.findViewById<EditText>(R.id.side_value)
val moleBodyPart = view.findViewById<EditText>(R.id.body_part_value)
val benignPrediction = view.findViewById<EditText>(R.id.benign_prediction_value)
val malignantPrediction = view.findViewById<EditText>(R.id.malignant_prediction_value)
val list = view.findViewById<RecyclerView>(R.id.log_list)
val emptyList = view.findViewById<TextView>(R.id.log_empty_text)
val logs = mutableListOf<Entities.LogEntry>()
with(db.readableDatabase.query(Tables.MOLES.toString(), null, null, null, null, null, null)) {
while (moveToNext()) {
logs.add(
Entities.LogEntry(
getString(getColumnIndexOrThrow("id")),
getString(getColumnIndexOrThrow("moleId")),
Instant.parse(getString(getColumnIndexOrThrow("dateCreated"))),
getString(getColumnIndexOrThrow("contents"))
)
)
}
close()
}
val adapter = LogsAdapter(logs, requireContext().filesDir)
list.layoutManager = LinearLayoutManager(requireContext())
list.adapter = adapter
if (adapter.itemCount == 0) {
list.visibility = View.GONE
emptyList.visibility = View.VISIBLE
}
else {
list.visibility = View.VISIBLE
emptyList.visibility = View.GONE
}
val saveButton = view.findViewById<ImageButton>(R.id.mole_save_button)
if (mode == PredictionMode.NEW || mode == PredictionMode.UPDATE) {
if (mode == PredictionMode.NEW) {
saveButton.visibility = View.VISIBLE
}
else {
saveButton.visibility = View.GONE
}
val toast = Toast.makeText(context, "Error making prediction", Toast.LENGTH_SHORT)
GlobalScope.launch {
try {
val result = api.predict(
apiKey!!, RequestBody.create(
MediaType.get("application/octet-stream"),
image.toByteArray()
)
)
if (result.containsKey("benign_probability") && result.containsKey("malignant_probability")) {
mole.prediction = Prediction(
result["benign_probability"]!!.toFloat(),
result["malignant_probability"]!!.toFloat()
)
benignPrediction.setText((mole.prediction!!.benign * 100).toString())
malignantPrediction.setText((mole.prediction!!.malignant * 100).toString())
}
}
catch (e: Exception) {
toast.show()
}
}
}
else {
saveButton.visibility = View.GONE
}
moleImage.setImageBitmap(BitmapFactory.decodeFile(image))
moleSide.setText(mole.location?.side.toString())
moleBodyPart.setText(mole.location?.bodyPart.toString())
benignPrediction.setText((mole.prediction?.benign?.times(100)).toString())
malignantPrediction.setText((mole.prediction?.malignant?.times(100))toString())
//TODO: On new prediction pressed: CameraFragment -> take photo -> MoleFragment -> predict -> update db
//TODO: On save pressed: insert mole to db
//TODO: On new log pressed: Popup -> save input from popup as contents -> insert new Log
}
}

View File

@ -0,0 +1,31 @@
package xyz.r0r5chach.dermy_app.fragments
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import kotlinx.coroutines.DelicateCoroutinesApi
import xyz.r0r5chach.dermy_app.R
@DelicateCoroutinesApi
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
findPreference<Preference>("disclaimer")
?.setOnPreferenceClickListener {
val dialog = AlertDialog.Builder(requireContext())
dialog
.setMessage(R.string.disclaimer)
.setTitle("Disclaimer")
.setNeutralButton("Ok") { window, _ -> window.dismiss() }
.create().show()
true
}
}
//TODO: Backup data
//TODO: Recover Account
}

View File

@ -0,0 +1,114 @@
package xyz.r0r5chach.dermy_app.map
import com.badlogic.gdx.ApplicationListener
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.assets.AssetManager
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.GL20
import com.badlogic.gdx.graphics.PerspectiveCamera
import com.badlogic.gdx.graphics.g2d.BitmapFont
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.g3d.Model
import com.badlogic.gdx.graphics.g3d.ModelBatch
import com.badlogic.gdx.graphics.g3d.ModelInstance
import com.badlogic.gdx.math.Vector3
import com.badlogic.gdx.math.collision.BoundingBox
import xyz.r0r5chach.dermy_app.map.locations.Location
class BodyMap(val observer: (Location?) -> Unit, private val backgroundColor: Int): ApplicationListener {
private val environment: MapEnvironment = MapEnvironment()
private var bodyModel: Model? = null
private var modelBatch: ModelBatch? = null
private var spriteBatch: SpriteBatch? = null
private var mapInstance: ModelInstance? = null
private var cam: PerspectiveCamera? = null
private var camController: MapCameraController? = null
private var assetManager: AssetManager = AssetManager()
private var font: BitmapFont? = null
private fun initModel() {
bodyModel = assetManager.get(Gdx.files.internal("human.obj").path())
mapInstance = ModelInstance(bodyModel)
val modelCenter = Vector3()
mapInstance!!.calculateBoundingBox(BoundingBox()).getCenter(modelCenter)
mapInstance!!.transform.translate(-modelCenter.x, -modelCenter.y, -modelCenter.z)
}
private fun initCam() {
cam = PerspectiveCamera(67f, Gdx.graphics.width.toFloat(), Gdx.graphics.height.toFloat())
camController = MapCameraController(cam, null)
cam!!.position[0f, 5f] = 25f
cam!!.lookAt(0f, 0f, 0f)
cam!!.near = 1f
cam!!.far = 300f
cam!!.update()
Gdx.input.inputProcessor = camController
}
override fun create() {
assetManager.load(Gdx.files.internal("human.obj").path(), Model::class.java)
initCam()
modelBatch = ModelBatch()
spriteBatch = SpriteBatch()
font = BitmapFont()
font!!.color = Color.WHITE
font!!.data.scale(5f)
}
override fun render() {
camController!!.update()
assetManager.update()
Gdx.gl.glClearColor(
((backgroundColor shr 0) and 0xFF) / 255f,
((backgroundColor shr 8) and 0xFF) / 255f,
((backgroundColor shr 16) and 0xFF) / 255f,
((backgroundColor shr 24) and 0xFF) / 255f)
Gdx.gl.glViewport(0, 0, Gdx.graphics.width, Gdx.graphics.height)
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT or GL20.GL_DEPTH_BUFFER_BIT)
if (assetManager.isFinished) {
initModel()
camController!!.modelInstance = mapInstance
camController!!.addObserver { newValue -> observer(newValue) }
modelBatch!!.begin(cam)
modelBatch!!.render(mapInstance!!, environment)
modelBatch!!.end()
}
else {
spriteBatch!!.begin()
font!!.draw(spriteBatch, "Map Loading...", (Gdx.graphics.width.toFloat() - font!!.region.regionWidth) / 3, (Gdx.graphics.height.toFloat() - font!!.region.regionHeight) / 1.75f)
spriteBatch!!.end()
}
}
override fun resize(width: Int, height: Int) {
}
override fun pause() {
}
override fun resume() {
}
override fun dispose() {
if (modelBatch != null) {
modelBatch!!.dispose()
}
if (bodyModel != null) {
bodyModel!!.dispose()
}
font!!.dispose()
spriteBatch!!.dispose()
}
}

View File

@ -0,0 +1,84 @@
package xyz.r0r5chach.dermy_app.map
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.PerspectiveCamera
import com.badlogic.gdx.graphics.g3d.ModelInstance
import com.badlogic.gdx.graphics.g3d.model.MeshPart
import com.badlogic.gdx.graphics.g3d.utils.CameraInputController
import com.badlogic.gdx.math.Intersector
import com.badlogic.gdx.math.Vector3
import xyz.r0r5chach.dermy_app.map.locations.BodyPart
import xyz.r0r5chach.dermy_app.map.locations.Location
import xyz.r0r5chach.dermy_app.map.locations.Side
import kotlin.math.abs
import kotlin.properties.Delegates
class MapCameraController(
camera: PerspectiveCamera?,
var modelInstance: ModelInstance?
) : CameraInputController(camera) {
private var initialX: Int = 0
private var initialY: Int = 0
private var selectedLocation: Location by Delegates.observable(Location(Side.NONE, BodyPart.NONE)) { _, _, newValue ->
notifyObservers(newValue)
}
private val observers = mutableListOf<(Location) -> Unit>()
init {
this.pinchZoomFactor = 25f
}
fun addObserver(observer: (Location) -> Unit) {
observers.add(observer)
}
private fun notifyObservers(newValue: Location) {
for (observer in observers) {
observer(newValue)
}
}
override fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean {
initialX = screenX
initialY = screenY
return super.touchDown(screenX, screenY, pointer, button)
}
override fun touchUp(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean {
if (abs((initialX - screenX).toDouble()) < 10 && abs((initialY - screenY).toDouble()) < 10) {
handleClick()
}
return super.touchUp(screenX, screenY, pointer, button)
}
private fun handleClick() {
if (modelInstance != null) {
val selectionRay = camera.getPickRay(Gdx.input.x.toFloat(), Gdx.input.y.toFloat())
var closestDistance = Float.MAX_VALUE
var closestPart: MeshPart? = null
val meshes = modelInstance!!.model.meshParts
for (part in meshes) {
val bounds = part.mesh.calculateBoundingBox()
bounds.mul(modelInstance!!.transform)
val intersection = Vector3()
if (Intersector.intersectRayBounds(selectionRay, bounds, intersection)) {
val distance = selectionRay.origin.dst(intersection)
if (distance < closestDistance) {
closestDistance = distance
closestPart = part
}
}
}
val id = closestPart?.id
val side = when (id?.get(0)) {
'L' -> Side.LEFT
'R' -> Side.RIGHT
else -> Side.LEFT
}
val bodyPart = BodyPart.valueOf(id!!.substring(1).uppercase())
selectedLocation = Location(side, bodyPart)
}
}
}

View File

@ -0,0 +1,21 @@
package xyz.r0r5chach.dermy_app.map
import com.badlogic.gdx.graphics.g3d.Environment
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight
class MapEnvironment internal constructor() : Environment() {
init {
this.set(ambientLight)
this.add(rightLight)
this.add(leftLight)
}
companion object {
private val ambientLight = ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f)
private val rightLight: DirectionalLight =
DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f)
private val leftLight: DirectionalLight =
DirectionalLight().set(0.8f, 0.8f, 0.8f, 1f, 0.8f, 0.2f)
}
}

View File

@ -0,0 +1,55 @@
package xyz.r0r5chach.dermy_app.map
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration
import com.badlogic.gdx.backends.android.AndroidFragmentApplication
import xyz.r0r5chach.dermy_app.PredictionMode
import xyz.r0r5chach.dermy_app.R
import xyz.r0r5chach.dermy_app.db.DermyDatabase
import xyz.r0r5chach.dermy_app.fragments.CameraFragment
import xyz.r0r5chach.dermy_app.fragments.LocationFragment
class MapFragment(private val mapMode: Mode, val db: DermyDatabase) : AndroidFragmentApplication() {
private var map: BodyMap? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreate(savedInstanceState)
val backgroundColor = requireContext().getColor(R.color.cardview_dark_background)
map = BodyMap ({ newValue ->
if (newValue != null) {
parentFragmentManager.commit {
setReorderingAllowed(true)
when (mapMode) {
Mode.ADD -> replace(R.id.fragment_container, CameraFragment(newValue, db, PredictionMode.NEW, null), null)
Mode.VIEW -> replace(R.id.fragment_container, LocationFragment(newValue, db), null)
}
addToBackStack(null)
}
}
}, backgroundColor)
return initializeForView(map, initConfig())
}
private fun initConfig(): AndroidApplicationConfiguration {
val config = AndroidApplicationConfiguration()
config.useAccelerometer = false
config.useCompass = false
config.useGyroscope = false
config.useWakelock = false
config.useImmersiveMode = false
config.useRotationVectorSensor = false
return config
}
}

View File

@ -0,0 +1,6 @@
package xyz.r0r5chach.dermy_app.map
enum class Mode {
ADD,
VIEW
}

View File

@ -0,0 +1,37 @@
package xyz.r0r5chach.dermy_app.map.locations
enum class BodyPart {
ABDOMEN,
ANKLE,
ARMPIT,
B_ELBOW,
B_FOREARM,
B_HEAD,
B_KNEE,
B_UPPER_ARM,
BUTTOCK,
CALF,
CHEEK,
CHEST,
EAR,
EYE,
F_ELBOW,
F_FOREARM,
F_KNEE,
FOOT,
FOREHEAD,
F_UPPER_ARM,
GROIN,
HAMSTRING,
HAND,
HEEL,
LOWER_BACK,
MOUTH,
NECK,
SHIN,
SHOULDER,
THIGH,
TOP_HEAD,
UPPER_BACK,
NONE
}

View File

@ -0,0 +1,8 @@
package xyz.r0r5chach.dermy_app.map.locations
class Location(val side: Side?, val bodyPart: BodyPart?) {
constructor(string: String): this(
Side.valueOf(string.split("/")[0]),
BodyPart.valueOf(string.split("/")[1])
)
}

View File

@ -0,0 +1,7 @@
package xyz.r0r5chach.dermy_app.map.locations
enum class Side {
LEFT,
RIGHT,
NONE
}

BIN
assets/body.blend Normal file

Binary file not shown.

BIN
assets/body.blend1 Normal file

Binary file not shown.

12
assets/human.mtl Normal file
View File

@ -0,0 +1,12 @@
# Blender 4.1.1 MTL File: 'body.blend'
# www.blender.org
newmtl default
Ns 0.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 1

78032
assets/human.obj Normal file

File diff suppressed because it is too large Load Diff

107
build.gradle Normal file
View File

@ -0,0 +1,107 @@
buildscript {
ext.kotlinVersion = '1.8.0'
repositories {
mavenLocal()
mavenCentral()
gradlePluginPortal()
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:1.5.30-1.0.0"
}
}
allprojects {
apply plugin: "eclipse"
version = '1.0'
ext {
appName = "dermy-app"
gdxVersion = '1.12.0'
roboVMVersion = '2.3.20'
box2DLightsVersion = '1.5'
ashleyVersion = '1.7.4'
aiVersion = '1.8.2'
gdxControllersVersion = '2.2.1'
retrofitVersion = '2.11.0'
cameraxVersion = '1.3.4'
roomVersion = '2.6.1'
}
repositories {
mavenLocal()
mavenCentral()
google()
gradlePluginPortal()
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
maven { url "https://oss.sonatype.org/content/repositories/releases/" }
maven { url "https://jitpack.io" }
}
}
project(":android") {
apply plugin: "android"
apply plugin: "kotlin-android"
apply plugin: "com.google.devtools.ksp"
configurations { natives }
dependencies {
implementation project(":core")
api "com.badlogicgames.gdx:gdx:$gdxVersion"
api "com.badlogicgames.gdx:gdx-backend-android:$gdxVersion"
api "com.badlogicgames.gdx:gdx-platform:$gdxVersion"
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
//MongoDB
implementation "org.mongodb:bson:5.1.1"
//Retrofit2 & Jackson
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-jackson:$retrofitVersion"
//Jsoup
implementation "org.jsoup:jsoup:1.17.2"
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.test.ext:junit:1.2.1'
implementation 'androidx.test.espresso:espresso-core:3.6.1'
//Room
implementation "androidx.room:room-ktx:$roomVersion"
implementation "androidx.room:room-common:2.6.1"
implementation "androidx.room:room-runtime:$roomVersion"
ksp "androidx.room:room-compiler:$roomVersion"
//CameraX
implementation "androidx.camera:camera-core:$cameraxVersion"
implementation "androidx.camera:camera-view:$cameraxVersion"
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
implementation "androidx.camera:camera-camera2:$cameraxVersion"
}
}
project(":core") {
apply plugin: "kotlin"
dependencies {
api "com.badlogicgames.gdx:gdx:$gdxVersion"
api "com.badlogicgames.gdx:gdx-bullet:$gdxVersion"
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
}
}

8
gradle.properties Normal file
View File

@ -0,0 +1,8 @@
org.gradle.daemon=true
org.gradle.jvmargs=-Xms128m -Xmx1500m
org.gradle.configureondemand=false
android.useAndroidX=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
android.suppressUnsupportedCompileSdk=35

1
settings.gradle Normal file
View File

@ -0,0 +1 @@
include 'android', 'core'