17 Commits

Author SHA1 Message Date
95a77fc570 Pin a specific rmlui commit 2026-08-11 11:36:52 -05:00
906e5097dd completely reworked stylesheet to use DP measurments 2026-08-09 14:55:30 -05:00
6f6a92257a Attempting to style for mobile 2026-08-09 11:57:13 -05:00
5c5bca6b3a Merge branch 'android-testing' 2026-08-09 00:16:47 -05:00
a3c7d9cf75 Can now build to android apk
Need to fix:
- Remove default android header bar
- Fix rcss styling to work on small screens
2026-08-09 00:16:23 -05:00
4aa23d3670 creating android project files 2026-08-07 17:39:49 -05:00
415061f862 Merge branch 'android-testing' 2026-08-07 14:09:01 -05:00
fb3e6dc705 SDL conversion 2026-08-07 11:36:22 -05:00
37b38a0537 Merge branch 'theme-test' 2026-08-07 10:21:36 -05:00
26ca6d1cf9 Adjusting colors 2026-08-07 10:21:24 -05:00
44f5a39d0c Re-theme test 2026-08-07 09:36:12 -05:00
5531d83994 Added a confirmation dialog 2026-08-07 09:12:22 -05:00
d9a3e8914b starting work on a confirmation dialog 2026-08-06 23:59:57 -05:00
b3945d9d0e Adding hot reload + attempting to style stuff 2026-08-06 23:32:23 -05:00
62a6f49eba Adding an image library because i apparently need TGAs 2026-08-05 23:24:32 -05:00
466e0570d4 Merge branch 'refactor' 2026-08-05 22:49:10 -05:00
11fe8ca54d Refactored stuff into new files, added a test modal for adding critters 2026-08-05 22:48:44 -05:00
35 changed files with 11791 additions and 489 deletions

View File

@@ -8,43 +8,58 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
find_package(Freetype REQUIRED) find_package(Freetype REQUIRED)
find_package(glfw3 CONFIG REQUIRED)
find_package(Lua REQUIRED) find_package(Lua REQUIRED)
find_package(SDL3 CONFIG REQUIRED)
find_package(SDL3_image CONFIG REQUIRED)
find_package(SQLiteCpp REQUIRED)
find_package(toml11 CONFIG REQUIRED)
pkg_check_modules(LUNASVG REQUIRED IMPORTED_TARGET lunasvg) pkg_check_modules(LUNASVG REQUIRED IMPORTED_TARGET lunasvg)
add_library(lunasvg::lunasvg ALIAS PkgConfig::LUNASVG) add_library(lunasvg::lunasvg ALIAS PkgConfig::LUNASVG)
#pkg_check_modules(SQLITE3 REQUIRED IMPORTED_TARGET sqlite3)
#add_library(sqlite3::sqlite3 ALIAS PkgConfig::SQLITE3)
find_package(SQLiteCpp REQUIRED)
find_package(toml11 CONFIG REQUIRED)
find_package(RmlUi CONFIG REQUIRED) find_package(RmlUi CONFIG REQUIRED)
add_library(rmlui_backend STATIC
vendor/rmlui_backend/RmlUi_Backend_SDL_SDLrenderer.cpp
vendor/rmlui_backend/RmlUi_Platform_SDL.cpp
vendor/rmlui_backend/RmlUi_Renderer_SDL.cpp
)
target_include_directories(rmlui_backend PUBLIC
vendor/rmlui_backend
)
target_compile_definitions(rmlui_backend PUBLIC
RMLUI_SDL_VERSION_MAJOR=3
)
target_link_libraries(rmlui_backend PUBLIC
RmlUi::RmlUi
SDL3::SDL3
SDL3_image::SDL3_image
Freetype::Freetype
)
add_executable(${PROJECT_NAME} add_executable(${PROJECT_NAME}
src/main.cpp src/main.cpp
vendor/rmlui_backend/RmlUi_Backend_GLFW_GL3.cpp src/database.cpp
vendor/rmlui_backend/RmlUi_Platform_GLFW.cpp src/ui.cpp
vendor/rmlui_backend/RmlUi_Renderer_GL3.cpp src/image_utils.cpp
) )
target_include_directories(${PROJECT_NAME} PRIVATE target_include_directories(${PROJECT_NAME} PRIVATE
src src
vendor/rmlui_backend
${FREETYPE_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS}
) )
target_link_libraries(${PROJECT_NAME} PRIVATE target_link_libraries(${PROJECT_NAME} PRIVATE
rmlui_backend
RmlUi::RmlUi RmlUi::RmlUi
RmlUi::Debugger RmlUi::Debugger
RmlUi::Lua RmlUi::Lua
${LUA_LIBRARIES} ${LUA_LIBRARIES}
Freetype::Freetype Freetype::Freetype
lunasvg::lunasvg lunasvg::lunasvg
glfw
toml11::toml11 toml11::toml11
SQLiteCpp SQLiteCpp
) )
@@ -55,4 +70,3 @@ install(DIRECTORY assets DESTINATION bin)
target_compile_definitions(${PROJECT_NAME} PRIVATE target_compile_definitions(${PROJECT_NAME} PRIVATE
ASSETS_DIR="${CMAKE_SOURCE_DIR}/bin/assets" ASSETS_DIR="${CMAKE_SOURCE_DIR}/bin/assets"
) )

3
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.gradle
build
app/.cxx

138
android/app/CMakeLists.txt Normal file
View File

@@ -0,0 +1,138 @@
cmake_minimum_required(VERSION 3.22.1)
project(critterfolio_android LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(ExternalProject)
set(DEPS_INSTALL ${CMAKE_BINARY_DIR}/deps/install)
# ExternalProject_Add spawns a completely separate CMake invocation for each
# dependency, so none of AGP's Android toolchain settings carry over
# automatically - we have to forward them explicitly to every sub-build.
set(ANDROID_TOOLCHAIN_ARGS
-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
-DANDROID_ABI=${ANDROID_ABI}
-DANDROID_PLATFORM=${ANDROID_PLATFORM}
-DANDROID_STL=c++_shared
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL}
-DCMAKE_PREFIX_PATH=${DEPS_INSTALL}
-DBUILD_SHARED_LIBS=OFF
-DCMAKE_CXX_STANDARD=17
-DCMAKE_CXX_STANDARD_REQUIRED=ON
-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH
-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH
-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH
)
find_package(SDL3 REQUIRED CONFIG)
find_package(SDL3_image REQUIRED CONFIG)
ExternalProject_Add(freetype_ext
GIT_REPOSITORY https://github.com/freetype/freetype.git
GIT_TAG VER-2-13-3
CMAKE_ARGS ${ANDROID_TOOLCHAIN_ARGS}
-DFT_DISABLE_ZLIB=TRUE
-DFT_DISABLE_BZIP2=TRUE
-DFT_DISABLE_PNG=TRUE
-DFT_DISABLE_HARFBUZZ=TRUE
-DFT_DISABLE_BROTLI=TRUE
)
ExternalProject_Add(lunasvg_ext
GIT_REPOSITORY https://github.com/sammycage/lunasvg.git
GIT_TAG v3.2.1
CMAKE_ARGS ${ANDROID_TOOLCHAIN_ARGS}
DEPENDS freetype_ext
)
# Lua ships no official CMakeLists, and we don't want its standalone lua/luac
# CLI targets (those need readline). We compile just the library sources.
ExternalProject_Add(lua_ext
GIT_REPOSITORY https://github.com/lua/lua.git
GIT_TAG v5.4.8
CONFIGURE_COMMAND ""
BUILD_IN_SOURCE 1
BUILD_COMMAND sh -c "${CMAKE_C_COMPILER} --target=${CMAKE_C_COMPILER_TARGET} --sysroot=${CMAKE_SYSROOT} -c -O2 -fPIC -DLUA_USE_POSIX $(ls *.c | grep -v -e '^lua.c$' -e '^luac.c$') && ${CMAKE_AR} rcs liblua.a *.o"
INSTALL_COMMAND ${CMAKE_COMMAND} -E make_directory ${DEPS_INSTALL}/lib ${DEPS_INSTALL}/include
COMMAND ${CMAKE_COMMAND} -E copy liblua.a ${DEPS_INSTALL}/lib/
COMMAND ${CMAKE_COMMAND} -E copy lua.h lauxlib.h lualib.h luaconf.h ${DEPS_INSTALL}/include/
)
ExternalProject_Add(sqlitecpp_ext
GIT_REPOSITORY https://github.com/SRombauts/SQLiteCpp.git
GIT_TAG 3.3.3
CMAKE_ARGS ${ANDROID_TOOLCHAIN_ARGS}
-DSQLITECPP_INTERNAL_SQLITE=ON
-DSQLITECPP_RUN_CPPLINT=OFF
-DSQLITECPP_RUN_CPPCHECK=OFF
)
ExternalProject_Add(toml11_ext
GIT_REPOSITORY https://github.com/ToruNiina/toml11.git
GIT_TAG v3.8.1
CMAKE_ARGS ${ANDROID_TOOLCHAIN_ARGS}
)
ExternalProject_Add(rmlui_ext
GIT_REPOSITORY https://github.com/mikke89/RmlUi.git
GIT_TAG master
CMAKE_ARGS ${ANDROID_TOOLCHAIN_ARGS}
-DFreetype_ROOT=${DEPS_INSTALL}
-DRMLUI_LUA_BINDINGS=ON
-DRMLUI_LUA_BINDINGS_LIBRARY=lua
-DRMLUI_SVG_PLUGIN=ON
-DRMLUI_SAMPLES=OFF
-DBUILD_TESTING=OFF
DEPENDS freetype_ext lunasvg_ext lua_ext
)
set(PROJECT_SRC_DIR ${CMAKE_SOURCE_DIR}/../../src)
set(RMLUI_BACKEND_DIR ${CMAKE_SOURCE_DIR}/../../vendor/rmlui_backend)
add_library(critterfolio SHARED
${PROJECT_SRC_DIR}/main.cpp
${PROJECT_SRC_DIR}/database.cpp
${PROJECT_SRC_DIR}/ui.cpp
${PROJECT_SRC_DIR}/image_utils.cpp
${RMLUI_BACKEND_DIR}/RmlUi_Backend_SDL_SDLrenderer.cpp
${RMLUI_BACKEND_DIR}/RmlUi_Platform_SDL.cpp
${RMLUI_BACKEND_DIR}/RmlUi_Renderer_SDL.cpp
)
add_dependencies(critterfolio freetype_ext lunasvg_ext lua_ext sqlitecpp_ext toml11_ext rmlui_ext)
target_include_directories(critterfolio PRIVATE
${PROJECT_SRC_DIR}
${RMLUI_BACKEND_DIR}
${DEPS_INSTALL}/include
)
target_compile_definitions(critterfolio PRIVATE
RMLUI_SDL_VERSION_MAJOR=3
RMLUI_STATIC_LIB
)
# Linked by explicit path rather than find_package(), since find_package()
# runs during THIS configure pass - before any ExternalProject_Add step
# above has actually built or installed anything (those only run later,
# during the actual build). This is what the superbuild pattern is for.
target_link_directories(critterfolio PRIVATE ${DEPS_INSTALL}/lib)
target_link_libraries(critterfolio PRIVATE
SDL3::SDL3
SDL3_image::SDL3_image
rmlui
rmlui_debugger
rmlui_lua
lua
freetype
lunasvg
plutovg
SQLiteCpp
sqlite3
)
target_link_options(critterfolio PRIVATE "-Wl,-u,SDL_main")

View File

@@ -0,0 +1,67 @@
plugins {
id("com.android.application")
}
android {
namespace = "com.critterfolio.app"
compileSdk = 34
ndkVersion = "26.3.11579264"
defaultConfig {
applicationId = "com.critterfolio.app"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "0.1.0"
ndk {
abiFilters += listOf("arm64-v8a")
}
externalNativeBuild {
cmake {
arguments += "-DANDROID_STL=c++_shared"
cppFlags += "-std=c++17"
}
}
}
buildFeatures {
prefab = true
}
externalNativeBuild {
cmake {
path = file("CMakeLists.txt")
version = "3.22.1"
}
}
sourceSets {
getByName("main") {
assets.srcDirs("../../assets")
}
}
buildTypes {
release {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
dependencies {
implementation(files("libs/SDL3-3.4.14.aar"))
implementation(files("libs/SDL3_image-3.4.4.aar"))
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:label="Critterfolio"
android:hasCode="true">
<activity
android:name="com.critterfolio.app.MainActivity"
android:label="Critterfolio"
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="true"
android:screenOrientation="fullSensor"
android:theme="@android:style/Theme.Black.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,13 @@
package com.critterfolio.app;
import org.libsdl.app.SDLActivity;
public class MainActivity extends SDLActivity {
@Override
protected String[] getLibraries() {
return new String[] {
"SDL3",
"critterfolio"
};
}
}

3
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,3 @@
plugins {
id("com.android.application") version "8.5.2" apply false
}

47
android/buildlog.txt Normal file
View File

@@ -0,0 +1,47 @@
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:generateDebugResValues UP-TO-DATE
> Task :app:mapDebugSourceSetPaths UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:packageDebugResources UP-TO-DATE
> Task :app:parseDebugLocalResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:mergeDebugShaders UP-TO-DATE
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:compressDebugAssets UP-TO-DATE
> Task :app:desugarDebugFileDependencies UP-TO-DATE
> Task :app:dexBuilderDebug UP-TO-DATE
> Task :app:mergeDebugGlobalSynthetics UP-TO-DATE
> Task :app:processDebugJavaRes NO-SOURCE
> Task :app:mergeDebugJavaResource UP-TO-DATE
> Task :app:checkDebugDuplicateClasses UP-TO-DATE
> Task :app:mergeDebugStartupProfile UP-TO-DATE
> Task :app:mergeExtDexDebug UP-TO-DATE
> Task :app:mergeLibDexDebug UP-TO-DATE
> Task :app:mergeProjectDexDebug UP-TO-DATE
> Task :app:configureCMakeDebug[arm64-v8a]
> Task :app:buildCMakeDebug[arm64-v8a]
> Task :app:mergeDebugJniLibFolders UP-TO-DATE
> Task :app:validateSigningDebug UP-TO-DATE
> Task :app:writeDebugAppMetadata UP-TO-DATE
> Task :app:writeDebugSigningConfigVersions UP-TO-DATE
> Task :app:mergeDebugNativeLibs
> Task :app:stripDebugDebugSymbols
> Task :app:packageDebug
> Task :app:createDebugApkListingFileRedirect UP-TO-DATE
> Task :app:assembleDebug
BUILD SUCCESSFUL in 9s
37 actionable tasks: 5 executed, 32 up-to-date

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx2048m
android.useAndroidX=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
android/gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,17 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "critterfolio"
include(":app")

Binary file not shown.

93
assets/fonts/OFL.txt Normal file
View File

@@ -0,0 +1,93 @@
Copyright 2016 The Hubballi Project Authors (https://github.com/erinmclaughlin/Hubballi)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

BIN
assets/images/icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

55
assets/images/icon.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 269 KiB

BIN
assets/images/icon.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

Binary file not shown.

View File

@@ -2,18 +2,81 @@
<head> <head>
<title>Critterfolio</title> <title>Critterfolio</title>
<link type="text/rcss" href="styles/main.rcss" /> <link type="text/rcss" href="styles/main.rcss" />
<script>
</script>
</head> </head>
<body data-model="app_data"> <body data-model="app_data">
<h1>{{page_title}}</h1> <div id="header">
<div class="header-title-group">
<h1>Critterfolio</h1>
<!-- <span class="header-tag">Field Registry</span> -->
</div>
<button id="new-critter-btn" data-event-click="is_add_critter_dialog_open = true"><div>+</div></button>
</div>
<h2>Critter Roster</h2>
<!-- Critter Cards / Drawer Rows -->
<div class="critter-list"> <div class="critter-list">
<div data-for="entry : critters" class="critter-card"> <button data-for="entry : critters" class="critter-card">
<h3>#{{entry.key}} - {{entry.value.name}}</h3> <div class="critter-card-body">
<p><strong>Species:</strong> {{entry.value.species}}</p> <div class="thumb-frame">
<p><strong>Gender:</strong> {{entry.value.gender}}</p> <img class="critter-thumb" src="images/icon.tga" />
<p><strong>Notes:</strong> {{entry.value.notes}}</p> </div>
<div class="critter-info">
<div class="critter-card-header">
<h3 class="critter-name">{{entry.value.name}}</h3>
<span class="critter-id">#{{entry.key}}</span>
</div>
<div class="info-pills">
<span class="pill"><strong>Sp:</strong> {{entry.value.species}}</span>
<span class="pill"><strong>Sex:</strong> {{entry.value.gender}}</span>
</div>
</div>
</div>
<button class="btn-delete" data-event-click="delete_critter(entry.key)">X</button>
</button>
</div>
<!-- Modals -->
<div class="modal-overlay" data-visible="is_add_critter_dialog_open">
<div class="modal-content">
<div class="modal-header">
<h2>Add New Critter</h2>
</div>
<div class="modal-body">
<p class="error" data-visible="error_message != ''">{{error_message}}</p>
<div class="form-group"><label>Name:</label><input type="text" data-value="new_critter_form.name" /></div>
<div class="form-row">
<div class="form-group half"><label>Species:</label><input type="text" data-value="new_critter_form.species" /></div>
<div class="form-group half"><label>Gender (M/F):</label><input type="text" data-value="new_critter_form.gender" /></div>
</div>
<div class="form-row">
<div class="form-group half"><label>Mother ID (Optional):</label><input type="text" data-value="new_critter_form.mother_id" /></div>
<div class="form-group half"><label>Father ID (Optional):</label><input type="text" data-value="new_critter_form.father_id" /></div>
</div>
<div class="form-group"><label>Notes:</label><textarea data-value="new_critter_form.notes"></textarea></div>
</div>
<div class="modal-buttons">
<button class="btn-cancel" data-event-click="is_add_critter_dialog_open = false">Cancel</button>
<button class="btn-submit" data-event-click="submit_add_critter">Save</button>
</div>
</div>
</div>
<div class="modal-overlay" data-visible="is_confirmation_dialog_open">
<div class="modal-content modal-content-small">
<div class="modal-header"><h2>Confirmation</h2></div>
<div class="modal-body"><p class="dialog-text">{{confirmation_dialog_text}}</p></div>
<div class="modal-buttons">
<button class="btn-cancel" data-event-click="confirm_dialog_no">Cancel</button>
<button class="btn-submit" data-event-click="confirm_dialog_yes">Yes</button>
</div>
</div> </div>
</div> </div>
</body> </body>

View File

@@ -1,25 +1,33 @@
* {
box-sizing: border-box;
font-family: "Hubballi";
--background-color: #2b2d2f;
--foreground-color: #f0eae1;
--accent-green: #606c38;
--accent-bright-green: #7d8d4c;
--accent-gold: #dda15e;
--accent-clay: #bc6c25;
--card-bg: #383b3e;
--border-dark: #191a1b;
}
/* ========================================================================== /* ==========================================================================
RCSS User Agent Defaults (Plain HTML Style) Mobile-First Base Styles (scaled via dp)
========================================================================== */ ========================================================================== */
/* Core Application Window Foundation */
body { body {
display: block; display: block;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
font-family: "rmlui-debugger-font"; font-size: 12dp;
font-size: 16px; background-color: var(--background-color);
background-color: #ffffff; color: var(--foreground-color);
color: #000000; margin: 0dp;
margin: 0px; padding: 12dp;
padding: 0px;
} }
/* Generic Structural Layout Defaults */ /* Generic Structural Layout Defaults */
* {
box-sizing: border-box;
}
div, section, article, header, footer, main, nav, aside { div, section, article, header, footer, main, nav, aside {
display: block; display: block;
} }
@@ -31,238 +39,478 @@ span, em, strong, a {
/* Typographic Defaults */ /* Typographic Defaults */
p { p {
display: block; display: block;
margin: 1em 0px; margin: 0dp;
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
display: block; display: block;
font-weight: bold; font-weight: bold;
margin: 0.83em 0px; margin: 0dp;
} }
h1 { font-size: 2em; } h1 { font-size: 1.6em; }
h2 { font-size: 1.5em; } h2 { font-size: 1.5em; }
h3 { font-size: 1.17em; } h3 { font-size: 1.17em; }
h4 { font-size: 1em; }
h5 { font-size: 0.83em; }
h6 { font-size: 0.67em; }
em { em { font-style: italic; }
font-style: italic; strong { font-weight: bold; }
}
strong { /* Form Inputs & Interactive UI Components */
font-weight: bold;
}
a {
color: #0000ee;
text-decoration: underline;
}
a:hover {
color: #551a8b;
}
blockquote {
display: block;
margin: 1em 40px;
padding-left: 10px;
border-left: 2px #cccccc;
color: #555555;
}
code, pre {
font-family: "rmlui-debugger-font";
background-color: #606060;
color: #000000;
padding: 3px;
font-style: italic;
}
pre {
display: block;
white-space: pre;
margin: 1em 0px;
padding: 8px;
}
/* Lists */
ul, ol {
display: block;
margin: 1em 0px;
padding-left: 40px;
}
li {
display: block;
}
/* ==========================================================================
Tables
========================================================================== */
table {
display: table;
}
tr {
display: table-row;
}
td, th {
display: table-cell;
padding: 4px 8px;
text-align: left;
}
th {
font-weight: bold;
background-color: #eaeaea;
}
col {
display: table-column;
}
colgroup {
display: table-column-group;
}
thead, tbody, tfoot {
display: table-row-group;
}
/* ==========================================================================
Form Inputs & Interactive UI Components
========================================================================== */
button, input, select, textarea { button, input, select, textarea {
display: inline-block; display: inline-block;
font-family: "rmlui-debugger-font"; font-family: "Hubballi";
font-size: 14px; font-size: 14dp;
margin: 2px; margin: 0dp;
vertical-align: middle; vertical-align: middle;
} }
/* Traditional gray bevel button style */ button {
button, input[type="submit"], input[type="button"] { padding: 6dp 16dp;
padding: 4px 12px; background-color: var(--accent-green);
min-width: 40px; color: var(--foreground-color);
background-color: #e1e1e1; border: 3dp var(--border-dark);
color: #000000; border-radius: 6dp;
border: 1px #767676;
text-align: center; text-align: center;
font-weight: bold;
} }
button:hover, input[type="submit"]:hover, input[type="button"]:hover { button:hover {
background-color: #d0d0d0; background-color: var(--accent-bright-green);
border: 1px #4f4f4f;
} }
button:active, input[type="submit"]:active, input[type="button"]:active { button:active {
background-color: #b5b5b5; background-color: var(--accent-gold);
border: 1px #000000; color: var(--border-dark);
} }
/* Standard text field styling */ input[type="text"], textarea {
input[type="text"], input[type="password"], textarea { padding: 8dp 12dp;
padding: 4px; background-color: #212325;
background-color: #ffffff; color: var(--foreground-color);
color: #000000; border: 3dp var(--border-dark);
border: 1px #767676; border-radius: 6dp;
width: 100%;
} }
input[type="text"]:focus, input[type="password"]:focus, textarea:focus { input[type="text"]:focus, textarea:focus {
border: 1px #005aff; background-color: #2b2d2f;
border-color: var(--accent-bright-green);
} }
textarea { textarea {
display: block; display: block;
white-space: pre-wrap; height: 120dp;
width: 300px;
height: 100px;
} }
/* Select Box & Dropdowns */ /* App Header */
select { #header {
padding: 4px; display: flex;
background-color: #ffffff; flex-direction: row;
color: #000000; align-items: center;
border: 1px #767676; justify-content: space-between;
padding: 10dp 14dp;
background-color: var(--accent-green);
border: 3dp var(--border-dark);
border-radius: 8dp;
margin-bottom: 14dp;
}
.header-title-group {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 12dp;
}
#header h1 {
font-size: 1.6em;
color: var(--foreground-color);
letter-spacing: 1dp;
}
.header-tag {
font-size: 10dp;
font-weight: bold;
background-color: var(--border-dark);
color: var(--accent-gold);
padding: 2dp 10dp;
border-radius: 12dp;
}
#new-critter-btn {
height: 40dp;
width: 40dp;
border-radius: 8dp;
font-size: 20dp;
display: flex;
align-items: center;
justify-content: center;
border: 3dp var(--border-dark);
background-color: var(--accent-gold);
color: var(--border-dark);
padding: 0dp;
}
#new-critter-btn:hover {
background-color: var(--accent-bright-green);
color: var(--foreground-color);
}
/* Drawer List Styles */
.critter-list {
display: flex;
flex-direction: column;
gap: 8dp;
}
.critter-card {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 8dp 10dp;
background-color: var(--card-bg);
color: var(--foreground-color);
border: 3dp var(--border-dark);
border-left: 10dp var(--accent-green);
border-radius: 6dp;
text-align: left; text-align: left;
} }
/* ========================================================================== .critter-card:hover {
RmlUi Specific Elements (Tabsets, Progress Bars, etc.) background-color: #45484c;
========================================================================== */ border-left-color: var(--accent-bright-green);
tabset {
display: block;
} }
tabset tabs { .critter-card-body {
display: block;
border-bottom: 1px #cccccc;
}
/* Simple default layout style for built-in tabs */
tab {
display: inline-block;
padding: 6px 12px;
background-color: #f0f0f0;
border: 1px #cccccc;
margin-right: 2px;
}
tab:selected {
background-color: #ffffff;
border-bottom: 1px #ffffff; /* Overlap the bottom border line */
font-weight: bold;
}
panel {
display: block;
padding: 10px;
background-color: #ffffff;
}
/* Standard HTML5 / RmlUi Progress Bar Fallback */
progress {
display: inline-block;
width: 160px;
height: 20px;
background-color: #e6e6e6;
border: 1px #b0b0b0;
}
progress-value {
display: block;
height: 100%;
background-color: #0078d7; /* Classic Windows Blue fill */
}
/* ==========================================================================
Utility & Modern Layout Helpers
========================================================================== */
.flex {
display: flex; display: flex;
}
.flex-row {
flex-direction: row; flex-direction: row;
align-items: center;
gap: 10dp;
flex: 1;
} }
.flex-col { .thumb-frame {
background-color: var(--accent-green);
border: 2dp var(--border-dark);
border-radius: 4dp;
padding: 2dp;
}
.critter-thumb {
width: 40dp;
height: 40dp;
}
.critter-info {
display: flex;
flex-direction: column; flex-direction: column;
gap: 2dp;
flex: 1;
} }
.justify-center { .critter-card-header {
display: flex;
align-items: baseline;
gap: 10dp;
}
.critter-name {
font-size: 14dp;
margin: 0dp;
color: var(--foreground-color);
}
.critter-id {
font-size: 9dp;
font-weight: bold;
color: var(--accent-gold);
}
.info-pills {
display: flex;
flex-direction: row;
gap: 8dp;
}
.pill {
font-size: 10dp;
background-color: #212325;
color: var(--foreground-color);
border: 1dp var(--accent-green);
padding: 1dp 6dp;
border-radius: 4dp;
}
.btn-delete {
padding: 3dp 8dp;
font-size: 9dp;
font-weight: bold;
background-color: var(--accent-clay);
color: #ffffff;
border: 2dp var(--border-dark);
border-radius: 4dp;
}
.btn-delete:hover {
background-color: #a3561a;
}
/* Modals Layout */
.modal-overlay {
position: absolute;
top: 0dp;
left: 0dp;
width: 100%;
height: 100%;
background-color: #1f1f1fdd;
display: flex;
align-items: center;
justify-content: center; justify-content: center;
} }
.align-center { .modal-content {
align-items: center; background-color: var(--card-bg);
color: var(--foreground-color);
border: 4dp var(--border-dark);
border-radius: 10dp;
width: 94vw;
padding: 0dp;
display: flex;
flex-direction: column;
} }
.hidden { .modal-content-small {
display: none; width: 88vw;
} }
.modal-header {
background-color: var(--accent-green);
color: #ffffff;
padding: 10dp 16px;
border-bottom: 3dp var(--border-dark);
}
.modal-header h2 {
font-size: 16dp;
color: #ffffff;
}
.modal-body {
padding: 14dp 16px;
display: flex;
flex-direction: column;
gap: 10dp;
}
.form-group {
display: flex;
flex-direction: column;
gap: 4dp;
}
.form-row {
display: flex;
flex-direction: column;
gap: 10dp;
}
.form-group.half {
width: 100%;
}
.modal-content label {
font-size: 12dp;
font-weight: bold;
color: var(--foreground-color);
}
.error {
color: #ffffff;
font-weight: bold;
padding: 8dp;
border: 2dp var(--border-dark);
background-color: var(--accent-clay);
border-radius: 4dp;
}
.dialog-text {
font-size: 14dp;
color: var(--foreground-color);
padding: 10dp 0dp;
}
.modal-buttons {
padding: 12dp 16px;
background-color: #2b2d2f;
border-top: 3dp var(--border-dark);
display: flex;
flex-direction: row;
gap: 12dp;
justify-content: flex-end;
}
.modal-buttons button {
min-width: 90dp;
height: 38dp;
font-size: 13dp;
}
.btn-cancel {
background-color: #45484c;
color: var(--foreground-color);
}
.btn-cancel:hover {
background-color: #565a5f;
}
.btn-submit {
background-color: var(--accent-green);
color: #ffffff;
}
.btn-submit:hover {
background-color: var(--accent-bright-green);
}
/* Extra Narrow Viewports (< 380dp) */
@media (max-width: 380dp) {
#header h1 {
font-size: 1.3em;
}
.critter-name {
font-size: 12dp;
}
.info-pills {
flex-direction: column;
gap: 4dp;
}
}
/* ==========================================================================
Desktop / Larger Screens Media Query (Min-Width >= 600dp)
========================================================================== */
@media (min-width: 600dp) {
body {
padding: 24dp;
font-size: 15dp;
}
textarea {
height: 150dp;
}
#header {
padding: 12dp 20px;
margin-bottom: 24dp;
}
.header-title-group {
gap: 16dp;
}
#header h1 {
font-size: 2.5em;
}
.header-tag {
font-size: 12dp;
}
#new-critter-btn {
height: 48dp;
width: 48dp;
font-size: 28dp;
}
.critter-list {
gap: 10dp;
}
.critter-card {
padding: 8dp 14dp;
}
.critter-card-body {
gap: 14dp;
}
.critter-thumb {
width: 50dp;
height: 50dp;
}
.critter-card-header {
gap: 12dp;
}
.critter-name {
font-size: 18dp;
}
.critter-id {
font-size: 11dp;
}
.pill {
font-size: 12dp;
padding: 1dp 8dp;
}
.btn-delete {
padding: 4dp 10dp;
font-size: 11dp;
}
.modal-content {
width: 600dp;
}
.modal-content-small {
width: 450dp;
}
.modal-header {
padding: 12dp 20px;
}
.modal-header h2 {
font-size: 20dp;
}
.modal-body {
padding: 18dp 20px;
gap: 12px;
}
.form-row {
flex-direction: row;
gap: 12dp;
}
.form-group.half {
width: 50%;
}
.modal-content label {
font-size: 14dp;
}
.dialog-text {
font-size: 18dp;
}
.modal-buttons {
padding: 14px 20px;
}
.modal-buttons button {
min-width: 120dp;
height: 42dp;
font-size: 16dp;
}
}

View File

@@ -9,16 +9,23 @@
outputs = { self, nixpkgs, utils }: outputs = { self, nixpkgs, utils }:
utils.lib.eachDefaultSystem (system: utils.lib.eachDefaultSystem (system:
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs {
inherit system;
config = {
allowUnfree = true;
android_sdk.accept_license = true;
};
};
rmlui = pkgs.stdenv.mkDerivation rec { rmlui = pkgs.stdenv.mkDerivation rec {
pname = "rmlui"; pname = "rmlui";
version = "master"; version = "6.2-unstable-2026-8-7";
src = pkgs.fetchFromGitHub { src = pkgs.fetchFromGitHub {
owner = "mikke89"; owner = "mikke89";
repo = "RmlUi"; repo = "RmlUi";
rev = "master"; rev = "9cbb5440de3e86e7031544c26116fe7b814028b9";
hash = "sha256-vfFtjNHw7coXWrGgtcFyuIsu5rctW5dBJoOt8MTZwDU="; hash = "sha256-5vyrVJhmNE/msJuVSESsKhiW9Pc+peA0NTYJDVoRanA=";
}; };
nativeBuildInputs = [ nativeBuildInputs = [
@@ -52,14 +59,9 @@
]; ];
runtimeDeps = with pkgs; [ runtimeDeps = with pkgs; [
raylib sdl3
glfw sdl3-image
libGL libGL
libX11
libXcursor
libXrandr
libXinerama
libXi
wayland wayland
libxkbcommon libxkbcommon
rmlui rmlui
@@ -70,18 +72,45 @@
sqlitecpp sqlitecpp
toml11 toml11
]; ];
ndkVersion = "26.3.11579264";
androidComposition = pkgs.androidenv.composeAndroidPackages {
platformVersions = [ "34" ];
buildToolsVersions = [ "34.0.0" ];
includeNDK = true;
ndkVersions = [ ndkVersion ];
cmakeVersions = [ "3.22.1" ];
abiVersions = [ "arm64-v8a" ];
includeEmulator = false;
};
androidSdkRoot = "${androidComposition.androidsdk}/libexec/android-sdk";
androidNdkRoot = "${androidSdkRoot}/ndk/${ndkVersion}";
androidDeps = [
androidComposition.androidsdk
pkgs.gradle
pkgs.jdk17
pkgs.ninja
];
in in
{ {
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
nativeBuildInputs = buildDeps; nativeBuildInputs = buildDeps ++ androidDeps;
buildInputs = runtimeDeps; buildInputs = runtimeDeps;
shellHook = '' shellHook = ''
export CP_PATH="${pkgs.raylib}/include:${pkgs.glfw}/include:${rmlui}/include" export CP_PATH="${rmlui}/include"
export LIBRARY_PATH="${pkgs.raylib}/lib:${pkgs.glfw}/lib:${pkgs.libGL}/lib:${rmlui}/lib" export LIBRARY_PATH="${pkgs.libGL}/lib:${rmlui}/lib"
export LD_LIBRARY_PATH="$LIBRARY_PATH:$LD_LIBRARY_PATH" export LD_LIBRARY_PATH="$LIBRARY_PATH:$LD_LIBRARY_PATH"
echo "Env ready" export ANDROID_HOME="${androidSdkRoot}"
export ANDROID_SDK_ROOT="${androidSdkRoot}"
export ANDROID_NDK_HOME="${androidNdkRoot}"
export ANDROID_NDK_ROOT="${androidNdkRoot}"
echo "Env ready (desktop + Android NDK ${ndkVersion}, arm64-v8a)"
''; '';
}; };

16
src/asset_io.hpp Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
inline SDL_IOStream *OpenAssetFile(const std::string &virtual_path) {
std::string resolved = virtual_path;
#if defined(__ANDROID__)
if (resolved.rfind("./assets/", 0) == 0) {
resolved = resolved.substr(9);
} else if (resolved.rfind("assets/", 0) == 0) {
resolved = resolved.substr(7);
}
#endif
return SDL_IOFromFile(resolved.c_str(), "rb");
}

191
src/database.cpp Normal file
View File

@@ -0,0 +1,191 @@
#include "database.hpp"
#include "asset_io.hpp"
#include <SQLiteCpp/Database.h>
#include <SDL3/SDL.h>
#include <iostream>
#include <vector>
std::string get_writable_db_path() {
char *pref = SDL_GetPrefPath("Critterfolio", "CritterFolio");
std::string path = pref ? pref : "./";
if (pref) {
SDL_free(pref);
}
path += "local.db";
return path;
}
bool ensure_db_copied() {
std::string dest_path = get_writable_db_path();
SDL_IOStream *check = SDL_IOFromFile(dest_path.c_str(), "rb");
if (check) {
SDL_CloseIO(check);
return true;
}
SDL_IOStream *src = OpenAssetFile("./assets/local.db");
if (!src) {
std::cout << "[ERROR] Could not open seed database in assets: " << SDL_GetError() << "\n";
return false;
}
Sint64 size = SDL_GetIOSize(src);
if (size < 0) {
std::cout << "[ERROR] Could not determine seed database size.\n";
SDL_CloseIO(src);
return false;
}
std::vector<Uint8> buffer(static_cast<size_t>(size));
if (SDL_ReadIO(src, buffer.data(), buffer.size()) != buffer.size()) {
std::cout << "[ERROR] Failed reading seed database.\n";
SDL_CloseIO(src);
return false;
}
SDL_CloseIO(src);
SDL_IOStream *dst = SDL_IOFromFile(dest_path.c_str(), "wb");
if (!dst) {
std::cout << "[ERROR] Could not open destination database for writing: " << SDL_GetError() << "\n";
return false;
}
bool ok = SDL_WriteIO(dst, buffer.data(), buffer.size()) == buffer.size();
SDL_CloseIO(dst);
if (!ok) {
std::cout << "[ERROR] Failed writing copied database.\n";
}
return ok;
}
bool db_get_all_critters(std::map<int, Critter> &critters) {
try {
SQLite::Database db(get_writable_db_path(), SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
SQLite::Statement q(db, "SELECT * FROM critters");
critters.clear();
while (q.executeStep()) {
int id = q.getColumn(0);
critters.try_emplace(
id,
id,
q.getColumn(1).getText(),
q.getColumn(2).getText(),
q.getColumn(3).getInt(),
q.getColumn(4).getInt(),
q.getColumn(5).getText(),
q.getColumn(6).getText()
);
}
}
catch (const std::exception &e) {
std::cout << "[ERROR] DB exception: " << e.what() << std::endl;
return false;
}
return true;
}
bool db_add_critter(const Critter &c, int &out_id) {
try {
SQLite::Database db(get_writable_db_path(), SQLite::OPEN_READWRITE);
SQLite::Statement q(db, "INSERT INTO critters (name, gender, mother_id, father_id, notes, species) VALUES (?, ?, ?, ?, ?, ?)");
q.bind(1, c.name);
q.bind(2, c.gender);
q.bind(3, c.mother_id);
q.bind(4, c.father_id);
q.bind(5, c.notes);
q.bind(6, c.species);
q.exec();
out_id = static_cast<int>(db.getLastInsertRowid());
return true;
}
catch (const std::exception &e) {
std::cout << "[ERROR] DB add exception: " << e.what() << std::endl;
return false;
}
}
bool db_delete_critter(int id) {
try {
SQLite::Database db(get_writable_db_path(), SQLite::OPEN_READWRITE);
SQLite::Statement q(db, "DELETE FROM critters WHERE id = ?");
q.bind(1, id);
q.exec();
return true;
}
catch (const std::exception &e) {
std::cout << "[ERROR] DB delete exception: " << e.what() << std::endl;
return false;
}
}
void db_print_all_critters(const std::map<int, Critter> &critters) {
std::cout << "Current loaded critters:";
for (const auto &[key, c] : critters) {
std::cout << "\nCritter " << key << "\n";
std::cout << "ID: " << c.id << std::endl;
std::cout << "Name: " << c.name << std::endl;
std::cout << "Gender: " << c.gender << std::endl;
std::cout << "Mother ID: " << c.mother_id << std::endl;
std::cout << "Father ID: " << c.father_id << std::endl;
std::cout << "Notes: " << c.notes << std::endl;
std::cout << "Species: " << c.species << std::endl;
}
std::cout << "\n";
}
bool validate_critter(const Critter &c, const std::map<int, Critter> &all_critters, std::string &out_error) {
if (c.mother_id != 0 && c.father_id != 0 && c.mother_id == c.father_id) {
out_error = "Mother ID and Father ID cannot be the same.";
return false;
}
if (c.mother_id != 0) {
auto it = all_critters.find(c.mother_id);
if (it == all_critters.end()) {
out_error = "Mother ID " + std::to_string(c.mother_id) + " does not exist.";
return false;
}
const Critter &mother = it->second;
if (mother.gender != "F" && mother.gender != "Female") {
out_error = "Mother must have female gender ('F' or 'Female').";
return false;
}
if (mother.species != c.species) {
out_error = "Mother species ('" + mother.species + "') must match critter species ('" + c.species + "').";
return false;
}
}
if (c.father_id != 0) {
auto it = all_critters.find(c.father_id);
if (it == all_critters.end()) {
out_error = "Father ID " + std::to_string(c.father_id) + " does not exist.";
return false;
}
const Critter &father = it->second;
if (father.gender != "M" && father.gender != "Male") {
out_error = "Father must have male gender ('M' or 'Male').";
return false;
}
if (father.species != c.species) {
out_error = "Father species ('" + father.species + "') must match critter species ('" + c.species + "').";
return false;
}
}
return true;
}

View File

@@ -1,104 +1,13 @@
#pragma once
#include "critter.hpp" #include "critter.hpp"
#include <SQLiteCpp/Database.h>
#include <iostream>
#include <map> #include <map>
#include <string>
inline static bool db_get_all_critters(std::map<int, Critter> &critters) { std::string get_writable_db_path();
try { bool ensure_db_copied();
SQLite::Database db("./assets/local.db", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); bool db_get_all_critters(std::map<int, Critter> &critters);
SQLite::Statement q(db, "SELECT * FROM critters"); bool db_add_critter(const Critter &c, int &out_id);
bool db_delete_critter(int id);
critters.clear(); void db_print_all_critters(const std::map<int, Critter> &critters);
bool validate_critter(const Critter &c, const std::map<int, Critter> &all_critters, std::string &out_error);
while (q.executeStep()) {
int id = q.getColumn(0);
critters.try_emplace(
id,
id,
q.getColumn(1).getText(),
q.getColumn(2).getText(),
q.getColumn(3).getInt(),
q.getColumn(4).getInt(),
q.getColumn(5).getText(),
q.getColumn(6).getText()
);
}
}
catch (const std::exception &e) {
std::cout << "[ERROR] DB exception: " << e.what() << std::endl;
return false;
}
return true;
}
inline static void db_print_all_critters(std::map<int, Critter> &critters) {
std::cout << "Current loaded critters:";
for (auto c : critters) {
std::cout << "\nCritter " << c.first << "\n";
std::cout << "ID: " << c.second.id << std::endl;
std::cout << "Name:" << c.second.name << std::endl;
std::cout << "Gender: " << c.second.gender << std::endl;
std::cout << "Mother ID: " << c.second.mother_id << std::endl;
std::cout << "Father ID: " << c.second.father_id << std::endl;
std::cout << "Notes: " << c.second.notes << std::endl;
std::cout << "Species: " << c.second.species << std::endl;
}
std::cout << "\n";
}
inline static bool validate_critter(const Critter &c, std::map<int, Critter> &all_critters, std::string &out_error) {
if (c.mother_id != 0 && c.father_id != 0 && c.mother_id == c.father_id) {
out_error = "Mother ID and Father ID cannot be the same.";
return false;
}
// Mother Validation
if (c.mother_id != 0) {
auto it = all_critters.find(c.mother_id);
if (it == all_critters.end()) {
out_error = "Mother ID " + std::to_string(c.mother_id) + " does not exist.";
return false;
}
const Critter& mother = it->second;
if (mother.gender != "F" && mother.gender != "Female") {
out_error = "Mother must have female gender ('F' or 'Female').";
return false;
}
if (mother.species != c.species) {
out_error = "Mother species ('" + mother.species + "') must match critter species ('" + c.species + "').";
return false;
}
}
// Father Validation
if (c.father_id != 0) {
auto it = all_critters.find(c.father_id);
if (it == all_critters.end()) {
out_error = "Father ID " + std::to_string(c.father_id) + " does not exist.";
return false;
}
const Critter& father = it->second;
if (father.gender != "M" && father.gender != "Male") {
out_error = "Father must have male gender ('M' or 'Male').";
return false;
}
if (father.species != c.species) {
out_error = "Father species ('" + father.species + "') must match critter species ('" + c.species + "').";
return false;
}
}
return true;
}
inline static bool db_try_add_critter() {
}

38
src/image_utils.cpp Normal file
View File

@@ -0,0 +1,38 @@
// image_utils.cpp
#define STB_IMAGE_IMPLEMENTATION
#include "../vendor/stb/stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "../vendor/stb/stb_image_write.h"
#include <iostream>
#include <string>
bool ConvertToThumbnailTGA(const std::string& input_path, const std::string& output_tga_path) {
int width, height, channels;
stbi_uc* data = stbi_load(input_path.c_str(), &width, &height, &channels, 4);
if (!data) {
std::cout << "[ERROR] Failed to decode image: " << input_path << std::endl;
return false;
}
int result = stbi_write_tga(output_tga_path.c_str(), width, height, 4, data);
stbi_image_free(data);
if (!result) {
std::cout << "[ERROR] Failed to save TGA thumbnail to: " << output_tga_path << std::endl;
return false;
}
return true;
}
void ProcessUserThumbnail(int critter_id, const std::string& user_selected_file_path) {
std::string target_tga_path = "./data/thumbnails/" + std::to_string(critter_id) + ".tga";
if (ConvertToThumbnailTGA(user_selected_file_path, target_tga_path)) {
std::cout << "[INFO] Thumbnail successfully generated for Critter #" << critter_id << std::endl;
}
}

View File

@@ -1,162 +1,210 @@
#include <RmlUi/Config/Config.h>
#include <RmlUi/Core/Core.h>
#include <RmlUi/Core/DataModelHandle.h>
#include <RmlUi/Core/ElementDocument.h>
#include <RmlUi/Core/Math.h>
#include <RmlUi/Lua/Interpreter.h>
#include <RmlUi/Lua/Lua.h>
#include <SQLiteCpp/Database.h>
#include <iostream>
#include <RmlUi/Core.h>
#include <RmlUi_Backend.h>
#include <RmlUi/Lua/Lua.h>
#include <RmlUi/Core/Context.h>
#include <lua.h>
#include <RmlUi/Debugger/Debugger.h>
#include "critter.hpp" #include "critter.hpp"
#include <SQLiteCpp/SQLiteCpp.h> #include "database.hpp"
#include "ui.hpp"
#include "sdl_file_interface.hpp"
#include "../vendor/stb/stb_image.h"
#include "../vendor/stb/stb_image_write.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <RmlUi/Core.h>
#include <RmlUi/Core/Core.h>
#include <RmlUi/Debugger/Debugger.h>
#include <RmlUi/Lua/Lua.h>
#include <RmlUi_Backend.h>
#include <iostream>
#include <map> #include <map>
#include <vector> #include <vector>
#include "database.hpp"
static bool is_initalized = false; static bool is_initalized = false;
static bool is_running = false; static bool is_running = false;
static Rml::Context *ctx; static Rml::Context *ctx = nullptr;
static std::map<int, Critter> critters = {}; static std::map<int, Critter> critters;
static std::vector<CritterEntry> critter_list = {}; static std::vector<CritterEntry> critter_list;
static Rml::ElementDocument *doc;
static AppData app_data;
static SDLFileInterface file_interface;
struct AppData { void shutdown() {
Rml::String page_title = "Main";
} app_data;
void SyncMapToList() {
critter_list.clear();
for (const auto &[id, critter] : critters) {
critter_list.push_back({id, critter});
}
}
void Shutdown() {
Rml::Shutdown(); Rml::Shutdown();
Backend::Shutdown(); Backend::Shutdown();
SDL_Quit();
} }
bool SetupDataBinding(Rml::Context *context, Rml::DataModelHandle &model_handle) { bool initialize() {
Rml::DataModelConstructor dmc = ctx->CreateDataModel("app_data"); if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
if (!dmc) { std::cout << "[ERROR] Failed to initialize SDL3: " << SDL_GetError() << "\n";
std::cout << "[ERROR] could not create 'app_data' data binding.";
return false; return false;
} }
if (auto handle = dmc.RegisterStruct<Critter>()) {
handle.RegisterMember("id", &Critter::id);
handle.RegisterMember("name", &Critter::name);
handle.RegisterMember("gender", &Critter::gender);
handle.RegisterMember("species", &Critter::species);
handle.RegisterMember("notes", &Critter::notes);
handle.RegisterMember("mother_id", &Critter::mother_id);
handle.RegisterMember("father_id", &Critter::father_id);
}
if (auto handle = dmc.RegisterStruct<CritterEntry>()) {
handle.RegisterMember("key", &CritterEntry::key);
handle.RegisterMember("value", &CritterEntry::value);
}
dmc.RegisterArray<std::vector<CritterEntry>>();
dmc.Bind("critters", &critter_list);
dmc.Bind("page_title", &app_data.page_title);
model_handle = dmc.GetModelHandle();
return true;
}
bool Initalize() {
if (!Backend::Initialize("CritterFolio", 800, 600, true)) { if (!Backend::Initialize("CritterFolio", 800, 600, true)) {
std::cout << "[ERROR] Failed to initalize the backend."; std::cout << "[ERROR] Failed to initialize backend.\n";
return false; return false;
} }
std::cout << "[INFO] RmlUI Backend initalized.\n"; std::cout << "[INFO] RmlUI Backend initialized.\n";
Rml::SetSystemInterface(Backend::GetSystemInterface()); Rml::SetSystemInterface(Backend::GetSystemInterface());
Rml::SetRenderInterface(Backend::GetRenderInterface()); Rml::SetRenderInterface(Backend::GetRenderInterface());
Rml::SetFileInterface(&file_interface);
Rml::Initialise(); Rml::Initialise();
Rml::Lua::Initialise(); Rml::Lua::Initialise();
// TODO: Add more error handling for initalization std::cout << "[INFO] Preparing local DB...\n";
if (!ensure_db_copied()) {
std::cout << "[ERROR] Could not prepare local DB.\n";
return false;
}
std::cout << "[INFO] Loading local DB...\n"; std::cout << "[INFO] Loading local DB...\n";
bool db_result = db_get_all_critters(critters); if (!db_get_all_critters(critters)) {
if (!db_result) { std::cout << "[ERROR] Local DB failed to initialize.\n";
std::cout << "[ERROR] Local DB failed to initalize.\n";
return false; return false;
} }
std::cout << "[INFO] Local DB initalized.\n"; std::cout << "[INFO] Local DB initialized.\n";
SyncMapToList(); sync_map_to_list(critters, critter_list);
// ------- DB TESTING int window_width = 800;
// db_print_all_critters(critters); int window_height = 600;
// ------- END DB TESTING {
int window_count = 0;
SDL_Window **windows = SDL_GetWindows(&window_count);
if (windows && window_count > 0) {
SDL_GetWindowSize(windows[0], &window_width, &window_height);
}
SDL_free(windows);
}
ctx = Rml::CreateContext("main", Rml::Vector2i(800, 600)); ctx = Rml::CreateContext("main", Rml::Vector2i(window_width, window_height));
std::cout << "[INFO] Context created at " << window_width << "x" << window_height << "\n";
{
int window_count = 0;
SDL_Window **windows = SDL_GetWindows(&window_count);
if (windows && window_count > 0) {
float display_scale = SDL_GetWindowDisplayScale(windows[0]);
ctx->SetDensityIndependentPixelRatio(display_scale);
SDL_Log("Display scale (dp ratio): %f", display_scale);
}
SDL_free(windows);
}
SDL_Log("Context created at %dx%d", window_width, window_height);
if (!ctx) { if (!ctx) {
std::cout << "[ERROR] Failed to create context."; std::cout << "[ERROR] Failed to create context.\n";
return false; return false;
} }
std::cout << "[INFO] Main context initalized.\n"; std::cout << "[INFO] Main context initialized.\n";
Rml::LoadFontFace("./assets/fonts/Hubballi-Regular.ttf");
Rml::Debugger::Initialise(ctx); Rml::Debugger::Initialise(ctx);
// lua_State *L = Rml::Lua::Interpreter::GetLuaState();
return true; return true;
} }
void Update(Rml::DataModelHandle model_handle) { bool load_document() {
doc = ctx->LoadDocument("./assets/main.rml");
if (!doc) {
std::cout << "[ERROR] Could not load assets/main.rml document, shutting down.\n";
shutdown();
return false;
}
std::cout << "[INFO] Document loaded\n";
doc->Show();
return true;
} }
int main() { bool reload_doc() {
std::cout << "[INFO] Starting critterfolio...\n"; if (doc) {
Rml::Factory::ClearStyleSheetCache();
Rml::Factory::ClearTemplateCache();
doc->Close();
doc = nullptr;
}
if (!load_document()) {
return false;
}
return true;
}
is_initalized = Initalize(); bool f5_was_pressed = false;
bool f8_was_pressed = false;
void handle_input() {
const bool *state = SDL_GetKeyboardState(NULL);
if (state) {
bool f5_state = state[SDL_SCANCODE_F5];
if (f5_state && !f5_was_pressed) {
f5_was_pressed = true;
reload_doc();
} else if (!f5_state) {
f5_was_pressed = false;
}
bool f8_state = state[SDL_SCANCODE_F8];
if (f8_state && !f8_was_pressed) {
f8_was_pressed = true;
bool is_visible = Rml::Debugger::IsVisible();
Rml::Debugger::SetVisible(!is_visible);
} else if (!f8_state) {
f8_was_pressed = false;
}
}
}
int main(int arc, char *argv[]) {
std::cout << "[INFO] Starting CritterFolio...\n";
is_initalized = initialize();
if (!is_initalized) { if (!is_initalized) {
std::cout << "[ERROR] Failed to initalize, shutting down."; std::cout << "[ERROR] Failed to initialize, shutting down.\n";
Shutdown(); shutdown();
return 1; return 1;
} }
std::cout << "[INFO] Application initalized, all systems running.\n";
is_running = true; is_running = true;
Rml::DataModelHandle app_data_handle; Rml::DataModelHandle app_data_handle;
SetupDataBinding(ctx, app_data_handle); if (!setup_data_binding(ctx, app_data_handle, app_data, critter_list, critters)) {
shutdown();
Rml::ElementDocument *doc;
doc = ctx->LoadDocument("./assets/main.rml");
if (!doc) {
std::cout << "[ERROR] Could no load assets/main.rml document, shutting down.";
Shutdown();
return 1; return 1;
} }
if (!load_document()) {
shutdown();
return 1;
}
std::cout << "[INFO] Main page loaded.\n"; std::cout << "[INFO] Main page loaded.\n";
doc->Show(); while (is_running) {
while(is_running) {
is_running = Backend::ProcessEvents(ctx); is_running = Backend::ProcessEvents(ctx);
Update(app_data_handle); {
int window_count = 0;
SDL_Window **windows = SDL_GetWindows(&window_count);
if (windows && window_count > 0) {
int current_width = 0, current_height = 0;
SDL_GetWindowSize(windows[0], &current_width, &current_height);
float display_scale = SDL_GetWindowDisplayScale(windows[0]);
if (Rml::Vector2i(current_width, current_height) != ctx->GetDimensions()) {
ctx->SetDimensions(Rml::Vector2i(current_width, current_height));
}
if (display_scale != ctx->GetDensityIndependentPixelRatio()) {
ctx->SetDensityIndependentPixelRatio(display_scale);
}
}
SDL_free(windows);
}
handle_input();
ctx->Update(); ctx->Update();
Backend::BeginFrame(); Backend::BeginFrame();
ctx->Render(); ctx->Render();
Backend::PresentFrame(); Backend::PresentFrame();
} }
Shutdown(); shutdown();
return 0; return 0;
} }

View File

@@ -0,0 +1,37 @@
#pragma once
#include "asset_io.hpp"
#include <RmlUi/Core/FileInterface.h>
#include <SDL3/SDL.h>
class SDLFileInterface : public Rml::FileInterface {
public:
Rml::FileHandle Open(const Rml::String &path) override {
SDL_IOStream *io = OpenAssetFile(path);
return reinterpret_cast<Rml::FileHandle>(io);
}
void Close(Rml::FileHandle file) override {
SDL_CloseIO(reinterpret_cast<SDL_IOStream *>(file));
}
size_t Read(void *buffer, size_t size, Rml::FileHandle file) override {
return SDL_ReadIO(reinterpret_cast<SDL_IOStream *>(file), buffer, size);
}
bool Seek(Rml::FileHandle file, long offset, int origin) override {
SDL_IOWhence whence = SDL_IO_SEEK_SET;
if (origin == SEEK_CUR) whence = SDL_IO_SEEK_CUR;
else if (origin == SEEK_END) whence = SDL_IO_SEEK_END;
return SDL_SeekIO(reinterpret_cast<SDL_IOStream *>(file), offset, whence) >= 0;
}
size_t Tell(Rml::FileHandle file) override {
return static_cast<size_t>(SDL_TellIO(reinterpret_cast<SDL_IOStream *>(file)));
}
size_t Length(Rml::FileHandle file) override {
return static_cast<size_t>(SDL_GetIOSize(reinterpret_cast<SDL_IOStream *>(file)));
}
};

139
src/ui.cpp Normal file
View File

@@ -0,0 +1,139 @@
#include "ui.hpp"
#include "database.hpp"
#include <iostream>
void sync_map_to_list(const std::map<int, Critter> &critters, std::vector<CritterEntry> &critter_list) {
critter_list.clear();
for (const auto &[id, critter] : critters) {
critter_list.push_back({id, critter});
}
}
bool setup_data_binding(Rml::Context *context, Rml::DataModelHandle &model_handle, AppData &app_data, std::vector<CritterEntry> &critter_list, std::map<int, Critter> &critters) {
Rml::DataModelConstructor dmc = context->CreateDataModel("app_data");
if (!dmc) {
std::cout << "[ERROR] Could not create 'app_data' data binding.\n";
return false;
}
if (auto handle = dmc.RegisterStruct<Critter>()) {
handle.RegisterMember("id", &Critter::id);
handle.RegisterMember("name", &Critter::name);
handle.RegisterMember("gender", &Critter::gender);
handle.RegisterMember("species", &Critter::species);
handle.RegisterMember("notes", &Critter::notes);
handle.RegisterMember("mother_id", &Critter::mother_id);
handle.RegisterMember("father_id", &Critter::father_id);
}
if (auto handle = dmc.RegisterStruct<CritterEntry>()) {
handle.RegisterMember("key", &CritterEntry::key);
handle.RegisterMember("value", &CritterEntry::value);
}
if (auto handle = dmc.RegisterStruct<NewCritterForm>()) {
handle.RegisterMember("name", &NewCritterForm::name);
handle.RegisterMember("gender", &NewCritterForm::gender);
handle.RegisterMember("mother_id", &NewCritterForm::mother_id);
handle.RegisterMember("father_id", &NewCritterForm::father_id);
handle.RegisterMember("notes", &NewCritterForm::notes);
handle.RegisterMember("species", &NewCritterForm::species);
}
dmc.RegisterArray<std::vector<CritterEntry>>();
dmc.Bind("critters", &critter_list);
dmc.Bind("page_title", &app_data.page_title);
dmc.Bind("error_message", &app_data.error_message);
// New Critter dialog
dmc.Bind("is_add_critter_dialog_open", &app_data.is_add_critter_dialog_open);
dmc.Bind("new_critter_form", &app_data.new_critter_form);
// Confirmation dialog
dmc.Bind("is_confirmation_dialog_open", &app_data.is_confirmation_dialog_open);
dmc.Bind("confirmation_dialog_text", &app_data.confirmation_dialog_text);
// Submit Add Critter
dmc.BindEventCallback("submit_add_critter", [&app_data, &critters, &critter_list](Rml::DataModelHandle handle, Rml::Event &, const Rml::VariantList &) {
Critter temp;
temp.name = app_data.new_critter_form.name;
temp.gender = app_data.new_critter_form.gender;
temp.mother_id = app_data.new_critter_form.mother_id;
temp.father_id = app_data.new_critter_form.father_id;
temp.notes = app_data.new_critter_form.notes;
temp.species = app_data.new_critter_form.species;
std::string err;
if (!validate_critter(temp, critters, err)) {
app_data.error_message = err;
handle.DirtyVariable("error_message");
return;
}
int new_id = 0;
if (db_add_critter(temp, new_id)) {
db_get_all_critters(critters);
sync_map_to_list(critters, critter_list);
app_data.new_critter_form.Clear();
app_data.error_message.clear();
app_data.is_add_critter_dialog_open = false;
handle.DirtyVariable("critters");
handle.DirtyVariable("new_critter_form");
handle.DirtyVariable("error_message");
handle.DirtyVariable("is_add_critter_dialog_open");
} else {
app_data.error_message = "Failed to write to database.";
handle.DirtyVariable("error_message");
}
});
// Request Critter Deletion
dmc.BindEventCallback("delete_critter", [&app_data, &critters, &critter_list](Rml::DataModelHandle const_handle, Rml::Event &, const Rml::VariantList &args) {
if (args.empty()) return;
Rml::DataModelHandle handle = const_handle;
int target_id = args[0].Get<int>();
app_data.confirmation_dialog_text = "Are you sure you want to delete critter '" + critters[target_id].name + "'? This action cannot be undone.";
app_data.on_confirm_action = [&critters, &critter_list, target_id, const_handle]() {
Rml::DataModelHandle handle = const_handle;
if (db_delete_critter(target_id)) {
critters.erase(target_id);
sync_map_to_list(critters, critter_list);
handle.DirtyVariable("critters");
std::cout << "[INFO] Deleted critter with ID #" << target_id << std::endl;
}
};
app_data.is_confirmation_dialog_open = true;
handle.DirtyVariable("confirmation_dialog_text");
handle.DirtyVariable("is_confirmation_dialog_open");
});
dmc.BindEventCallback("confirm_dialog_yes", [&app_data](Rml::DataModelHandle const_handle, Rml::Event &, const Rml::VariantList &) {
Rml::DataModelHandle handle = const_handle;
if (app_data.on_confirm_action) {
app_data.on_confirm_action();
app_data.on_confirm_action = nullptr;
}
app_data.is_confirmation_dialog_open = false;
handle.DirtyVariable("is_confirmation_dialog_open");
});
dmc.BindEventCallback("confirm_dialog_no", [&app_data](Rml::DataModelHandle const_handle, Rml::Event &, const Rml::VariantList &) {
Rml::DataModelHandle handle = const_handle;
app_data.on_confirm_action = nullptr;
app_data.is_confirmation_dialog_open = false;
handle.DirtyVariable("is_confirmation_dialog_open");
});
model_handle = dmc.GetModelHandle();
return true;
}

42
src/ui.hpp Normal file
View File

@@ -0,0 +1,42 @@
#pragma once
#include "critter.hpp"
#include <RmlUi/Config/Config.h>
#include <RmlUi/Core/Context.h>
#include <RmlUi/Core/DataModelHandle.h>
#include <functional>
#include <map>
#include <vector>
struct NewCritterForm {
Rml::String name;
Rml::String gender = "F";
int mother_id = 0;
int father_id = 0;
Rml::String notes;
Rml::String species;
void Clear() {
name.clear();
gender = "F";
mother_id = 0;
father_id = 0;
notes.clear();
species.clear();
}
};
struct AppData {
Rml::String page_title = "Main";
bool is_add_critter_dialog_open = false;
bool is_confirmation_dialog_open = false;
Rml::String confirmation_dialog_text = "Are you sure?";
bool confimation_dialog_result = false;
Rml::String error_message;
NewCritterForm new_critter_form;
std::function<void()> on_confirm_action = nullptr;
};
void sync_map_to_list(const std::map<int, Critter> &critters, std::vector<CritterEntry> &critter_list);
bool setup_data_binding(Rml::Context *context, Rml::DataModelHandle &model_handle, AppData &app_data, std::vector<CritterEntry> &critter_list, std::map<int, Critter> &critters);

7988
vendor/stb/stb_image.h vendored Normal file

File diff suppressed because it is too large Load Diff

1724
vendor/stb/stb_image_write.h vendored Normal file

File diff suppressed because it is too large Load Diff