aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore16
-rw-r--r--build.gradle18
-rw-r--r--gradle.properties23
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 59203 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew185
-rw-r--r--gradlew.bat89
-rw-r--r--mobile/.gitignore1
-rw-r--r--mobile/build.gradle59
-rw-r--r--mobile/proguard-rules.pro21
-rw-r--r--mobile/src/androidTest/java/dev/equestria/pluralwear/ExampleInstrumentedTest.kt24
-rw-r--r--mobile/src/main/AndroidManifest.xml26
-rw-r--r--mobile/src/main/java/dev/equestria/pluralwear/MainActivity.kt88
-rw-r--r--mobile/src/main/java/dev/equestria/pluralwear/settings/SettingActivity.kt15
-rw-r--r--mobile/src/main/java/dev/equestria/pluralwear/settings/SettingsFragment.kt11
-rw-r--r--mobile/src/main/res/drawable-v24/ic_launcher_foreground.xml30
-rw-r--r--mobile/src/main/res/drawable/ic_launcher_background.xml170
-rw-r--r--mobile/src/main/res/font/product_sans_bold.ttfbin0 -> 55548 bytes
-rw-r--r--mobile/src/main/res/font/product_sans_bold_italic.ttfbin0 -> 56488 bytes
-rw-r--r--mobile/src/main/res/font/product_sans_italic.ttfbin0 -> 56508 bytes
-rw-r--r--mobile/src/main/res/font/product_sans_regular.ttfbin0 -> 41116 bytes
-rw-r--r--mobile/src/main/res/layout/activity_main.xml45
-rw-r--r--mobile/src/main/res/layout/activity_token.xml24
-rw-r--r--mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml6
-rw-r--r--mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml6
-rw-r--r--mobile/src/main/res/mipmap-hdpi/ic_launcher.webpbin0 -> 1404 bytes
-rw-r--r--mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webpbin0 -> 2898 bytes
-rw-r--r--mobile/src/main/res/mipmap-mdpi/ic_launcher.webpbin0 -> 982 bytes
-rw-r--r--mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webpbin0 -> 1772 bytes
-rw-r--r--mobile/src/main/res/mipmap-xhdpi/ic_launcher.webpbin0 -> 1900 bytes
-rw-r--r--mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webpbin0 -> 3918 bytes
-rw-r--r--mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webpbin0 -> 2884 bytes
-rw-r--r--mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webpbin0 -> 5914 bytes
-rw-r--r--mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webpbin0 -> 3844 bytes
-rw-r--r--mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webpbin0 -> 7778 bytes
-rw-r--r--mobile/src/main/res/values-night/themes.xml5
-rw-r--r--mobile/src/main/res/values/colors.xml12
-rw-r--r--mobile/src/main/res/values/strings.xml7
-rw-r--r--mobile/src/main/res/values/themes.xml5
-rw-r--r--mobile/src/main/res/values/wear.xml7
-rw-r--r--mobile/src/main/res/xml/backup_rules.xml13
-rw-r--r--mobile/src/main/res/xml/data_extraction_rules.xml19
-rw-r--r--mobile/src/main/res/xml/preferences.xml7
-rw-r--r--mobile/src/test/java/dev/equestria/pluralwear/ExampleUnitTest.kt17
-rw-r--r--settings.gradle23
-rw-r--r--wear/.gitignore1
-rw-r--r--wear/build.gradle83
-rw-r--r--wear/proguard-rules.pro21
-rw-r--r--wear/src/main/AndroidManifest.xml69
-rw-r--r--wear/src/main/ic_launcher-playstore.pngbin0 -> 26980 bytes
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/complication/MainComplicationService.kt59
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/components/CircleTransformation.kt37
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/components/MemberList.kt296
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/components/ScalingLazyListWithRSB.kt147
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/PkErrors.kt17
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/PluralKt.kt445
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/Request.kt21
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/Response.kt51
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullFronter.kt24
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullGroup.kt68
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullMember.kt86
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullSystem.kt111
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkAutoProxy.kt15
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkColor.kt27
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkError.kt15
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkErrorMessage.kt15
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGroup.kt23
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGuildMember.kt16
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGuildSystem.kt17
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMember.kt40
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMemberPrivacy.kt23
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMessage.kt20
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkPrivacy.kt12
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkProxyMode.kt12
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkProxyTag.kt12
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSnowflake.kt23
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSwitch.kt43
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystem.kt27
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystemPrivacy.kt22
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystemSettings.kt23
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkType.kt5
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkTypes.kt4
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkUuid.kt24
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/presentation/MainActivity.kt295
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/presentation/PluralKitSyncWorker.kt163
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/presentation/activity/home/HomeActivity.kt133
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/presentation/activity/loading/LoadingActivity.kt141
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/presentation/activity/register_switch/MemberUiState.kt15
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/presentation/activity/register_switch/SwitchActivity.kt195
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Color.kt21
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Theme.kt17
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Type.kt28
-rw-r--r--wear/src/main/java/dev/equestria/pluralwear/tile/MainTileService.kt73
-rw-r--r--wear/src/main/res/drawable-round/tile_preview.pngbin0 -> 14977 bytes
-rw-r--r--wear/src/main/res/drawable/baseline_app_settings_alt_48.xml5
-rw-r--r--wear/src/main/res/drawable/baseline_arrow_back_24.xml5
-rw-r--r--wear/src/main/res/drawable/baseline_check_24.xml5
-rw-r--r--wear/src/main/res/drawable/baseline_check_48.xml5
-rw-r--r--wear/src/main/res/drawable/baseline_home_24.xml5
-rw-r--r--wear/src/main/res/drawable/baseline_switch_account_24.xml5
-rw-r--r--wear/src/main/res/drawable/baseline_sync_problem_48.xml5
-rw-r--r--wear/src/main/res/drawable/baseline_warning_24.xml5
-rw-r--r--wear/src/main/res/drawable/baseline_warning_48.xml5
-rw-r--r--wear/src/main/res/drawable/ic_launcher_background.xml17
-rw-r--r--wear/src/main/res/drawable/ic_launcher_foreground.xml17
-rw-r--r--wear/src/main/res/drawable/tile_preview.pngbin0 -> 8505 bytes
-rw-r--r--wear/src/main/res/font/product_sans_bold.ttfbin0 -> 55548 bytes
-rw-r--r--wear/src/main/res/font/product_sans_bold_italic.ttfbin0 -> 56488 bytes
-rw-r--r--wear/src/main/res/font/product_sans_italic.ttfbin0 -> 56508 bytes
-rw-r--r--wear/src/main/res/font/product_sans_regular.ttfbin0 -> 41116 bytes
-rw-r--r--wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml5
-rw-r--r--wear/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml5
-rw-r--r--wear/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 4692 bytes
-rw-r--r--wear/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 4692 bytes
-rw-r--r--wear/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2949 bytes
-rw-r--r--wear/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 2949 bytes
-rw-r--r--wear/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 6555 bytes
-rw-r--r--wear/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 6555 bytes
-rw-r--r--wear/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 10234 bytes
-rw-r--r--wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 10234 bytes
-rw-r--r--wear/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 14786 bytes
-rw-r--r--wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 14786 bytes
-rw-r--r--wear/src/main/res/values-round/strings.xml3
-rw-r--r--wear/src/main/res/values/strings.xml19
-rw-r--r--wear/src/main/res/values/wear.xml7
125 files changed, 4131 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index a8b0d1d..891f80d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,19 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+
# ---> Android
# Gradle files
.gradle/
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..88913ba
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,18 @@
+buildscript {
+ ext {
+ wear_compose_version = '1.1.1'
+ horologist_version = '0.1.5'
+ wear_tiles_version = '1.1.0'
+ pluralkt_version = '1.2'
+ ktor_version = "2.1.0"
+ picasso_version = "2.8"
+ fragment_version = "1.5.5"
+ work_version = "2.7.1"
+ }
+}// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id 'com.android.application' version '8.0.0-alpha11' apply false
+ id 'com.android.library' version '8.0.0-alpha11' apply false
+ id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
+ id "org.jetbrains.kotlin.plugin.serialization" version "1.7.21"
+} \ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..3c5031e
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true \ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..25bc30d
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jan 11 16:07:41 GMT 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/mobile/.gitignore b/mobile/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/mobile/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/mobile/build.gradle b/mobile/build.gradle
new file mode 100644
index 0000000..722595a
--- /dev/null
+++ b/mobile/build.gradle
@@ -0,0 +1,59 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'dev.equestria.pluralwear'
+ compileSdk 33
+
+ defaultConfig {
+ applicationId "dev.equestria.pluralwear"
+ minSdk 30
+ targetSdk 33
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+dependencies {
+ implementation("com.android.volley:volley:1.2.1")
+ implementation("androidx.core:core-ktx:1.9.0")
+ implementation("androidx.appcompat:appcompat:1.6.0")
+ implementation("com.google.android.material:material:1.7.0")
+ implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
+ implementation("androidx.navigation:navigation-fragment-ktx:2.5.3")
+ implementation("androidx.navigation:navigation-ui-ktx:2.5.3")
+ implementation 'androidx.preference:preference:1.2.0'
+ implementation 'com.google.android.gms:play-services-wearable:18.0.0'
+ implementation "androidx.fragment:fragment-ktx:$fragment_version"
+ implementation 'androidx.preference:preference-ktx:1.2.0'
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+ implementation("com.squareup.picasso:picasso:2.8")
+ implementation(platform("com.google.firebase:firebase-bom:31.1.1"))
+ implementation("com.google.firebase:firebase-analytics-ktx")
+ implementation("com.google.firebase:firebase-messaging-ktx")
+}
diff --git a/mobile/proguard-rules.pro b/mobile/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/mobile/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# 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 *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/mobile/src/androidTest/java/dev/equestria/pluralwear/ExampleInstrumentedTest.kt b/mobile/src/androidTest/java/dev/equestria/pluralwear/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..ef05db4
--- /dev/null
+++ b/mobile/src/androidTest/java/dev/equestria/pluralwear/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package dev.equestria.pluralwear
+
+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("dev.equestria.pluralwear", appContext.packageName)
+ }
+} \ No newline at end of file
diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7fbbb36
--- /dev/null
+++ b/mobile/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <application
+ 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.Pluralwear"
+ tools:targetApi="31">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/mobile/src/main/java/dev/equestria/pluralwear/MainActivity.kt b/mobile/src/main/java/dev/equestria/pluralwear/MainActivity.kt
new file mode 100644
index 0000000..adf3065
--- /dev/null
+++ b/mobile/src/main/java/dev/equestria/pluralwear/MainActivity.kt
@@ -0,0 +1,88 @@
+package dev.equestria.pluralwear
+
+import android.annotation.SuppressLint
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.util.Log
+import android.view.Window
+import androidx.core.view.isInvisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.preference.PreferenceManager
+import com.google.android.gms.wearable.CapabilityClient
+import com.google.android.gms.wearable.CapabilityInfo
+import com.google.android.gms.wearable.DataClient
+import com.google.android.gms.wearable.Node
+import com.google.android.gms.wearable.NodeClient
+import com.google.android.gms.wearable.PutDataMapRequest
+import com.google.android.gms.wearable.PutDataRequest
+import com.google.android.gms.wearable.Wearable
+import com.google.android.material.color.DynamicColors
+import com.google.android.material.elevation.SurfaceColors
+import dev.equestria.pluralwear.databinding.ActivityMainBinding
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.tasks.await
+import kotlinx.coroutines.withContext
+import kotlin.concurrent.thread
+import kotlin.reflect.javaType
+import kotlin.reflect.typeOf
+
+class MainActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityMainBinding
+
+ private lateinit var dataClient: DataClient
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ DynamicColors.applyToActivitiesIfAvailable(application)
+
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ dataClient = Wearable.getDataClient(this)
+
+ /*supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
+ supportActionBar?.hide()
+
+ val color = SurfaceColors.SURFACE_3.getColor(this)
+
+ Log.d("color", color.toString())
+
+ window.statusBarColor = color
+ window.navigationBarColor = color*/
+
+ binding.informationTextView.isInvisible = true
+ binding.fragmentContainerView.isInvisible = false
+
+ PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener { sharedPreferences, key ->
+ sharedPreferences.getString(key, "")?.let {
+ Log.d(key, it)
+ CoroutineScope(Dispatchers.IO).launch {
+ sendPreferenceUpdate(key, it)
+ }
+ }
+ }
+ }
+
+ @SuppressLint("VisibleForTests")
+ private suspend fun sendPreferenceUpdate(key: String, value: String) {
+ try {
+ val request = PutDataMapRequest.create("/preferences").apply {
+ dataMap.putString(key, value)
+ }
+ .asPutDataRequest()
+ .setUrgent()
+
+ val result = dataClient.putDataItem(request).await()
+ Log.d("Pluralwear", "DataItem saved: $result")
+ } catch (cancellationException: CancellationException) {
+ throw cancellationException
+ } catch (exception: Exception) {
+ Log.d("Pluralwear", "Saving DataItem failed: $exception")
+ }
+ }
+} \ No newline at end of file
diff --git a/mobile/src/main/java/dev/equestria/pluralwear/settings/SettingActivity.kt b/mobile/src/main/java/dev/equestria/pluralwear/settings/SettingActivity.kt
new file mode 100644
index 0000000..9de7f69
--- /dev/null
+++ b/mobile/src/main/java/dev/equestria/pluralwear/settings/SettingActivity.kt
@@ -0,0 +1,15 @@
+package dev.equestria.pluralwear.settings
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import dev.equestria.pluralwear.R
+
+class SettingActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.fragment_container_view, SettingsFragment())
+ .commit()
+ }
+} \ No newline at end of file
diff --git a/mobile/src/main/java/dev/equestria/pluralwear/settings/SettingsFragment.kt b/mobile/src/main/java/dev/equestria/pluralwear/settings/SettingsFragment.kt
new file mode 100644
index 0000000..adc8f8d
--- /dev/null
+++ b/mobile/src/main/java/dev/equestria/pluralwear/settings/SettingsFragment.kt
@@ -0,0 +1,11 @@
+package dev.equestria.pluralwear.settings
+
+import android.os.Bundle
+import androidx.preference.PreferenceFragmentCompat
+import dev.equestria.pluralwear.R
+
+class SettingsFragment : PreferenceFragmentCompat() {
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.preferences, rootKey)
+ }
+} \ No newline at end of file
diff --git a/mobile/src/main/res/drawable-v24/ic_launcher_foreground.xml b/mobile/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/mobile/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -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> \ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_launcher_background.xml b/mobile/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/mobile/src/main/res/drawable/ic_launcher_background.xml
@@ -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>
diff --git a/mobile/src/main/res/font/product_sans_bold.ttf b/mobile/src/main/res/font/product_sans_bold.ttf
new file mode 100644
index 0000000..d847195
--- /dev/null
+++ b/mobile/src/main/res/font/product_sans_bold.ttf
Binary files differ
diff --git a/mobile/src/main/res/font/product_sans_bold_italic.ttf b/mobile/src/main/res/font/product_sans_bold_italic.ttf
new file mode 100644
index 0000000..129d12d
--- /dev/null
+++ b/mobile/src/main/res/font/product_sans_bold_italic.ttf
Binary files differ
diff --git a/mobile/src/main/res/font/product_sans_italic.ttf b/mobile/src/main/res/font/product_sans_italic.ttf
new file mode 100644
index 0000000..5fc56d4
--- /dev/null
+++ b/mobile/src/main/res/font/product_sans_italic.ttf
Binary files differ
diff --git a/mobile/src/main/res/font/product_sans_regular.ttf b/mobile/src/main/res/font/product_sans_regular.ttf
new file mode 100644
index 0000000..c0442ee
--- /dev/null
+++ b/mobile/src/main/res/font/product_sans_regular.ttf
Binary files differ
diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..0c500d6
--- /dev/null
+++ b/mobile/src/main/res/layout/activity_main.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.core.widget.NestedScrollView 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:fillViewport="true"
+ android:orientation="vertical"
+ tools:context="dev.equestria.pluralwear.MainActivity">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="16dp">
+
+ <TextView
+ android:id="@+id/information_text_view"
+ android:layout_width="373dp"
+ android:layout_height="33dp"
+ android:fontFamily="@font/product_sans_bold"
+ android:text="@string/find"
+ android:textAlignment="center"
+ android:textAllCaps="false"
+ android:textColor="@color/black"
+ android:textSize="24sp"
+ android:textStyle="bold"
+ android:visibility="visible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/fragment_container_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="visible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ android:name="dev.equestria.pluralwear.settings.SettingsFragment"
+ tools:layout="@layout/activity_token" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.core.widget.NestedScrollView> \ No newline at end of file
diff --git a/mobile/src/main/res/layout/activity_token.xml b/mobile/src/main/res/layout/activity_token.xml
new file mode 100644
index 0000000..9091556
--- /dev/null
+++ b/mobile/src/main/res/layout/activity_token.xml
@@ -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">
+
+ <EditText
+ android:id="@+id/editTextText"
+ android:layout_width="396dp"
+ android:layout_height="50dp"
+ android:layout_marginStart="7dp"
+ android:layout_marginTop="432dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="432dp"
+ android:ems="10"
+ android:inputType="text"
+ android:text="token"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -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> \ No newline at end of file
diff --git a/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/mobile/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -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> \ No newline at end of file
diff --git a/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
--- /dev/null
+++ b/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
diff --git a/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
--- /dev/null
+++ b/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Binary files differ
diff --git a/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
--- /dev/null
+++ b/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
diff --git a/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
--- /dev/null
+++ b/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Binary files differ
diff --git a/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
--- /dev/null
+++ b/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
diff --git a/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
--- /dev/null
+++ b/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
--- /dev/null
+++ b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
diff --git a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
--- /dev/null
+++ b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
--- /dev/null
+++ b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
diff --git a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
--- /dev/null
+++ b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/mobile/src/main/res/values-night/themes.xml b/mobile/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..c5c1af3
--- /dev/null
+++ b/mobile/src/main/res/values-night/themes.xml
@@ -0,0 +1,5 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Theme.Pluralwear" parent="Theme.Material3.DynamicColors.DayNight">
+ </style>
+</resources> \ No newline at end of file
diff --git a/mobile/src/main/res/values/colors.xml b/mobile/src/main/res/values/colors.xml
new file mode 100644
index 0000000..b66d7fc
--- /dev/null
+++ b/mobile/src/main/res/values/colors.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="orange_700">#f7be45</color>
+ <color name="shandy">#fce46f</color>
+ <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> \ No newline at end of file
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2954fba
--- /dev/null
+++ b/mobile/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+<resources>
+ <string name="app_name">Pluralwear</string>
+ <string name="find">Finding Wear OS device and app…</string>
+ <string name="fail_wearos">No Wear OS device found.</string>
+ <string name="fail_wearapp">No Wear OS app found.</string>
+ <string name="exception">Exception encountered.</string>
+</resources> \ No newline at end of file
diff --git a/mobile/src/main/res/values/themes.xml b/mobile/src/main/res/values/themes.xml
new file mode 100644
index 0000000..c5c1af3
--- /dev/null
+++ b/mobile/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Theme.Pluralwear" parent="Theme.Material3.DynamicColors.DayNight">
+ </style>
+</resources> \ No newline at end of file
diff --git a/mobile/src/main/res/values/wear.xml b/mobile/src/main/res/values/wear.xml
new file mode 100644
index 0000000..345d163
--- /dev/null
+++ b/mobile/src/main/res/values/wear.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<resources xmlns:tools="http://schemas.android.com/tools"
+ tools:keep="@array/android_wear_capabilities">
+ <string-array name="android_wear_capabilities">
+ <item>pluralwear_phone_app</item>
+ </string-array>
+</resources> \ No newline at end of file
diff --git a/mobile/src/main/res/xml/backup_rules.xml b/mobile/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/mobile/src/main/res/xml/backup_rules.xml
@@ -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> \ No newline at end of file
diff --git a/mobile/src/main/res/xml/data_extraction_rules.xml b/mobile/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/mobile/src/main/res/xml/data_extraction_rules.xml
@@ -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> \ No newline at end of file
diff --git a/mobile/src/main/res/xml/preferences.xml b/mobile/src/main/res/xml/preferences.xml
new file mode 100644
index 0000000..1b898aa
--- /dev/null
+++ b/mobile/src/main/res/xml/preferences.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <EditTextPreference
+ app:key="pk_token"
+ app:title="PluralKit Token"/>
+</PreferenceScreen> \ No newline at end of file
diff --git a/mobile/src/test/java/dev/equestria/pluralwear/ExampleUnitTest.kt b/mobile/src/test/java/dev/equestria/pluralwear/ExampleUnitTest.kt
new file mode 100644
index 0000000..4ec1e4f
--- /dev/null
+++ b/mobile/src/test/java/dev/equestria/pluralwear/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package dev.equestria.pluralwear
+
+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)
+ }
+} \ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..70a19f5
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,23 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ maven {
+ url "https://maven.proxyfox.dev"
+ }
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven {
+ url "https://maven.proxyfox.dev"
+ }
+ }
+}
+rootProject.name = "Pluralwear"
+include ':mobile'
+include ':wear'
diff --git a/wear/.gitignore b/wear/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/wear/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/wear/build.gradle b/wear/build.gradle
new file mode 100644
index 0000000..b92a048
--- /dev/null
+++ b/wear/build.gradle
@@ -0,0 +1,83 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+ id 'org.jetbrains.kotlin.plugin.serialization'
+}
+
+android {
+ namespace 'dev.equestria.pluralwear'
+ compileSdk 33
+
+ defaultConfig {
+ applicationId "dev.equestria.pluralwear"
+ minSdk 30
+ targetSdk 33
+ versionCode 1
+ versionName "1.0"
+ vectorDrawables {
+ useSupportLibrary true
+ }
+
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion '1.3.2'
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/{AL2.0,LGPL2.1}'
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.core:core-ktx:1.9.0'
+ implementation 'com.google.android.gms:play-services-wearable:18.0.0'
+ implementation 'androidx.percentlayout:percentlayout:1.0.0'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'androidx.recyclerview:recyclerview:1.2.1'
+ implementation platform('androidx.compose:compose-bom:2022.10.00')
+ implementation 'androidx.compose.ui:ui'
+ implementation 'androidx.compose.ui:ui-tooling-preview'
+ implementation "androidx.wear.compose:compose-material:$wear_compose_version"
+ implementation "androidx.wear.compose:compose-foundation:$wear_compose_version"
+ implementation "androidx.wear.compose:compose-navigation:$wear_compose_version"
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
+ implementation 'androidx.activity:activity-compose:1.6.1'
+ implementation "androidx.wear.tiles:tiles:$wear_tiles_version"
+ implementation "androidx.wear.tiles:tiles-material:$wear_tiles_version"
+ implementation "com.google.android.horologist:horologist-compose-tools:$horologist_version"
+ implementation "com.google.android.horologist:horologist-tiles:$horologist_version"
+ implementation 'androidx.wear.watchface:watchface-complications-data-source-ktx:1.1.1'
+ implementation "androidx.work:work-runtime-ktx:$work_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0"
+ implementation 'androidx.preference:preference:1.2.0'
+ implementation "androidx.fragment:fragment-ktx:$fragment_version"
+ implementation "io.ktor:ktor-client-core:$ktor_version"
+ implementation "io.ktor:ktor-client-cio:$ktor_version"
+ implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
+ implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
+ implementation "com.squareup.picasso:picasso:$picasso_version"
+ implementation "com.makeramen:roundedimageview:2.3.0"
+ androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
+ debugImplementation 'androidx.compose.ui:ui-tooling'
+ debugImplementation 'androidx.compose.ui:ui-test-manifest'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4'
+} \ No newline at end of file
diff --git a/wear/proguard-rules.pro b/wear/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/wear/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# 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 *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..559a9aa
--- /dev/null
+++ b/wear/src/main/AndroidManifest.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-feature android:name="android.hardware.type.watch" />
+
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@android:style/Theme.DeviceDefault">
+ <service
+ android:name=".complication.MainComplicationService"
+ android:exported="true"
+ android:label="@string/complication_label"
+ android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
+ <intent-filter>
+ <action android:name="android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.support.wearable.complications.SUPPORTED_TYPES"
+ android:value="SMALL_IMAGE" />
+ <meta-data
+ android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
+ android:value="0" />
+ </service>
+ <service
+ android:name=".tile.MainTileService"
+ android:exported="true"
+ android:label="@string/front_tile_label"
+ android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">
+ <intent-filter>
+ <action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
+ </intent-filter>
+
+ <meta-data
+ android:name="androidx.wear.tiles.PREVIEW"
+ android:resource="@drawable/tile_preview" />
+ </service>
+
+ <uses-library
+ android:name="com.google.android.wearable"
+ android:required="true" />
+ <!--
+ Set to true if your app is Standalone, that is, it does not require the handheld
+ app to run.
+ -->
+ <meta-data
+ android:name="com.google.android.wearable.standalone"
+ android:value="false" />
+
+ <activity
+ android:name=".presentation.MainActivity"
+ android:exported="true"
+ android:label="@string/app_name"
+ android:theme="@android:style/Theme.DeviceDefault">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/wear/src/main/ic_launcher-playstore.png b/wear/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..0fe5924
--- /dev/null
+++ b/wear/src/main/ic_launcher-playstore.png
Binary files differ
diff --git a/wear/src/main/java/dev/equestria/pluralwear/complication/MainComplicationService.kt b/wear/src/main/java/dev/equestria/pluralwear/complication/MainComplicationService.kt
new file mode 100644
index 0000000..1e59c3c
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/complication/MainComplicationService.kt
@@ -0,0 +1,59 @@
+package dev.equestria.pluralwear.complication
+
+import androidx.core.graphics.drawable.toIcon
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.PlainComplicationText
+import androidx.wear.watchface.complications.data.ShortTextComplicationData
+import androidx.wear.watchface.complications.data.SmallImage
+import androidx.wear.watchface.complications.data.SmallImageComplicationData
+import androidx.wear.watchface.complications.data.SmallImageType
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService
+import dev.equestria.pluralwear.presentation.system
+import java.util.Calendar
+
+/**
+ * Skeleton for complication data source that returns short text.
+ */
+class MainComplicationService : SuspendingComplicationDataSourceService() {
+
+ override fun getPreviewData(type: ComplicationType): ComplicationData? {
+ if (type != ComplicationType.SHORT_TEXT) {
+ return null
+ }
+ return createComplicationData("Mon", "Monday")
+ }
+
+ override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData {
+ return when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
+ Calendar.SUNDAY -> createComplicationData("Sun", "Sunday")
+ Calendar.MONDAY -> createComplicationData("Mon", "Monday")
+ Calendar.TUESDAY -> createComplicationData("Tue", "Tuesday")
+ Calendar.WEDNESDAY -> createComplicationData("Wed", "Wednesday")
+ Calendar.THURSDAY -> createComplicationData("Thu", "Thursday")
+ Calendar.FRIDAY -> createComplicationData("Fri!", "Friday!")
+ Calendar.SATURDAY -> createComplicationData("Sat", "Saturday")
+ else -> throw IllegalArgumentException("too many days")
+ }
+ }
+
+
+ private fun createComplicationData(text: String, contentDescription: String): SmallImageComplicationData {
+ /*ShortTextComplicationData.Builder(
+ text = PlainComplicationText.Builder(text).build(),
+ contentDescription = PlainComplicationText.Builder(contentDescription).build()
+ ).build()*/
+ return SmallImageComplicationData.Builder(
+ smallImage = if (system != null && system!!.front != null && system!!.front!!.members.isNotEmpty() && system!!.front!!.members.first().avatar != null) {
+ SmallImage.Builder(
+ system!!.front!!.members.first().avatar!!.toIcon(),
+ SmallImageType.PHOTO
+ ).build()
+ } else {
+ SmallImage.PLACEHOLDER
+ },
+ contentDescription = PlainComplicationText.Builder("Test").build()
+ ).build()
+ }
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/components/CircleTransformation.kt b/wear/src/main/java/dev/equestria/pluralwear/components/CircleTransformation.kt
new file mode 100644
index 0000000..21e1586
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/components/CircleTransformation.kt
@@ -0,0 +1,37 @@
+package dev.equestria.pluralwear.presentation
+
+import android.graphics.Bitmap
+import android.graphics.BitmapShader
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Shader
+import com.squareup.picasso.Transformation
+
+class CircleTransform : Transformation {
+ override fun transform(source: Bitmap): Bitmap {
+ val size = Math.min(source.width, source.height)
+ val x = (source.width - size) / 2
+ val y = (source.height - size) / 2
+ val squaredBitmap = Bitmap.createBitmap(source, x, y, size, size)
+ if (squaredBitmap != source) {
+ source.recycle()
+ }
+ val bitmap = Bitmap.createBitmap(size, size, source.config)
+ val canvas = Canvas(bitmap)
+ val paint = Paint()
+ val shader = BitmapShader(
+ squaredBitmap,
+ Shader.TileMode.CLAMP, Shader.TileMode.CLAMP
+ )
+ paint.shader = shader
+ paint.isAntiAlias = true
+ val r = size / 2f
+ canvas.drawCircle(r, r, r, paint)
+ squaredBitmap.recycle()
+ return bitmap
+ }
+
+ override fun key(): String {
+ return "circle"
+ }
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/components/MemberList.kt b/wear/src/main/java/dev/equestria/pluralwear/components/MemberList.kt
new file mode 100644
index 0000000..6c619c0
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/components/MemberList.kt
@@ -0,0 +1,296 @@
+package dev.equestria.pluralwear.components
+
+import android.util.Log
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.ExperimentalUnitApi
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.Button
+import androidx.wear.compose.material.ButtonDefaults
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.ListHeader
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.ToggleChip
+import androidx.wear.compose.material.ToggleChipDefaults
+import androidx.wear.compose.material.dialog.Alert
+import androidx.wear.compose.material.dialog.Dialog
+import androidx.wear.compose.material.rememberScalingLazyListState
+import dev.equestria.pluralwear.R
+import dev.equestria.pluralwear.pluralkt.fulltypes.PkFullMember
+
+/**
+ * Create a simple member list based on clickable Cards.
+ * Supports scrolling with a rotational input (e.g. crown)
+ *
+ * @param modifier A custom modifier, if required.
+ * @param title The title of the member list.
+ * @param members A list of `PkFullMember`.
+ * @param onClick The function to run when a member is clicked. `it` will contain the `PkFullMember`.
+ */
+@Composable
+fun SingleMemberList(
+ modifier: Modifier = Modifier.fillMaxWidth(),
+ title: String = "Member List",
+ members: List<PkFullMember>,
+ onClick: (PkFullMember) -> Unit,
+) {
+ val state = rememberScalingLazyListState()
+
+ val sortedMembers = sortAlphabetically(members)
+
+ ScalingLazyColumnWithRSB(
+ modifier = modifier.fillMaxWidth(),
+ state = state,
+ snap = true
+ ) {
+ Log.d("MemberList", "RUN!")
+ Log.d("MemberList", "member count: " + sortedMembers.size.toString())
+ item {
+ ListHeader {
+ Text(text = title)
+ }
+ }
+
+ for (index in sortedMembers.indices) {
+ item {
+ SingleSelectMember(member = sortedMembers[index], onClick = onClick)
+ }
+ }
+ }
+}
+
+/**
+ * Create a simple member list based on clickable ToggleCards and a "Submit" button.
+ * Supports scrolling with a rotational input (e.g. crown)
+ *
+ * @param modifier A custom modifier, if required.
+ * @param title The title of the member list.
+ * @param members A list of `PkFullMember`.
+ * @param onSubmit The function to run when the "submit" button is clicked. `it` is a List of `PkFullMember`s who were toggled on.
+ */
+@OptIn(ExperimentalUnitApi::class)
+@Composable
+fun MultipleMemberList(
+ modifier: Modifier = Modifier.fillMaxWidth(),
+ title: String = "Member List",
+ members: List<PkFullMember>,
+ onSubmit: (List<PkFullMember>) -> Unit,
+) {
+ var showNoSelectedDialog by remember { mutableStateOf(false) }
+
+ val state = rememberScalingLazyListState()
+
+ val sortedMembers = sortAlphabetically(members)
+
+ val toggledMembers: MutableList<PkFullMember> = mutableListOf()
+
+ ScalingLazyColumnWithRSB(
+ modifier = modifier.fillMaxWidth(),
+ state = state,
+ snap = true
+ ) {
+ Log.d("MemberList", "RUN!")
+ Log.d("MemberList", "member count: " + sortedMembers.size.toString())
+ item {
+ ListHeader(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(1.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(text = title)
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ overflow = TextOverflow.Visible,
+ text = "Tap the member(s) to select then press \"Submit\"",
+ fontSize = TextUnit(1.5f, TextUnitType.Em),
+ lineHeight = TextUnit(1f, TextUnitType.Em),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ }
+
+ for (index in sortedMembers.indices) {
+ item {
+ MultiSelectMember(member = sortedMembers[index], state = toggledMembers.contains(sortedMembers[index]), onSelect = { member: PkFullMember, toggle: Boolean ->
+ if (toggle) {
+ if (!toggledMembers.contains(member)) toggledMembers.add(member)
+ } else {
+ if (toggledMembers.contains(member)) toggledMembers.remove(member)
+ }
+ })
+ }
+ }
+
+ item {
+ Chip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {
+ if (!toggledMembers.isEmpty()) {
+ onSubmit.invoke(toggledMembers)
+ } else {
+ showNoSelectedDialog = true
+ }
+ },
+ label = { Text("Register switch") },
+ colors = ChipDefaults.primaryChipColors(),
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.baseline_check_24),
+ contentDescription = "Check icon",
+ modifier = Modifier
+ .size(ChipDefaults.IconSize)
+ .wrapContentSize(align = Alignment.Center),
+ tint = Color.Black
+ )
+ }
+ )
+ }
+ }
+
+ val dialogScrollState = rememberScalingLazyListState()
+ Dialog(
+ showDialog = showNoSelectedDialog,
+ onDismissRequest = { showNoSelectedDialog = false },
+ scrollState = dialogScrollState
+ ) {
+ Alert(
+ scrollState = dialogScrollState,
+ verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top),
+ contentPadding =
+ PaddingValues(start = 10.dp, end = 10.dp, top = 24.dp, bottom = 52.dp),
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_warning_48),
+ contentDescription = "warning",
+ modifier = Modifier
+ .size(48.dp)
+ .wrapContentSize(align = Alignment.Center)
+ )
+ },
+ title = { Text(text = "No members selected", textAlign = TextAlign.Center) },
+ message = {
+ Text(
+ text = "You didn't select any members to register a switch with.\n\nThis is a temporary restriction. It will be fixed in a later build.",
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.body2
+ )
+ },
+ ) {
+ item {
+ Button(
+ onClick = { showNoSelectedDialog = false },
+ colors = ButtonDefaults.primaryButtonColors()
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_arrow_back_24),
+ contentDescription = "Close"
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun SingleSelectMember(
+ member: PkFullMember,
+ onClick: (PkFullMember) -> Unit
+) {
+ Chip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {
+ onClick.invoke(member)
+ },
+ label = { Text(if(member.displayName == null) member.name else member.displayName!!) },
+ colors = ChipDefaults.secondaryChipColors(),
+ icon = {
+ member.avatar?.asImageBitmap()?.let {
+ Icon(
+ bitmap = it,
+ contentDescription = "Member Avatar",
+ modifier = Modifier
+ .size(ChipDefaults.IconSize)
+ .wrapContentSize(align = Alignment.Center),
+ tint = Color.Unspecified
+ )
+ }
+ }
+ )
+}
+
+@Composable
+fun MultiSelectMember(
+ member: PkFullMember,
+ state: Boolean = false,
+ onSelect: (PkFullMember, Boolean) -> Unit
+) {
+ var checked by remember { mutableStateOf(state) }
+
+ ToggleChip(
+ modifier = Modifier.fillMaxWidth(),
+ onCheckedChange = {
+ checked = it
+ onSelect.invoke(member, it)
+ },
+ toggleControl = {
+ },
+ label = { Text(if(member.displayName == null) member.name else member.displayName!!) },
+ colors = ToggleChipDefaults.toggleChipColors(
+ uncheckedToggleControlColor = ToggleChipDefaults.SwitchUncheckedIconColor,
+ uncheckedContentColor = MaterialTheme.colors.onSurface,
+ uncheckedStartBackgroundColor = MaterialTheme.colors.surface,
+ uncheckedEndBackgroundColor = MaterialTheme.colors.surface,
+ checkedContentColor = MaterialTheme.colors.onSurface,
+ checkedStartBackgroundColor = MaterialTheme.colors.surface,
+ checkedEndBackgroundColor = MaterialTheme.colors.primary,
+ ),
+ appIcon = {
+ member.avatar?.asImageBitmap()?.let {
+ Icon(
+ bitmap = it,
+ contentDescription = "Member Avatar",
+ modifier = Modifier
+ .size(ChipDefaults.IconSize)
+ .wrapContentSize(align = Alignment.Center),
+ tint = Color.Unspecified
+ )
+ }
+ },
+ checked = checked
+ )
+}
+
+fun sortAlphabetically(list: List<PkFullMember>): List<PkFullMember>{
+ val returnList = list.sortedWith(
+ compareBy(String.CASE_INSENSITIVE_ORDER) { if (it.displayName == null) it.name else it.displayName!! }
+ )
+ return returnList
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/components/ScalingLazyListWithRSB.kt b/wear/src/main/java/dev/equestria/pluralwear/components/ScalingLazyListWithRSB.kt
new file mode 100644
index 0000000..979fe5a
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/components/ScalingLazyListWithRSB.kt
@@ -0,0 +1,147 @@
+package dev.equestria.pluralwear.components
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.input.rotary.onRotaryScrollEvent
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.AutoCenteringParams
+import androidx.wear.compose.material.ScalingLazyColumn
+import androidx.wear.compose.material.ScalingLazyColumnDefaults
+import androidx.wear.compose.material.ScalingLazyListScope
+import androidx.wear.compose.material.ScalingLazyListState
+import androidx.wear.compose.material.ScalingParams
+import androidx.wear.compose.material.rememberScalingLazyListState
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.receiveAsFlow
+
+internal data class TimestampedDelta(val time: Long, val delta: Float)
+
+@SuppressLint("ModifierInspectorInfo")
+@OptIn(ExperimentalComposeUiApi::class)
+@Suppress("ComposableModifierFactory")
+@Composable
+fun Modifier.rsbScroll(
+ scrollableState: ScrollableState,
+ flingBehavior: FlingBehavior,
+ focusRequester: FocusRequester
+): Modifier {
+ val channel = remember {
+ Channel<TimestampedDelta>(
+ capacity = 10,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ }
+
+ var lastTimeMillis = remember { 0L }
+ var smoothSpeed = remember { 0f }
+ val speedWindowMillis = 200L
+ val timeoutToFling = 100L
+
+ return composed {
+ var rsbScrollInProgress by remember { mutableStateOf(false) }
+ LaunchedEffect(rsbScrollInProgress) {
+ if (rsbScrollInProgress) {
+ scrollableState.scroll(MutatePriority.UserInput) {
+ channel.receiveAsFlow().collectLatest {
+ val toScroll = if (lastTimeMillis > 0L && it.time > lastTimeMillis) {
+ val timeSinceLastEventMillis = it.time - lastTimeMillis
+
+ // Speed is in pixels per second.
+ val speed = it.delta * 1000 / timeSinceLastEventMillis
+ val cappedElapsedTimeMillis =
+ timeSinceLastEventMillis.coerceAtMost(speedWindowMillis)
+ smoothSpeed = ((speedWindowMillis - cappedElapsedTimeMillis) * speed +
+ cappedElapsedTimeMillis * smoothSpeed) / speedWindowMillis
+ smoothSpeed * cappedElapsedTimeMillis / 1000
+ } else {
+ 0f
+ }
+ lastTimeMillis = it.time
+ scrollBy(toScroll)
+
+ // If more than the given time pass, start a fling.
+ delay(timeoutToFling)
+
+ lastTimeMillis = 0L
+
+ if (smoothSpeed != 0f) {
+ val launchSpeed = smoothSpeed
+ smoothSpeed = 0f
+ with(flingBehavior) {
+ performFling(launchSpeed)
+ }
+ rsbScrollInProgress = false
+ }
+ }
+ }
+ }
+ }
+ this.onRotaryScrollEvent {
+ channel.trySend(TimestampedDelta(it.uptimeMillis, it.verticalScrollPixels))
+ rsbScrollInProgress = true
+ true
+ }
+ .focusRequester(focusRequester)
+ .focusable()
+ }
+}
+
+
+@Composable
+fun ScalingLazyColumnWithRSB(
+ modifier: Modifier = Modifier,
+ state: ScalingLazyListState = rememberScalingLazyListState(),
+ scalingParams: ScalingParams = ScalingLazyColumnDefaults.scalingParams(),
+ reverseLayout: Boolean = false,
+ snap: Boolean = true,
+ horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+ verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(
+ space = 4.dp,
+ alignment = if (!reverseLayout) Alignment.Top else Alignment.Bottom
+ ),
+ autoCentering: AutoCenteringParams = AutoCenteringParams(),
+ content: ScalingLazyListScope.() -> Unit
+) {
+ val flingBehavior = if (snap) ScalingLazyColumnDefaults.snapFlingBehavior(
+ state = state
+ ) else ScrollableDefaults.flingBehavior()
+ val focusRequester = remember { FocusRequester() }
+ ScalingLazyColumn(
+ modifier = modifier.rsbScroll(
+ scrollableState = state,
+ flingBehavior = flingBehavior,
+ focusRequester = focusRequester
+ ),
+ state = state,
+ reverseLayout = reverseLayout,
+ scalingParams = scalingParams,
+ flingBehavior = flingBehavior,
+ horizontalAlignment = horizontalAlignment,
+ verticalArrangement = verticalArrangement,
+ autoCentering = autoCentering,
+ content = content
+ )
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/PkErrors.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/PkErrors.kt
new file mode 100644
index 0000000..dd1a7e6
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/PkErrors.kt
@@ -0,0 +1,17 @@
+package dev.equestria.pluralwear.pluralkt
+
+/**
+ * Thrown when a PluralKit request is missing a required Authorization header,
+ * or when the existing Authorization header is invalid.
+ *
+ * @param message Additional message to describe the exception.
+ */
+class PkAuthorizationException(message: String) : Exception(message)
+
+/**
+ * Thrown when a PluralKit requests attempts to access something
+ * it does not have access to.
+ *
+ * @param message Additional message to describe the exception.
+ */
+class PkForbiddenException(message: String) : Exception(message) \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/PluralKt.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/PluralKt.kt
new file mode 100644
index 0000000..ff9b6b6
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/PluralKt.kt
@@ -0,0 +1,445 @@
+package dev.equestria.pluralwear.pluralkt
+
+import android.content.res.Resources.NotFoundException
+import android.util.Log
+import dev.equestria.pluralwear.pluralkt.types.*
+import io.ktor.client.*
+import io.ktor.client.call.*
+import io.ktor.client.engine.cio.*
+import io.ktor.client.plugins.*
+import io.ktor.client.plugins.contentnegotiation.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.serialization.*
+import io.ktor.serialization.kotlinx.json.*
+import kotlinx.coroutines.*
+import kotlinx.datetime.Instant
+import kotlinx.serialization.json.*
+import kotlinx.serialization.modules.*
+import java.util.concurrent.*
+import kotlin.coroutines.CoroutineContext
+
+object PluralKt {
+ private var _token = ""
+ private var _tokenInvalid = false
+
+ private val requestQueue: ArrayList<Request<*, *>> = arrayListOf()
+ private const val baseUrl = "https://api.pluralkit.me/v2/"
+ private val module = SerializersModule {
+ polymorphic(PkType::class) {
+ subclass(PkErrorMessage::class)
+ subclass(PkAutoProxy::class)
+ subclass(PkColor::class)
+ subclass(PkError::class)
+ subclass(PkGroup::class)
+ subclass(PkGuildMember::class)
+ subclass(PkGuildSystem::class)
+ subclass(PkMember::class)
+ subclass(PkMemberPrivacy::class)
+ subclass(PkMessage::class)
+ subclass(PkProxyTag::class)
+ subclass(PkSwitch::class)
+ subclass(PkFronter::class)
+ subclass(PkSystem::class)
+ subclass(PkSystemPrivacy::class)
+ subclass(PkSystemSettings::class)
+ }
+ }
+ val json = Json {
+ serializersModule = module
+ prettyPrint = true
+ isLenient = true
+ ignoreUnknownKeys = true
+ }
+ private val client = HttpClient(CIO) {
+ install(UserAgent) {
+ agent = "PluralWear/1.0"
+ }
+ install(ContentNegotiation) {
+ json(json)
+ }
+ }
+ private var scheduler = Executors.newScheduledThreadPool(1)
+ private var tmpDelay: Long = 0
+ private var active: Boolean = false
+
+ private suspend fun <I, O> completeRequest(request: Request<I, O>): HttpResponse {
+ val req = when (request.type) {
+ RequestType.GET -> client.get(request.createUrl(baseUrl)) {
+ request.token?.let { header("Authorization", it) }
+ }
+ RequestType.POST -> client.post(request.createUrl(baseUrl)) {
+ header("content-type", "application/json")
+ request.token?.let { header("Authorization", it) }
+ setBody(request.data, request.inputTypeInfo)
+ }
+ RequestType.PATCH -> client.patch(request.createUrl(baseUrl)) {
+ header("content-type", "application/json")
+ request.token?.let { header("Authorization", it) }
+ setBody(request.data, request.inputTypeInfo)
+ }
+ RequestType.DELETE -> client.delete(request.createUrl(baseUrl)) {
+ request.token?.let { header("Authorization", it) }
+ }
+ }
+ try {
+ request.onComplete(ResponseSuccess(req.body(request.outputTypeInfo)))
+ } catch (ex: JsonConvertException) {
+ try {
+ request.onComplete(ResponseError(req.body()))
+ } catch (exx: JsonConvertException) {
+ Log.d("ex", exx.message.orEmpty())
+ Log.d("Stack Trace", exx.stackTrace.joinToString("\n"))
+ request.onComplete(ResponseNull())
+ }
+ }
+ return req
+ }
+
+ private fun schedule(delay: Long) {
+ if (requestQueue.isEmpty()) {
+ tmpDelay = delay
+ active = false
+ scheduler.shutdown()
+ return
+ }
+ scheduler.schedule({
+ runBlocking {
+ try {
+ handleRequest()
+ } catch (err: Throwable) {
+ err.printStackTrace()
+ schedule(0)
+ }
+ }
+ }, delay, TimeUnit.MILLISECONDS)
+ }
+
+ private suspend fun handleRequest() {
+ val request = requestQueue.shift() ?: run {
+ schedule(0)
+ return
+ }
+ val res = completeRequest(request)
+ schedule(1000/res.headers["x-ratelimit-limit"]!!.toLong())
+ }
+
+ fun <I, O> push(request: Request<I, O>) {
+ requestQueue.push(request)
+ if (!active) {
+ active = true
+ scheduler = Executors.newScheduledThreadPool(1)
+ schedule(tmpDelay)
+ }
+ }
+
+ fun setToken(token: String) {
+ _token = token
+ }
+
+ fun setTokenInvalid(invalid: Boolean) {
+ _tokenInvalid = invalid
+ }
+
+ fun tokenExists(): Boolean {
+ return _token != ""
+ }
+
+ object System {
+ fun getMe(onComplete: Response<PkSystem>.() -> Unit) {
+ push(get("systems/@me", _token, onComplete))
+ }
+
+ fun getMe(): Future<PkSystem> {
+ val future = CompletableFuture<PkSystem>()
+ val onComplete: Response<PkSystem>.() -> Unit = {
+ if(this.isError()) {
+ future.cancel(true)
+ throw UnknownError("PluralKit error: " + this.getError().message)
+ } else if (this.isSuccess()) {
+ future.complete(this.getSuccess())
+ } else {
+ throw NotFoundException("System does not exist.")
+ }
+ }
+
+ getMe(onComplete)
+ return future
+ }
+
+ fun getSystem(systemRef: PkReference, onComplete: Response<PkSystem>.() -> Unit) {
+ push(get("systems/$systemRef", _token, onComplete))
+ }
+
+ fun getSystem(systemRef: PkReference): Future<PkSystem> {
+ val future = CompletableFuture<PkSystem>()
+ val onComplete: Response<PkSystem>.() -> Unit = {
+ if(this.isError()) {
+ future.cancel(true)
+ throw UnknownError("PluralKit error: " + this.getError().message)
+ } else if (this.isSuccess()) {
+ future.complete(this.getSuccess())
+ } else {
+ throw NotFoundException("System does not exist.")
+ }
+ }
+
+ getSystem(systemRef, onComplete)
+ return future
+ }
+
+ fun updateSystem(system: PkSystem, onComplete: Response<PkSystem>.() -> Unit) {
+ push(patch("systems/@me", system, _token, onComplete))
+ }
+
+ fun updateSystem(system: PkSystem): Future<PkSystem> {
+ val future = CompletableFuture<PkSystem>()
+ val onComplete: Response<PkSystem>.() -> Unit = {
+ if(this.isError()) {
+ future.cancel(true)
+ throw UnknownError("PluralKit error: " + this.getError().message)
+ } else if (this.isSuccess()) {
+ future.complete(this.getSuccess())
+ } else {
+ throw NotFoundException("System does not exist.")
+ }
+ }
+
+ updateSystem(system, onComplete)
+ return future
+ }
+
+ fun getSystemSettings(systemRef: PkReference, onComplete: Response<PkSystemSettings>.() -> Unit) {
+ push(get("systems/$systemRef/settings", _token, onComplete))
+ }
+
+ fun getSystemSettings(systemRef: PkReference): Future<PkSystemSettings> {
+ val future = CompletableFuture<PkSystemSettings>()
+ val onComplete: Response<PkSystemSettings>.() -> Unit = {
+ if(this.isError()) {
+ future.cancel(true)
+ throw UnknownError("PluralKit error: " + this.getError().message)
+ } else if (this.isSuccess()) {
+ future.complete(this.getSuccess())
+ } else {
+ throw NotFoundException("System does not exist.")
+ }
+ }
+
+ getSystemSettings(systemRef, onComplete)
+ return future
+ }
+
+ fun updateSystemSettings(settings: PkSystemSettings, onComplete: Response<PkSystemSettings>.() -> Unit) {
+ push(patch("systems/@me/settings", settings, _token, onComplete))
+ }
+
+ fun updateSystemSettings(settings: PkSystemSettings): Future<PkSystemSettings> {
+ val future = CompletableFuture<PkSystemSettings>()
+ val onComplete: Response<PkSystemSettings>.() -> Unit = {
+ if(this.isError()) {
+ future.cancel(true)
+ throw UnknownError("PluralKit error: " + this.getError().message)
+ } else if (this.isSuccess()) {
+ future.complete(this.getSuccess())
+ } else {
+ throw NotFoundException("System does not exist.")
+ }
+ }
+
+ updateSystemSettings(settings, onComplete)
+ return future
+ }
+
+ fun getSystemGuildSettings(guild: PkSnowflake, token: String, onComplete: Response<PkGuildSystem>.() -> Unit) {
+ push(get("systems/@me/guilds/$guild", token, onComplete))
+ }
+
+ fun updateSystemGuildSettings(guild: PkSnowflake, settings: PkGuildSystem, token: String, onComplete: Response<PkGuildSystem>.() -> Unit) {
+ push(patch("systems/@me/guilds/$guild", settings, token, onComplete))
+ }
+
+ fun getAutoProxy(guild: PkSnowflake, token: String, onComplete: Response<PkAutoProxy>.() -> Unit) {
+ push(get("systems/@me/autoproxy?guild_id=$guild", token, onComplete))
+ }
+
+ fun updateAutoProxy(guild: PkSnowflake, autoProxy: PkAutoProxy, token: String, onComplete: Response<PkAutoProxy>.() -> Unit) {
+ push(patch("systems/@me/autoproxy?guild_id=$guild", autoProxy, token, onComplete))
+ }
+ }
+
+ object Member {
+ fun getMembers(systemRef: PkReference, onComplete: Response<Array<PkMember>>.() -> Unit) {
+ push(get("systems/$systemRef/members", _token, onComplete))
+ }
+
+ fun getMembers(systemRef: PkReference): Future<Array<PkMember>> {
+ val future = CompletableFuture<Array<PkMember>>()
+ val onComplete: Response<Array<PkMember>>.() -> Unit = {
+ if(this.isError()) {
+ future.cancel(true)
+ throw UnknownError("PluralKit error: " + this.getError().message)
+ } else if (this.isSuccess()) {
+ future.complete(this.getSuccess())
+ } else {
+ throw NotFoundException("System does not exist.")
+ }
+ }
+
+ getMembers(systemRef, onComplete)
+ return future
+ }
+
+ fun createMember(member: PkMember, token: String, onComplete: Response<PkMember>.() -> Unit) {
+ push(post("members", member, token, onComplete))
+ }
+
+ fun getMember(memberRef: PkReference, token: String? = null, onComplete: Response<PkMember>.() -> Unit) {
+ push(get("members/$memberRef", token, onComplete))
+ }
+
+ fun updateMember(memberRef: PkReference, member: PkMember, token: String, onComplete: Response<PkMember>.() -> Unit) {
+ push(patch("members/$memberRef", member, token, onComplete))
+ }
+
+ fun deleteMember(memberRef: PkReference, token: String, onComplete: Response<PkMember>.() -> Unit) {
+ push(delete("members/$memberRef", token, onComplete))
+ }
+
+ fun getMemberGroups(memberRef: PkReference, token: String? = null, onComplete: Response<Array<PkGroup>>.() -> Unit) {
+ push(get("members/$memberRef/groups", token, onComplete))
+ }
+
+ fun addMemberToGroups(memberRef: PkReference, groups: Array<PkReference>, token: String, onComplete: Response<Array<PkReference>>.() -> Unit) {
+ push(post("members/$memberRef/groups/add", groups, token, onComplete))
+ }
+
+ fun removeMemberFromGroups(memberRef: PkReference, groups: Array<PkReference>, token: String, onComplete: Response<Array<PkReference>>.() -> Unit) {
+ push(post("members/$memberRef/groups/add", groups, token, onComplete))
+ }
+
+ fun overwriteMemberGroups(memberRef: PkReference, groups: Array<PkReference>, token: String, onComplete: Response<Array<PkReference>>.() -> Unit) {
+ push(post("members/$memberRef/groups/overwrite", groups, token, onComplete))
+ }
+
+ fun getMemberGuild(memberRef: PkReference, guild: PkSnowflake, token: String? = null, onComplete: Response<PkMember>.() -> Unit) {
+ push(get("members/$memberRef/guilds/$guild", token, onComplete))
+ }
+
+ fun updateMemberGuild(memberRef: PkReference, guild: PkSnowflake, member: PkGuildMember, token: String, onComplete: Response<PkGuildMember>.() -> Unit) {
+ push(patch("members/$memberRef/guilds/$guild", member, token, onComplete))
+ }
+ }
+
+ object Group {
+ fun getGroups(systemRef: PkReference, onComplete: Response<Array<PkGroup>>.() -> Unit) {
+ push(get("systems/$systemRef/groups", _token, onComplete))
+ }
+
+ fun getGroups(systemRef: PkReference): Future<Array<PkGroup>> {
+ val future = CompletableFuture<Array<PkGroup>>()
+ val onComplete: Response<Array<PkGroup>>.() -> Unit = {
+ if(this.isError()) {
+ future.cancel(true)
+ throw UnknownError("PluralKit error: " + this.getError().message)
+ } else if (this.isSuccess()) {
+ future.complete(this.getSuccess())
+ } else {
+ throw NotFoundException("System does not exist.")
+ }
+ }
+
+ getGroups(systemRef, onComplete)
+ return future
+ }
+
+ fun createGroup(group: PkGroup, token: String, onComplete: Response<PkGroup>.() -> Unit) {
+ push(post("groups", group, token, onComplete))
+ }
+
+ fun getGroup(groupRef: PkReference, token: String? = null, onComplete: Response<PkGroup>.() -> Unit) {
+ push(get("groups/$groupRef", token, onComplete))
+ }
+
+ fun updateGroup(groupRef: PkReference, group: PkGroup, token: String, onComplete: Response<PkGroup>.() -> Unit) {
+ push(patch("groups/$groupRef", group, token, onComplete))
+ }
+
+ fun deleteGroup(groupRef: PkReference, token: String, onComplete: Response<PkGroup>.() -> Unit) {
+ push(delete("groups/$groupRef", token, onComplete))
+ }
+
+ fun getGroupMembers(groupRef: PkReference, token: String? = null, onComplete: Response<Array<PkMember>>.() -> Unit) {
+ push(get("groups/$groupRef/members", token, onComplete))
+ }
+
+ fun addMembersToGroup(groupRef: PkReference, members: Array<PkReference>, token: String, onComplete: Response<Array<PkReference>>.() -> Unit) {
+ push(post("groups/$groupRef/members/add", members, token, onComplete))
+ }
+
+ fun removeMembersFromGroup(groupRef: PkReference, members: Array<PkReference>, token: String, onComplete: Response<Array<PkReference>>.() -> Unit) {
+ push(post("groups/$groupRef/members/remove", members, token, onComplete))
+ }
+
+ fun overwriteMembersInGroup(groupRef: PkReference, members: Array<PkReference>, token: String, onComplete: Response<Array<PkReference>>.() -> Unit) {
+ push(post("groups/$groupRef/members/overwrite", members, token, onComplete))
+ }
+ }
+
+ object Switch {
+ fun getSwitches(systemRef: PkReference, before: Instant, limit: Int = 100, token: String? = null, onComplete: Response<Array<PkSwitch>>.() -> Unit) {
+ if (limit > 100) throw IllegalArgumentException("Limit cannot be greater than 100.")
+ push(get("systems/$systemRef/switches?$before&$limit", token, onComplete))
+ }
+
+ fun getFronters(systemRef: PkReference, onComplete: Response<PkFronter>.() -> Unit) {
+ push(get("systems/$systemRef/fronters", _token, onComplete))
+ }
+
+ fun createSwitch(create: SwitchCreate, onComplete: Response<PkFronter>.() -> Unit) {
+ push(post("systems/@me/switches", create, _token, onComplete))
+ }
+
+ fun createSwitch(create: SwitchCreate): Future<PkFronter> {
+ val future = CompletableFuture<PkFronter>()
+ val onComplete: Response<PkFronter>.() -> Unit = {
+ if(this.isError()) {
+ future.cancel(true)
+ throw UnknownError("PluralKit error: " + this.getError().message)
+ } else if (this.isSuccess()) {
+ future.complete(this.getSuccess())
+ } else {
+ throw NotFoundException("System does not exist.")
+ }
+ }
+
+ createSwitch(create, onComplete)
+ return future
+ }
+
+ fun getSwitch(systemRef: PkReference, switchRef: PkReference, token: String? = null, onComplete: Response<PkFronter>.() -> Unit) {
+ push(get("systems/$systemRef/switches/$switchRef", token, onComplete))
+ }
+
+ fun updateSwitch(systemRef: PkReference, switchRef: PkReference, switch: SwitchCreate, token: String, onComplete: Response<PkFronter>.() -> Unit) {
+ push(patch("systems/$systemRef/switches/$switchRef", switch, token, onComplete))
+ }
+
+ fun deleteSwitch(systemRef: PkReference, switchRef: PkReference, token: String, onComplete: Response<PkFronter>.() -> Unit) {
+ push(delete("systems/$systemRef/switches/$switchRef", token, onComplete))
+ }
+
+ fun updateSwitchMembers(systemRef: PkReference, switchRef: PkReference, members: Array<PkReference>, token: String, onComplete: Response<PkFronter>.() -> Unit) {
+ push(patch("systems/$systemRef/switches/$switchRef/members", members, token, onComplete))
+ }
+ }
+
+ object Misc {
+ fun getMessage(message: PkSnowflake, token: String? = null, onComplete: Response<PkMessage>.() -> Unit) {
+ push(get("messages/$message", token, onComplete))
+ }
+ }
+}
+
+fun <T> ArrayList<T>.push(t: T) = add(t)
+fun <T> ArrayList<T>.shift(): T? = removeFirstOrNull()
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/Request.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/Request.kt
new file mode 100644
index 0000000..51dcc6b
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/Request.kt
@@ -0,0 +1,21 @@
+package dev.equestria.pluralwear.pluralkt
+
+import android.util.Log
+import dev.equestria.pluralwear.pluralkt.types.*
+import io.ktor.util.reflect.*
+
+class Request<I, O>(val endpoint: String, val type: RequestType, val token: String?, val data: I?, val outputTypeInfo: TypeInfo, val inputTypeInfo: TypeInfo, val onComplete: Response<O>.() -> Unit) {
+ fun createUrl(baseUrl: String) = baseUrl+endpoint
+}
+
+inline fun <reified O> get(endpoint: String, token: String? = null, noinline onComplete: Response<O>.() -> Unit) = Request(endpoint, RequestType.GET, token, null, typeInfo<O>(), typeInfo<PkType>(), onComplete)
+inline fun <reified I, reified O> post(endpoint: String, data: I? = null, token: String, noinline onComplete: Response<O>.() -> Unit) = Request(endpoint, RequestType.POST, token, data, typeInfo<O>(), typeInfo<I>(), onComplete)
+inline fun <reified I, reified O> patch(endpoint: String, data: I? = null, token: String, noinline onComplete: Response<O>.() -> Unit) = Request(endpoint, RequestType.PATCH, token, data, typeInfo<O>(), typeInfo<I>(), onComplete)
+inline fun <reified O> delete(endpoint: String, token: String, noinline onComplete: Response<O>.() -> Unit) = Request(endpoint, RequestType.DELETE, token, null, typeInfo<O>(), typeInfo<PkType>(), onComplete)
+
+enum class RequestType {
+ GET,
+ POST,
+ PATCH,
+ DELETE
+}
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/Response.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/Response.kt
new file mode 100644
index 0000000..ab27bcc
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/Response.kt
@@ -0,0 +1,51 @@
+package dev.equestria.pluralwear.pluralkt
+
+import dev.equestria.pluralwear.pluralkt.types.*
+
+interface Response<T> {
+ fun isSuccess(): Boolean
+
+ fun getSuccess(): T
+
+ fun isError(): Boolean
+
+ fun getError(): PkError
+
+ override fun toString(): String
+}
+
+class ResponseSuccess<T>(private val value: T) : Response<T> {
+ override fun isSuccess(): Boolean = true
+
+ override fun getSuccess(): T = value
+
+ override fun isError(): Boolean = false
+
+ override fun getError(): PkError = throw IllegalStateException("Response is not an error")
+
+ override fun toString(): String = value.toString()
+}
+
+class ResponseError<T>(private val error: PkError) : Response<T> {
+ override fun isSuccess(): Boolean = false
+
+ override fun getSuccess(): T = throw IllegalStateException("Response is not a success")
+
+ override fun isError(): Boolean = true
+
+ override fun getError(): PkError = error
+
+ override fun toString(): String = error.toString()
+}
+
+class ResponseNull<T> : Response<T> {
+ override fun isSuccess(): Boolean = false
+
+ override fun getSuccess(): T = throw IllegalStateException("Response is not a success")
+
+ override fun isError(): Boolean = false
+
+ override fun getError(): PkError = throw IllegalStateException("Response is not an error")
+
+ override fun toString(): String = "null"
+}
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullFronter.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullFronter.kt
new file mode 100644
index 0000000..4b2f618
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullFronter.kt
@@ -0,0 +1,24 @@
+package dev.equestria.pluralwear.pluralkt.fulltypes
+
+import dev.equestria.pluralwear.pluralkt.types.PkFronter
+import dev.equestria.pluralwear.pluralkt.types.PkType
+import dev.equestria.pluralwear.pluralkt.types.PkUuid
+import kotlinx.datetime.Instant
+
+class PkFullFronter(pkFronter: PkFronter) : PkType {
+ val id: PkUuid
+ var timestamp: Instant
+ var members: ArrayList<PkFullMember> = arrayListOf()
+
+ init {
+ id = pkFronter.id
+ timestamp = pkFronter.timestamp
+
+ pkFronter.members.forEach {
+ members.add(PkFullMember(it))
+ }
+ }
+
+ override fun toString(): String = "PluralKit Fronter"
+}
+
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullGroup.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullGroup.kt
new file mode 100644
index 0000000..157d368
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullGroup.kt
@@ -0,0 +1,68 @@
+package dev.equestria.pluralwear.pluralkt.fulltypes
+
+import android.graphics.Bitmap
+import android.util.Log
+import com.squareup.picasso.Picasso
+import dev.equestria.pluralwear.pluralkt.types.PkColor
+import dev.equestria.pluralwear.pluralkt.types.PkGroup
+import dev.equestria.pluralwear.pluralkt.types.PkId
+import dev.equestria.pluralwear.pluralkt.types.PkType
+import dev.equestria.pluralwear.pluralkt.types.PkUuid
+import dev.equestria.pluralwear.presentation.CircleTransform
+import kotlinx.datetime.Instant
+import kotlin.concurrent.thread
+
+class PkFullGroup(
+ group: PkGroup
+) : PkType {
+ val id: PkId
+ val uuid: PkUuid
+ val created: Instant
+ var name: String = ""
+ var displayName: String? = null
+ var color: PkColor? = null
+ var iconUrl: String? = null
+ var icon: Bitmap? = null
+ var bannerUrl: String? = null
+ var banner: Bitmap? = null
+ var description: String? = null
+
+ init {
+ id = group.id
+ uuid = group.uuid
+ created = group.created
+ name = group.name
+ displayName = group.displayName
+ color = group.color
+ iconUrl = group.icon
+ bannerUrl = group.banner
+ description = group.description
+
+ thread {
+ // Process icon and banner
+ icon = try {
+ if (iconUrl != null)
+ Picasso.get().load(iconUrl).transform(CircleTransform()).get()
+ else
+ null
+ } catch(ex: Exception) {
+ // It doesn't matter what happened, we just couldn't load it.
+ Log.w("PluralWear", "Couldn't load group icon:\nName: " + this.name + "\nURL: " + this.iconUrl + "\nReason: " + ex.message)
+ null
+ }
+
+ banner = try {
+ if (bannerUrl != null)
+ Picasso.get().load(bannerUrl).transform(CircleTransform()).get()
+ else
+ null
+ } catch(ex: Exception) {
+ // It doesn't matter what happened, we just couldn't load it.
+ Log.w("PluralWear", "Couldn't load group banner:\nName: " + this.name + "\nURL: " + this.bannerUrl + "\nReason: " + ex.message)
+ null
+ }
+ }
+ }
+
+ override fun toString(): String = "PluralKit Group"
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullMember.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullMember.kt
new file mode 100644
index 0000000..7de8a7d
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullMember.kt
@@ -0,0 +1,86 @@
+package dev.equestria.pluralwear.pluralkt.fulltypes
+
+import android.graphics.Bitmap
+import android.util.Log
+import com.squareup.picasso.Picasso
+import dev.equestria.pluralwear.pluralkt.types.PkColor
+import dev.equestria.pluralwear.pluralkt.types.PkId
+import dev.equestria.pluralwear.pluralkt.types.PkMember
+import dev.equestria.pluralwear.pluralkt.types.PkMemberPrivacy
+import dev.equestria.pluralwear.pluralkt.types.PkProxyTag
+import dev.equestria.pluralwear.pluralkt.types.PkType
+import dev.equestria.pluralwear.pluralkt.types.PkUuid
+import dev.equestria.pluralwear.presentation.CircleTransform
+import kotlinx.datetime.Instant
+import kotlin.concurrent.thread
+
+class PkFullMember(
+ member: PkMember
+): PkType {
+ val id: PkId
+ val uuid: PkUuid
+ val created: Instant
+ var name: String = ""
+ var displayName: String? = null
+ var color: PkColor? = null
+ var birthday: String? = null
+ var pronouns: String? = null
+ var avatarUrl: String? = null
+ var avatar: Bitmap? = null
+ var bannerUrl: String? = null
+ var banner: Bitmap? = null
+ var description: String? = null
+ var proxyTags: ArrayList<PkProxyTag> = arrayListOf()
+ var keepProxy: Boolean = false
+ var autoProxyEnabled: Boolean? = null
+ val messageCount: Int?
+ val lastMessage: Instant?
+ var privacy: PkMemberPrivacy? = null
+
+ init {
+ id = member.id
+ uuid = member.uuid
+ created = member.created
+ displayName = member.displayName
+ color = member.color
+ birthday = member.birthday
+ pronouns = member.pronouns
+ avatarUrl = member.avatarUrl
+ bannerUrl = member.banner
+ description = member.description
+ proxyTags = member.proxyTags
+ keepProxy = member.keepProxy
+ autoProxyEnabled = member.autoProxyEnabled
+ messageCount = member.messageCount
+ lastMessage = member.lastMessage
+ privacy = member.privacy
+
+
+ thread {
+ // Process avatar and banner
+ avatar = try {
+ if (avatarUrl != null)
+ Picasso.get().load(avatarUrl).transform(CircleTransform()).get()
+ else
+ null
+ } catch(ex: Exception) {
+ // It doesn't matter what happened, we just couldn't load it.
+ Log.w("PluralWear", "Couldn't load member avatar:\nName: " + this.name + "\nURL: " + this.avatarUrl + "\nReason: " + ex.message)
+ null
+ }
+
+ banner = try {
+ if (bannerUrl != null)
+ Picasso.get().load(bannerUrl).transform(CircleTransform()).get()
+ else
+ null
+ } catch(ex: Exception) {
+ // It doesn't matter what happened, we just couldn't load it.
+ Log.w("PluralWear", "Couldn't load member banner:\nName: " + this.name + "\nURL: " + this.bannerUrl + "\nReason: " + ex.message)
+ null
+ }
+ }
+ }
+
+ override fun toString(): String = "PluralKit Member"
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullSystem.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullSystem.kt
new file mode 100644
index 0000000..8958268
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/fulltypes/PkFullSystem.kt
@@ -0,0 +1,111 @@
+package dev.equestria.pluralwear.pluralkt.fulltypes
+
+import android.graphics.Bitmap
+import android.util.Log
+import com.squareup.picasso.Picasso
+import dev.equestria.pluralwear.pluralkt.types.PkColor
+import dev.equestria.pluralwear.pluralkt.types.PkFronter
+import dev.equestria.pluralwear.pluralkt.types.PkGroup
+import dev.equestria.pluralwear.pluralkt.types.PkId
+import dev.equestria.pluralwear.pluralkt.types.PkMember
+import dev.equestria.pluralwear.pluralkt.types.PkSystem
+import dev.equestria.pluralwear.pluralkt.types.PkSystemPrivacy
+import dev.equestria.pluralwear.pluralkt.types.PkSystemSettings
+import dev.equestria.pluralwear.pluralkt.types.PkType
+import dev.equestria.pluralwear.pluralkt.types.PkUuid
+import dev.equestria.pluralwear.presentation.CircleTransform
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+import java.util.UUID
+import kotlin.concurrent.thread
+
+/*
+ A PkFullSystem is a system which contains all the relevant information
+ that a program may need to know about a System.
+
+ This includes full member data, full group data, switches, and so on.
+
+ */
+
+class PkFullSystem(
+ system: PkSystem?, systemSettings: PkSystemSettings?, systemMembers: Array<PkMember>?, systemGroups: Array<PkGroup>?, systemFronter: PkFronter?
+) : PkType {
+ // Basic system stuff
+ val id: PkId
+ val uuid: PkUuid
+ val created: Instant
+ var name: String? = null
+ var description: String? = null
+ var tag: String? = null
+ var pronouns: String? = null
+ var avatarUrl: String? = null
+ var avatar: Bitmap? = null
+ var bannerUrl: String? = null
+ var banner: Bitmap? = null
+ var color: PkColor? = null
+ var privacy: PkSystemPrivacy? = null
+ var webhookUrl: String? = null
+
+ // Custom stuff
+ val settings: PkSystemSettings?
+ var members: MutableList<PkFullMember>? = mutableListOf()
+ var groups: MutableList<PkFullGroup>? = mutableListOf()
+ var front: PkFullFronter?
+
+ init {
+ id = system?.id ?: ""
+ uuid = system?.uuid ?: PkUuid(UUID(0,0))
+ created = system?.created ?: Clock.System.now()
+ name = system?.name
+ description = system?.description
+ tag = system?.tag
+ pronouns = system?.pronouns
+ avatarUrl = system?.avatarUrl
+ bannerUrl = system?.banner
+ color = system?.color
+ privacy = system?.privacy
+ webhookUrl = system?.webhookUrl
+
+ settings = systemSettings
+ front = systemFronter?.let { PkFullFronter(it) }
+
+ if (systemMembers != null) {
+ systemMembers.forEach {
+ members?.add(PkFullMember(it))
+ }
+ }
+
+ if (systemGroups != null) {
+ systemGroups.forEach {
+ groups?.add(PkFullGroup(it))
+ }
+ }
+
+ thread {
+ // Process avatar and banner
+ avatar = try {
+ if (avatarUrl != null)
+ Picasso.get().load(avatarUrl).transform(CircleTransform()).get()
+ else
+ null
+ } catch(ex: Exception) {
+ // It doesn't matter what happened, we just couldn't load it.
+ Log.w("PluralWear", "Couldn't load system avatar:\nName: " + this.name + "\nURL: " + this.avatarUrl + "\nReason: " + ex.message)
+ null
+ }
+
+ banner = try {
+ if (bannerUrl != null)
+ Picasso.get().load(bannerUrl).get()
+ else
+ null
+ } catch(ex: Exception) {
+ // It doesn't matter what happened, we just couldn't load it.
+ Log.w("PluralWear", "Couldn't load system banner:\nName: " + this.name + "\nURL: " + this.bannerUrl + "\nReason: " + ex.message)
+ null
+ }
+ }
+ }
+
+ override fun toString(): String = "PluralKit System"
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkAutoProxy.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkAutoProxy.kt
new file mode 100644
index 0000000..869c85f
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkAutoProxy.kt
@@ -0,0 +1,15 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.datetime.Instant
+import kotlinx.serialization.*
+
+@Serializable
+class PkAutoProxy : PkType {
+ val lastLatchTimestamp: Instant? = null
+ @SerialName("autoproxy_mode")
+ var autoProxyMode: PkProxyMode = PkProxyMode.OFF
+ var autoProxyMember: PkId? = null
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkColor.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkColor.kt
new file mode 100644
index 0000000..1f16e19
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkColor.kt
@@ -0,0 +1,27 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+@JvmInline
+@Serializable(with = PkColor.Serializer::class)
+value class PkColor(val color: Int) : PkType {
+ constructor(color: String) : this(if (color == "") -1 else (color.toUIntOrNull(16)?.toInt() ?: Integer.decode(color)) and 0xFFFFFF)
+
+ fun getString(): String? = if (color < 0) null else color.toString(16).run { padStart(6, '0') }
+
+ class Serializer : KSerializer<PkColor> {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("color")
+ override fun deserialize(decoder: Decoder): PkColor = PkColor(decoder.decodeString())
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun serialize(encoder: Encoder, value: PkColor) = value.getString()?.let {encoder.encodeString(it)} ?: encoder.encodeNull()
+ }
+
+ override fun toString(): String = getString() ?: "000000"
+}
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkError.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkError.kt
new file mode 100644
index 0000000..12bd174
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkError.kt
@@ -0,0 +1,15 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.serialization.*
+
+@Serializable
+class PkError : PkType {
+ val code: Int = 0
+ val message: String = ""
+ val errors: HashMap<String, PkErrorMessage>? = null
+ @SerialName("retry_after")
+ val retryAfter: Int? = null
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkErrorMessage.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkErrorMessage.kt
new file mode 100644
index 0000000..f6befd3
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkErrorMessage.kt
@@ -0,0 +1,15 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.serialization.*
+
+@Serializable
+class PkErrorMessage : PkType {
+ val message: String = ""
+ @SerialName("max_length")
+ val maxLength: Int? = null
+ @SerialName("actual_length")
+ val actualLength: Int? = null
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGroup.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGroup.kt
new file mode 100644
index 0000000..565a1c9
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGroup.kt
@@ -0,0 +1,23 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+import kotlinx.serialization.*
+import java.util.*
+
+@Serializable
+class PkGroup : PkType {
+ val id: PkId = ""
+ val uuid: PkUuid = PkUuid(UUID(0,0))
+ val created: Instant = Clock.System.now()
+ var name: String = ""
+ @SerialName("display_name")
+ var displayName: String? = null
+ var color: PkColor? = null
+ var icon: String? = null
+ var banner: String? = null
+ var description: String? = null
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGuildMember.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGuildMember.kt
new file mode 100644
index 0000000..670479b
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGuildMember.kt
@@ -0,0 +1,16 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.serialization.*
+
+@Serializable
+class PkGuildMember : PkType {
+ @SerialName("guild_id")
+ val guildId: PkSnowflake = PkSnowflake(0UL)
+ @SerialName("display_name")
+ var displayName: String? = null
+ @SerialName("avatar_url")
+ var avatarUrl: String? = null
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGuildSystem.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGuildSystem.kt
new file mode 100644
index 0000000..30f3017
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkGuildSystem.kt
@@ -0,0 +1,17 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.serialization.*
+
+@Serializable
+class PkGuildSystem : PkType {
+ @SerialName("guild_id")
+ val guildId: PkSnowflake = PkSnowflake(0UL)
+ @SerialName("proxying_enabled")
+ var proxyingEnabled: Boolean = true
+ var tag: String? = null
+ @SerialName("tag_enabled")
+ var tagEnabled: Boolean = true
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMember.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMember.kt
new file mode 100644
index 0000000..b903c9b
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMember.kt
@@ -0,0 +1,40 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import android.graphics.Bitmap
+import com.squareup.picasso.Picasso
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+import kotlinx.serialization.*
+import java.util.*
+import kotlin.collections.ArrayList
+
+@Serializable
+class PkMember : PkType {
+ val id: PkId = ""
+ val uuid: PkUuid = PkUuid(UUID(0,0))
+ val created: Instant = Clock.System.now()
+ var name: String = ""
+ @SerialName("display_name")
+ var displayName: String? = null
+ var color: PkColor? = null
+ var birthday: String? = null
+ var pronouns: String? = null
+ @SerialName("avatar_url")
+ var avatarUrl: String? = null
+ var banner: String? = null
+ var description: String? = null
+ @SerialName("proxy_tags")
+ var proxyTags: ArrayList<PkProxyTag> = arrayListOf()
+ @SerialName("keep_proxy")
+ var keepProxy: Boolean = false
+ @SerialName("autoproxy_enabled")
+ var autoProxyEnabled: Boolean? = null
+ @SerialName("message_count")
+ val messageCount: Int? = null
+ @SerialName("last_message_timestamp")
+ val lastMessage: Instant? = null
+ var privacy: PkMemberPrivacy? = null
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+}
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMemberPrivacy.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMemberPrivacy.kt
new file mode 100644
index 0000000..78c9fcf
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMemberPrivacy.kt
@@ -0,0 +1,23 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.serialization.*
+
+@Serializable
+class PkMemberPrivacy : PkType {
+ var visibility: PkPrivacy? = null
+ @SerialName("name_privacy")
+ var name: PkPrivacy? = null
+ @SerialName("description_privacy")
+ var description: PkPrivacy? = null
+ @SerialName("birthday_privacy")
+ var birthday: PkPrivacy? = null
+ @SerialName("pronoun_privacy")
+ var pronoun: PkPrivacy? = null
+ @SerialName("avatar_privacy")
+ var avatar: PkPrivacy? = null
+ @SerialName("metadata_privacy")
+ var metadata: PkPrivacy? = null
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMessage.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMessage.kt
new file mode 100644
index 0000000..73c9e35
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkMessage.kt
@@ -0,0 +1,20 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+import kotlinx.serialization.*
+
+@Serializable
+class PkMessage : PkType {
+ val timestamp: Instant = Clock.System.now()
+ val id: PkSnowflake = PkSnowflake(0UL)
+ val original: PkSnowflake = PkSnowflake(0UL)
+ val sender: PkSnowflake = PkSnowflake(0UL)
+ val channel: PkSnowflake = PkSnowflake(0UL)
+ val guild: PkSnowflake = PkSnowflake(0UL)
+ val system: PkSystem? = null
+ val member: PkMember? = null
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkPrivacy.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkPrivacy.kt
new file mode 100644
index 0000000..47a7179
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkPrivacy.kt
@@ -0,0 +1,12 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+enum class PkPrivacy {
+ @SerialName("private")
+ PRIVATE,
+ @SerialName("public")
+ PUBLIC
+}
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkProxyMode.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkProxyMode.kt
new file mode 100644
index 0000000..15f5adf
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkProxyMode.kt
@@ -0,0 +1,12 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.serialization.*
+
+@Serializable
+enum class PkProxyMode {
+ @SerialName("off") OFF,
+ @SerialName("front") FRONT,
+ @SerialName("latch") LATCH,
+ @SerialName("member") MEMBER
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkProxyTag.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkProxyTag.kt
new file mode 100644
index 0000000..fd6d09d
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkProxyTag.kt
@@ -0,0 +1,12 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.serialization.*
+
+@Serializable
+class PkProxyTag : PkType {
+ var prefix: String? = null
+ var suffix: String? = null
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSnowflake.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSnowflake.kt
new file mode 100644
index 0000000..6e74a73
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSnowflake.kt
@@ -0,0 +1,23 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+@JvmInline
+@Serializable(with = PkSnowflake.Serializer::class)
+value class PkSnowflake(val value: ULong) : PkType {
+ constructor(value: String) : this(value.toULong())
+
+ class Serializer : KSerializer<PkSnowflake> {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("snowflake")
+ override fun deserialize(decoder: Decoder): PkSnowflake = PkSnowflake(decoder.decodeString())
+
+ override fun serialize(encoder: Encoder, value: PkSnowflake) = encoder.encodeString(value.toString())
+ }
+
+ override fun toString(): String = value.toString()
+}
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSwitch.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSwitch.kt
new file mode 100644
index 0000000..99580b3
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSwitch.kt
@@ -0,0 +1,43 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+import kotlinx.serialization.*
+import java.util.*
+import kotlin.collections.ArrayList
+
+@Serializable
+class PkSwitch : PkType {
+ val id: PkUuid = PkUuid(UUID(0,0))
+ var timestamp: Instant = Clock.System.now()
+ var members: ArrayList<String> = arrayListOf()
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+}
+
+@Serializable
+class PkFronter : PkType {
+ val id: PkUuid = PkUuid(UUID(0,0))
+ var timestamp: Instant = Clock.System.now()
+ var members: ArrayList<PkMember> = arrayListOf()
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+}
+
+@Serializable
+class SwitchCreate : PkType {
+ var timestamp: Instant? = null
+ var members: ArrayList<PkReference> = arrayListOf()
+
+ override fun toString(): String {
+ return if (members.isEmpty()) {
+ if (timestamp == null) {
+ "{\n \"members\": []\n}"
+ } else {
+ "{\n \"members\": [],\n \"timestamp\": \"" + timestamp.toString() + "\"\n}"
+ }
+ } else
+ PluralKt.json.encodeToString(this)
+ }
+}
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystem.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystem.kt
new file mode 100644
index 0000000..97d811a
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystem.kt
@@ -0,0 +1,27 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+import kotlinx.serialization.*
+import java.util.*
+
+@Serializable
+class PkSystem : PkType {
+ val id: PkId = ""
+ val uuid: PkUuid = PkUuid(UUID(0,0))
+ val created: Instant = Clock.System.now()
+ var name: String? = null
+ var description: String? = null
+ var tag: String? = null
+ var pronouns: String? = null
+ @SerialName("avatar_url")
+ var avatarUrl: String? = null
+ var banner: String? = null
+ var color: PkColor? = null
+ var privacy: PkSystemPrivacy? = null
+ @SerialName("webhook_url")
+ var webhookUrl: String? = null
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystemPrivacy.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystemPrivacy.kt
new file mode 100644
index 0000000..2235f09
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystemPrivacy.kt
@@ -0,0 +1,22 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.serialization.*
+
+@Serializable
+class PkSystemPrivacy : PkType {
+ @SerialName("description_privacy")
+ var description: PkPrivacy? = null
+ @SerialName("pronoun_privacy")
+ var pronoun: PkPrivacy? = null
+ @SerialName("member_list_privacy")
+ var memberList: PkPrivacy? = null
+ @SerialName("group_list_privacy")
+ var groupList: PkPrivacy? = null
+ @SerialName("front_privacy")
+ var front: PkPrivacy? = null
+ @SerialName("front_history_privacy")
+ var frontHistory: PkPrivacy? = null
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystemSettings.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystemSettings.kt
new file mode 100644
index 0000000..ee9a290
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkSystemSettings.kt
@@ -0,0 +1,23 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import dev.equestria.pluralwear.pluralkt.*
+import kotlinx.serialization.*
+
+@Serializable
+class PkSystemSettings : PkType {
+ @SerialName("member_limit")
+ val memberLimit: Int = 1000
+ @SerialName("group_limit")
+ val groupLimit: Int = 250
+ var timezone: String = "UTC"
+ @SerialName("pings_enabled")
+ var pingsEnabled: Boolean = true
+ @SerialName("member_default_private")
+ var memberDefaultPrivate: Boolean = false
+ @SerialName("group_default_private")
+ var groupDefaultPrivate: Boolean = false
+ @SerialName("show_private_info")
+ var showPrivateInfo: Boolean = false
+
+ override fun toString(): String = PluralKt.json.encodeToString(this)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkType.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkType.kt
new file mode 100644
index 0000000..bdb2a39
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkType.kt
@@ -0,0 +1,5 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+interface PkType {
+ override fun toString(): String
+}
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkTypes.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkTypes.kt
new file mode 100644
index 0000000..7e04930
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkTypes.kt
@@ -0,0 +1,4 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+typealias PkId = String
+typealias PkReference = String
diff --git a/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkUuid.kt b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkUuid.kt
new file mode 100644
index 0000000..dac995a
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/pluralkt/types/PkUuid.kt
@@ -0,0 +1,24 @@
+package dev.equestria.pluralwear.pluralkt.types
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import java.util.UUID
+
+@JvmInline
+@Serializable(with = PkUuid.Serializer::class)
+value class PkUuid(val uuid: UUID) : PkType {
+ constructor(value: String) : this(UUID.fromString(value))
+
+ class Serializer : KSerializer<PkUuid> {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("uuid")
+ override fun deserialize(decoder: Decoder): PkUuid = PkUuid(decoder.decodeString())
+
+ override fun serialize(encoder: Encoder, value: PkUuid) = encoder.encodeString(value.toString())
+ }
+
+ override fun toString(): String = uuid.toString()
+}
diff --git a/wear/src/main/java/dev/equestria/pluralwear/presentation/MainActivity.kt b/wear/src/main/java/dev/equestria/pluralwear/presentation/MainActivity.kt
new file mode 100644
index 0000000..dd6d211
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/presentation/MainActivity.kt
@@ -0,0 +1,295 @@
+/* While this template provides a good starting point for using Wear Compose, you can always
+ * take a look at https://github.com/android/wear-os-samples/tree/main/ComposeStarter and
+ * https://github.com/android/wear-os-samples/tree/main/ComposeAdvanced to find the most up to date
+ * changes to the libraries and their usages.
+ */
+
+package dev.equestria.pluralwear.presentation
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.dialog.Alert
+import androidx.wear.compose.material.rememberScalingLazyListState
+import androidx.wear.compose.navigation.SwipeDismissableNavHost
+import androidx.wear.compose.navigation.composable
+import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
+import androidx.wear.tiles.TileService
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkRequest
+import com.google.android.gms.wearable.DataMap
+import com.google.android.gms.wearable.DataMapItem
+import com.google.android.gms.wearable.Wearable
+import dev.equestria.pluralwear.R
+import dev.equestria.pluralwear.pluralkt.PluralKt
+import dev.equestria.pluralwear.pluralkt.fulltypes.PkFullSystem
+import dev.equestria.pluralwear.pluralkt.types.PkFronter
+import dev.equestria.pluralwear.pluralkt.types.PkGroup
+import dev.equestria.pluralwear.pluralkt.types.PkMember
+import dev.equestria.pluralwear.pluralkt.types.PkSystem
+import dev.equestria.pluralwear.pluralkt.types.PkSystemSettings
+import dev.equestria.pluralwear.presentation.activity.home.HomePage
+import dev.equestria.pluralwear.presentation.activity.loading.LoadingAppPage
+import dev.equestria.pluralwear.presentation.activity.loading.LoadingSystemPage
+import dev.equestria.pluralwear.presentation.activity.register_switch.SwitchPage
+import dev.equestria.pluralwear.presentation.theme.PluralwearTheme
+import dev.equestria.pluralwear.tile.MainTileService
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.tasks.await
+import kotlinx.datetime.Clock
+import java.time.Duration
+
+var system: PkFullSystem? = null
+
+class MainActivity : ComponentActivity() {
+ @SuppressLint("VisibleForTests")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ //capabilityClient = Wearable.getCapabilityClient(this)
+ if (system == null) {
+ Wearable.getDataClient(this).dataItems.also { item ->
+ CoroutineScope(Dispatchers.IO).launch {
+ val realItem = item.await()
+ if (realItem.count != 0) {
+ if ((realItem.get(0).uri.path?.compareTo("/preferences") ?: "") == 0) {
+ Log.d("Pluralwear", "AAAA!")
+ DataMapItem.fromDataItem(realItem.get(0)).dataMap.apply {
+ updatePreferences(this)
+ }
+ }
+ } else {
+ loadSetupApp()
+ }
+ realItem.release()
+ }
+ }
+
+ setContent {
+ LoadingAppPage()
+ }
+ } else {
+ setContent {
+ WearApp()
+ }
+ }
+ }
+
+ @SuppressLint("VisibleForTests")
+ fun updatePreferences(dataMap: DataMap) {
+ Log.d("PluralWear", "Update preference")
+ PluralKt.setToken(dataMap.getString("pk_token", ""))
+
+ if(PluralKt.tokenExists()) {
+ setContent {
+ LoadingSystemPage()
+ }
+
+ val context = this
+
+ val constraints = Constraints.Builder()
+ .setRequiresBatteryNotLow(true)
+ .build()
+
+ val pluralKitSyncWorkRequest: PeriodicWorkRequest =
+ PeriodicWorkRequestBuilder<PluralKitSyncWorker>(Duration.ofMinutes(15))
+ .setConstraints(constraints)
+ .build()
+
+ //WorkManager.getInstance(this).cancelAllWork()
+
+ WorkManager.getInstance(this).enqueueUniquePeriodicWork(
+ "PluralKitSync",
+ ExistingPeriodicWorkPolicy.REPLACE,
+ pluralKitSyncWorkRequest)
+
+ onPluralKitSync(true) {
+ if (it == null) {
+ system = null
+ PluralKt.setTokenInvalid(true)
+
+ setContent {
+ LoadBadTokenPage()
+ }
+ } else {
+ system = it
+ setContent {
+ WearApp()
+ }
+ TileService.getUpdater(this)
+ .requestUpdate(MainTileService::class.java)
+ }
+ }
+
+ onPluralKitSync {
+ if (it == null) {
+ system = null
+ PluralKt.setTokenInvalid(true)
+
+ setContent {
+ LoadBadTokenPage()
+ }
+ } else {
+ system = it
+ TileService.getUpdater(this)
+ .requestUpdate(MainTileService::class.java)
+ }
+ }
+ } else {
+ setContent {
+ LoadNoTokenPage()
+ }
+ }
+ }
+
+ fun loadSetupApp() {
+ setContent {
+ LoadSetupPage()
+ }
+ }
+}
+
+@Composable
+fun WearApp() {
+ val navController = rememberSwipeDismissableNavController()
+
+ PluralwearTheme {
+ SwipeDismissableNavHost(
+ navController = navController,
+ startDestination = "home"
+ ) {
+ composable("home") {
+ //PluralwearApp(navController)
+ HomePage(system, navController)
+ }
+ composable("switch") {
+ SwitchPage(system, navController)
+ }
+ }
+ }
+}
+
+@Composable
+fun LoadNoTokenPage() {
+ val scrollState = rememberScalingLazyListState()
+ Alert(
+ scrollState = scrollState,
+ verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top),
+ contentPadding =
+ PaddingValues(start = 10.dp, end = 10.dp, top = 24.dp, bottom = 52.dp),
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_warning_48),
+ contentDescription = "warning",
+ modifier = Modifier
+ .size(48.dp)
+ .wrapContentSize(align = Alignment.Center)
+ )
+ },
+ title = { Text(text = "No token", textAlign = TextAlign.Center) },
+ message = {
+ Text(
+ text = "You didn't add a token to the phone app.\n\nPlease add a token, then try again.",
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.body2
+ )
+ },
+ modifier = Modifier.fillMaxSize()
+ ) {}
+}
+
+@Composable
+fun LoadBadTokenPage() {
+ val scrollState = rememberScalingLazyListState()
+ Alert(
+ scrollState = scrollState,
+ verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top),
+ contentPadding =
+ PaddingValues(start = 10.dp, end = 10.dp, top = 24.dp, bottom = 52.dp),
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_sync_problem_48),
+ contentDescription = "sync problem",
+ modifier = Modifier
+ .size(48.dp)
+ .wrapContentSize(align = Alignment.Center)
+ )
+ },
+ title = { Text(text = "PluralKit connection failure", textAlign = TextAlign.Center) },
+ message = {
+ Text(
+ text = "The PluralKit token provided is no longer correct.\n\nPlease update the token in the mobile app, then try again.",
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.body2
+ )
+ },
+ modifier = Modifier.fillMaxSize()
+ ) {}
+}
+
+@Composable
+fun LoadSetupPage() {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ val scrollState = rememberScalingLazyListState()
+ Alert(
+ scrollState = scrollState,
+ verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top),
+ contentPadding =
+ PaddingValues(start = 10.dp, end = 10.dp, top = 24.dp, bottom = 52.dp),
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_app_settings_alt_48),
+ contentDescription = "app settings alt",
+ modifier = Modifier
+ .size(48.dp)
+ .wrapContentSize(align = Alignment.Center)
+ )
+ },
+ title = { Text(text = "Set up Pluralwear", textAlign = TextAlign.Center) },
+ message = {
+ Text(
+ text = "Download the Pluralwear mobile app to initiate setup.",
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.body2
+ )
+ },
+ ) {}
+ }
+}
+
+@Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
+@Composable
+fun DefaultPreview() {
+ WearApp()
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/presentation/PluralKitSyncWorker.kt b/wear/src/main/java/dev/equestria/pluralwear/presentation/PluralKitSyncWorker.kt
new file mode 100644
index 0000000..4e1247a
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/presentation/PluralKitSyncWorker.kt
@@ -0,0 +1,163 @@
+package dev.equestria.pluralwear.presentation
+
+import android.content.Context
+import android.util.Log
+import androidx.work.Data
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import androidx.work.workDataOf
+import dev.equestria.pluralwear.pluralkt.PkAuthorizationException
+import dev.equestria.pluralwear.pluralkt.PluralKt
+import dev.equestria.pluralwear.pluralkt.fulltypes.PkFullSystem
+import dev.equestria.pluralwear.pluralkt.types.PkError
+import dev.equestria.pluralwear.pluralkt.types.PkFronter
+import dev.equestria.pluralwear.pluralkt.types.PkGroup
+import dev.equestria.pluralwear.pluralkt.types.PkMember
+import dev.equestria.pluralwear.pluralkt.types.PkSystem
+import dev.equestria.pluralwear.pluralkt.types.PkSystemSettings
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+
+class PluralKitSyncWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams){
+ override fun doWork(): Result {
+ Log.i("PluralKit Sync", "Syncing with PluralKit API")
+
+ if (PluralKt.tokenExists()) {
+ var sys: PkSystem? = null
+ var settings: PkSystemSettings? = null
+ var members: Array<PkMember>? = null
+ var groups: Array<PkGroup>? = null
+ var front: PkFronter? = null
+
+ val startTime = Clock.System.now()
+
+ try {
+ Log.d("PluralKit Sync", "Pulling system information from API")
+ PluralKt.System.getSystem("@me") {
+ if (!this.isError() && !this.isSuccess()) {
+ Log.w(
+ "PluralKit Sync",
+ "Internal error within the PluralKt library. Intensitve debugging is required."
+ )
+ throw NullPointerException("An internal error occurred with the PluralKt library.")
+ } else if (this.isError()) {
+ handleErrors(this.getError())
+ } else {
+ Log.d("PluralKit Sync", "Got system")
+ sys = this.getSuccess()
+
+ checkForSync(startTime, sys, settings, members, groups, front)
+ }
+ }
+ PluralKt.System.getSystemSettings("@me") {
+ if (!this.isError() && !this.isSuccess()) {
+ throw NullPointerException("An internal error occurred with the PluralKt library.")
+ } else if (this.isError()) {
+ handleErrors(this.getError())
+ } else {
+ Log.d("PluralKit Sync", "Got settings")
+ settings = this.getSuccess()
+
+ checkForSync(startTime, sys, settings, members, groups, front)
+ }
+ }
+ PluralKt.Member.getMembers("@me") {
+ if (!this.isError() && !this.isSuccess()) {
+ throw NullPointerException("An internal error occurred with the PluralKt library.")
+ } else if (this.isError()) {
+ handleErrors(this.getError())
+ } else {
+ Log.d("PluralKit Sync", "Got members")
+ members = this.getSuccess()
+
+ checkForSync(startTime, sys, settings, members, groups, front)
+ }
+ }
+ PluralKt.Group.getGroups("@me") {
+ if (!this.isError() && !this.isSuccess()) {
+ throw NullPointerException("An internal error occurred with the PluralKt library.")
+ } else if (this.isError()) {
+ handleErrors(this.getError())
+ } else {
+ Log.d("PluralKit Sync", "Got group")
+ groups = this.getSuccess()
+
+ checkForSync(startTime, sys, settings, members, groups, front)
+ }
+ }
+ PluralKt.Switch.getFronters("@me") {
+ if (!this.isError() && !this.isSuccess()) {
+ throw NullPointerException("An internal error occurred with the PluralKt library.")
+ } else if (this.isError()) {
+ handleErrors(this.getError())
+ } else {
+ Log.d("PluralKit Sync", "Got fronters")
+ front = this.getSuccess()
+
+ checkForSync(startTime, sys, settings, members, groups, front)
+ }
+ }
+ } catch (ex: NullPointerException) {
+ Log.e("PluralKit Sync", "An internal exception was thrown within the PluralKt Library. Intensitve debugging is required.")
+ } catch (ex: PkAuthorizationException) {
+ Log.w("PluralKit Sync", "Invalid token was provided. Cannot request information from the PluralKit API.")
+ executeSync(null)
+ }
+ }
+
+ return Result.success()
+ }
+
+ private fun handleErrors(error: PkError) {
+ if (error.message.startsWith("401")) {
+ throw PkAuthorizationException(error.message.substring(6))
+ } else {
+ throw Exception(error.message)
+ }
+ }
+
+ private fun checkForSync(startTime: Instant, sys: PkSystem?, settings: PkSystemSettings?, members: Array<PkMember>?, groups: Array<PkGroup>?, front: PkFronter?) {
+ if (sys != null && settings != null && members != null && groups != null && front != null) {
+ val apiTime = Clock.System.now()
+ Log.i(
+ "PluralKit Sync",
+ "Completed API-bound requests in " + (apiTime.toEpochMilliseconds() - startTime.toEpochMilliseconds()) + "ms."
+ )
+ Log.d(
+ "PluralKit Sync",
+ "Creating full system object and getting images on a separate thread."
+ )
+ val system = PkFullSystem(sys, settings, members, groups, front)
+ val fullTime = Clock.System.now()
+ Log.i(
+ "PluralKit Sync",
+ "Completed full system object creation in " + (fullTime.toEpochMilliseconds() - apiTime.toEpochMilliseconds()) + "ms after API, " + (fullTime.toEpochMilliseconds() - startTime.toEpochMilliseconds()) + "ms overall."
+ )
+
+ executeSync(system)
+ }
+ }
+
+ private fun executeSync(system: PkFullSystem?) {
+ val toRun = syncRunners
+ toRun.forEach {
+ it.execute.invoke(system)
+ if (it.once)
+ syncRunners.remove(it)
+ }
+ }
+}
+
+private var syncRunners: ArrayList<PluralKitSyncData> = arrayListOf()
+
+fun onPluralKitSync(once: Boolean = false, execute: (PkFullSystem?) -> Unit) {
+ syncRunners.add(PluralKitSyncData(execute, once))
+}
+
+class PluralKitSyncData(execute: (PkFullSystem?) -> Unit, once: Boolean) {
+ val execute: (PkFullSystem?) -> Unit = execute
+ val once: Boolean = once
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/home/HomeActivity.kt b/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/home/HomeActivity.kt
new file mode 100644
index 0000000..d98d50b
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/home/HomeActivity.kt
@@ -0,0 +1,133 @@
+package dev.equestria.pluralwear.presentation.activity.home
+
+import dev.equestria.pluralwear.pluralkt.fulltypes.PkFullSystem
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.ExperimentalUnitApi
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
+import androidx.navigation.NavHostController
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.TimeText
+import dev.equestria.pluralwear.R
+import dev.equestria.pluralwear.components.ScalingLazyColumnWithRSB
+import java.util.Calendar
+
+class HomeActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ HomePage(null, null)
+ }
+ }
+}
+
+@Composable
+fun HomePage(system: PkFullSystem?, navigator: NavHostController?) {
+ TimeText()
+
+ ScalingLazyColumnWithRSB(
+ modifier = Modifier.fillMaxWidth(),
+ snap = true
+ ) {
+ item {
+ if (system != null) {
+ Greeting(
+ //if (system.name != null) system.name!! else "No Name"
+ if (system.front != null) {
+ if (system.front!!.members.isEmpty()) {
+ if (system.name != null) system.name!! else "No Name"
+ } else if (system.front!!.members.size <= 2) {
+ var name = system.front!!.members
+ name.joinToString(", ") {
+ if(it.displayName != null) it.displayName!! else it.name
+ }
+ } else {
+ if (system.name != null) system.name!! else "No Name"
+ }
+ } else {
+ if (system.name != null) system.name!! else "No Name"
+ }
+ )
+ } else {
+ Greeting(null)
+ }
+ }
+
+ item {
+ Chip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { navigator?.navigate("switch") },
+ label = { Text("Register switch") },
+ colors = ChipDefaults.secondaryChipColors(),
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_switch_account_24),
+ contentDescription = "Switch",
+ modifier = Modifier
+ .size(ChipDefaults.IconSize)
+ .wrapContentSize(align = Alignment.Center),
+ tint = Color.Unspecified
+ )
+ }
+ )
+ }
+ }
+}
+
+
+
+@OptIn(ExperimentalUnitApi::class)
+@Composable
+fun Greeting(greetingName: String?) {
+ val greeting = when (Calendar.getInstance().get(Calendar.HOUR_OF_DAY)) {
+ 6, 7, 8, 9, 10, 11 -> stringResource(R.string.greeting_morning)
+ 12, 13, 14, 15, 16 -> stringResource(R.string.greeting_afternoon)
+ else -> stringResource(R.string.greeting_evening)
+ }
+
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colors.onSurface,
+ text = stringResource(R.string.greeting, greeting)
+ )
+ if (greetingName != null) {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colors.onSurfaceVariant,
+ text = greetingName,
+ fontSize = TextUnit(1.9F, TextUnitType.Em)
+ )
+ }
+ }
+}
+
+@Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
+@Composable
+fun DefaultPreview() {
+ HomePage(null, null)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/loading/LoadingActivity.kt b/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/loading/LoadingActivity.kt
new file mode 100644
index 0000000..3ab8d7d
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/loading/LoadingActivity.kt
@@ -0,0 +1,141 @@
+package dev.equestria.pluralwear.presentation.activity.loading
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.ExperimentalUnitApi
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import androidx.wear.compose.material.Button
+import androidx.wear.compose.material.ButtonDefaults
+import androidx.wear.compose.material.CircularProgressIndicator
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.dialog.Alert
+import androidx.wear.compose.material.dialog.Dialog
+import androidx.wear.compose.material.rememberScalingLazyListState
+import dev.equestria.pluralwear.R
+
+class LoadingActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ LoadingSystemPage()
+ }
+ }
+}
+
+@OptIn(ExperimentalUnitApi::class)
+@Composable
+fun LoadingSystemPage() {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth().padding(5.dp),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colors.onBackground,
+ text = stringResource(R.string.load_system)
+ )
+ CircularProgressIndicator(
+ modifier = Modifier.fillMaxWidth(),
+ startAngle = 270.0f,
+ indicatorColor = MaterialTheme.colors.onBackground,
+ trackColor = MaterialTheme.colors.background
+ )
+ Text(
+ modifier = Modifier.fillMaxWidth().padding(5.dp),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colors.onBackground,
+ text = stringResource(R.string.please_wait),
+ fontSize = TextUnit(2f, TextUnitType.Em)
+ )
+ }
+}
+
+@OptIn(ExperimentalUnitApi::class)
+@Composable
+fun LoadingAppPage() {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth().padding(5.dp),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colors.onBackground,
+ text = stringResource(R.string.wait_app)
+ )
+ CircularProgressIndicator(
+ modifier = Modifier.fillMaxWidth(),
+ startAngle = 270.0f,
+ indicatorColor = MaterialTheme.colors.onBackground,
+ trackColor = MaterialTheme.colors.background
+ )
+ Text(
+ modifier = Modifier.fillMaxWidth().padding(5.dp),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colors.onBackground,
+ text = stringResource(R.string.please_wait),
+ fontSize = TextUnit(2f, TextUnitType.Em)
+ )
+ }
+}
+
+@Composable
+fun LoadingDialog(
+ showDialog: Boolean
+) {
+ val dialogScrollState = rememberScalingLazyListState()
+ Dialog(
+ showDialog = showDialog,
+ onDismissRequest = {},
+ scrollState = dialogScrollState
+ ) {
+ Alert(
+ scrollState = dialogScrollState,
+ verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top),
+ contentPadding =
+ PaddingValues(start = 10.dp, end = 10.dp, top = 24.dp, bottom = 52.dp),
+ icon = {
+ CircularProgressIndicator(
+ modifier = Modifier.fillMaxWidth(),
+ startAngle = 270.0f,
+ indicatorColor = MaterialTheme.colors.onBackground,
+ trackColor = MaterialTheme.colors.background
+ )
+ },
+ title = { Text(text = "Registering switch", textAlign = TextAlign.Center) },
+ message = {
+ Text(text = "Please wait...", textAlign = TextAlign.Center, style = MaterialTheme.typography.body2)
+ }
+ ) {
+ }
+ }
+}
+
+@Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
+@Composable
+fun DefaultPreview() {
+ LoadingSystemPage()
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/register_switch/MemberUiState.kt b/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/register_switch/MemberUiState.kt
new file mode 100644
index 0000000..c309e11
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/register_switch/MemberUiState.kt
@@ -0,0 +1,15 @@
+package dev.equestria.pluralwear.presentation.activity.register_switch
+
+import androidx.compose.runtime.toMutableStateList
+import dev.equestria.pluralwear.pluralkt.fulltypes.PkFullMember
+
+class MemberUiState(
+ initialMembers: List<PkFullMember> = listOf()
+) {
+ private val _members: MutableList<PkFullMember> = initialMembers.toMutableStateList()
+ val members: List<PkFullMember> = _members
+
+ fun addMember(member: PkFullMember) {
+ _members.add(member)
+ }
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/register_switch/SwitchActivity.kt b/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/register_switch/SwitchActivity.kt
new file mode 100644
index 0000000..3c4d07b
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/presentation/activity/register_switch/SwitchActivity.kt
@@ -0,0 +1,195 @@
+package dev.equestria.pluralwear.presentation.activity.register_switch
+
+import android.content.res.Resources.NotFoundException
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import androidx.wear.compose.material.Button
+import androidx.wear.compose.material.ButtonDefaults
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.dialog.Alert
+import androidx.wear.compose.material.dialog.Dialog
+import androidx.wear.compose.material.rememberScalingLazyListState
+import dev.equestria.pluralwear.R
+import dev.equestria.pluralwear.pluralkt.PluralKt
+import dev.equestria.pluralwear.pluralkt.fulltypes.PkFullSystem
+import dev.equestria.pluralwear.pluralkt.types.SwitchCreate
+import dev.equestria.pluralwear.presentation.activity.loading.LoadingDialog
+import dev.equestria.pluralwear.components.MultipleMemberList
+import dev.equestria.pluralwear.presentation.theme.PluralwearTheme
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class HomeActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ SwitchPage(null, null)
+ }
+ }
+}
+
+@Composable
+fun SwitchPage(system: PkFullSystem?, navController: NavHostController?) {
+ var showLoadingDialog by remember { mutableStateOf(false) }
+ var showFailureDialog by remember { mutableStateOf(false) }
+ var showSuccessDialog by remember { mutableStateOf(false) }
+
+ val uiState = MemberUiState()
+ val coroutineScope = rememberCoroutineScope()
+
+ if(system?.members != null) {
+ for(member in system.members!!) {
+ uiState.addMember(member)
+ }
+ }
+
+ PluralwearTheme {
+ MultipleMemberList(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colors.background),
+ members = uiState.members
+ ) {
+ coroutineScope.launch {
+ showLoadingDialog = true
+ val switch = SwitchCreate()
+
+ for (member in it) {
+ switch.members.add(member.id)
+ }
+
+ withContext(Dispatchers.IO) {
+ try {
+ val data = PluralKt.Switch.createSwitch(switch).get()
+ showSuccessDialog = true
+ } catch (ex: NotFoundException) {
+ Log.e("Pluralwear", "Couldn't find system?")
+ showFailureDialog = true
+ } catch (ex: UnknownError) {
+ Log.e("Pluralwear", "PluralKit error: " + ex.message)
+ showFailureDialog = true
+ }
+ }
+ showLoadingDialog = false
+ }
+ }
+ }
+
+ LoadingDialog(showLoadingDialog)
+
+ val failureDialogScrollState = rememberScalingLazyListState()
+ Dialog(
+ showDialog = showFailureDialog,
+ onDismissRequest = { showFailureDialog = false },
+ scrollState = failureDialogScrollState
+ ) {
+ Alert(
+ scrollState = failureDialogScrollState,
+ verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top),
+ contentPadding =
+ PaddingValues(start = 10.dp, end = 10.dp, top = 24.dp, bottom = 52.dp),
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_warning_48),
+ contentDescription = "warning",
+ modifier = Modifier
+ .size(48.dp)
+ .wrapContentSize(align = Alignment.Center)
+ )
+ },
+ title = { Text(text = "Something went wrong", textAlign = TextAlign.Center) },
+ message = {
+ Text(
+ text = "An unknown error has occurred, we're sorry for the inconvenience\n\nIf you have telemetry enabled, an error dump has been sent to Equestria.dev.",
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.body2
+ )
+ },
+ ) {
+ item {
+ Button(
+ onClick = { showFailureDialog = false },
+ colors = ButtonDefaults.primaryButtonColors()
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_arrow_back_24),
+ contentDescription = "Close"
+ )
+ }
+ }
+ }
+ }
+
+ val successDialogScrollState = rememberScalingLazyListState()
+ Dialog(
+ showDialog = showSuccessDialog,
+ onDismissRequest = {
+ showSuccessDialog = false
+ navController?.navigate("home")
+ },
+ scrollState = successDialogScrollState
+ ) {
+ Alert(
+ scrollState = successDialogScrollState,
+ verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Top),
+ contentPadding =
+ PaddingValues(start = 10.dp, end = 10.dp, top = 24.dp, bottom = 52.dp),
+ icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_check_48),
+ contentDescription = "check",
+ modifier = Modifier
+ .size(48.dp)
+ .wrapContentSize(align = Alignment.Center)
+ )
+ },
+ title = { Text(text = "Successfully registered switch", textAlign = TextAlign.Center) },
+ ) {
+ item {
+ Button(
+ onClick = {
+ showSuccessDialog = false
+ navController?.navigate("home")
+ },
+ colors = ButtonDefaults.primaryButtonColors()
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_home_24),
+ contentDescription = "Home"
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
+@Composable
+fun DefaultPreview() {
+ SwitchPage(null, null)
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Color.kt b/wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Color.kt
new file mode 100644
index 0000000..19a4179
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Color.kt
@@ -0,0 +1,21 @@
+package dev.equestria.pluralwear.presentation.theme
+
+import androidx.compose.ui.graphics.Color
+import androidx.wear.compose.material.Colors
+
+val Purple200 = Color(0xFFBB86FC)
+val Purple500 = Color(0xFF6200EE)
+val Purple700 = Color(0xFF3700B3)
+val Teal200 = Color(0xFF03DAC5)
+val Red400 = Color(0xFFCF6679)
+
+internal val wearColorPalette: Colors = Colors(
+ primary = Purple200,
+ primaryVariant = Purple700,
+ secondary = Teal200,
+ secondaryVariant = Teal200,
+ error = Red400,
+ onPrimary = Color.Black,
+ onSecondary = Color.Black,
+ onError = Color.Black
+) \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Theme.kt b/wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Theme.kt
new file mode 100644
index 0000000..8428955
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Theme.kt
@@ -0,0 +1,17 @@
+package dev.equestria.pluralwear.presentation.theme
+
+import androidx.compose.runtime.Composable
+import androidx.wear.compose.material.MaterialTheme
+
+@Composable
+fun PluralwearTheme(
+ content: @Composable () -> Unit
+) {
+ MaterialTheme(
+ colors = wearColorPalette,
+ typography = Typography,
+ // For shapes, we generally recommend using the default Material Wear shapes which are
+ // optimized for round and non-round devices.
+ content = content
+ )
+} \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Type.kt b/wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Type.kt
new file mode 100644
index 0000000..b25a75e
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/presentation/theme/Type.kt
@@ -0,0 +1,28 @@
+package dev.equestria.pluralwear.presentation.theme
+
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import androidx.wear.compose.material.Typography
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ body1 = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp
+ )
+ /* Other default text styles to override
+ button = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ fontSize = 14.sp
+ ),
+ caption = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp
+ )
+ */
+) \ No newline at end of file
diff --git a/wear/src/main/java/dev/equestria/pluralwear/tile/MainTileService.kt b/wear/src/main/java/dev/equestria/pluralwear/tile/MainTileService.kt
new file mode 100644
index 0000000..c290b98
--- /dev/null
+++ b/wear/src/main/java/dev/equestria/pluralwear/tile/MainTileService.kt
@@ -0,0 +1,73 @@
+package dev.equestria.pluralwear.tile
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.wear.tiles.DimensionBuilders.expand
+import androidx.wear.tiles.DimensionBuilders.wrap
+import androidx.wear.tiles.LayoutElementBuilders
+import androidx.wear.tiles.LayoutElementBuilders.Column
+import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
+import androidx.wear.tiles.RequestBuilders
+import androidx.wear.tiles.ResourceBuilders
+import androidx.wear.tiles.TileBuilders
+import androidx.wear.tiles.TimelineBuilders
+import androidx.wear.tiles.material.Button
+import androidx.wear.tiles.material.Text
+import androidx.wear.tiles.material.Typography
+import androidx.wear.tiles.material.layouts.PrimaryLayout
+import com.google.android.horologist.compose.tools.LayoutRootPreview
+import com.google.android.horologist.compose.tools.buildDeviceParameters
+import com.google.android.horologist.tiles.CoroutinesTileService
+import dev.equestria.pluralwear.presentation.system
+
+
+private const val RESOURCES_VERSION = "0"
+
+/**
+ * Skeleton for a tile with no images.
+ */
+class MainTileService : CoroutinesTileService() {
+
+ override suspend fun resourcesRequest(
+ requestParams: RequestBuilders.ResourcesRequest
+ ): ResourceBuilders.Resources {
+ return ResourceBuilders.Resources.Builder().setVersion(RESOURCES_VERSION).build()
+ }
+
+ override suspend fun tileRequest(
+ requestParams: RequestBuilders.TileRequest
+ ): TileBuilders.Tile {
+ val singleTileTimeline = TimelineBuilders.Timeline.Builder().addTimelineEntry(
+ TimelineBuilders.TimelineEntry.Builder().setLayout(
+ LayoutElementBuilders.Layout.Builder().setRoot(tileLayout(this)).build()
+ ).build()
+ ).build()
+
+ return TileBuilders.Tile.Builder().setResourcesVersion(RESOURCES_VERSION)
+ .setTimeline(singleTileTimeline).build()
+ }
+}
+
+private fun tileLayout(context: Context): LayoutElementBuilders.LayoutElement {
+ return PrimaryLayout.Builder(buildDeviceParameters(context.resources))
+ .setContent(
+ Text.Builder(context, if(system != null) "i exist!" else "i don't exist")
+ .setTypography(Typography.TYPOGRAPHY_CAPTION1)
+ .build()
+ ).build()
+}
+
+@Preview(
+ device = Devices.WEAR_OS_SMALL_ROUND,
+ showSystemUi = true,
+ backgroundColor = 0xff000000,
+ showBackground = true
+)
+@Composable
+fun TilePreview() {
+ LayoutRootPreview(root = tileLayout(LocalContext.current))
+} \ No newline at end of file
diff --git a/wear/src/main/res/drawable-round/tile_preview.png b/wear/src/main/res/drawable-round/tile_preview.png
new file mode 100644
index 0000000..474fac4
--- /dev/null
+++ b/wear/src/main/res/drawable-round/tile_preview.png
Binary files differ
diff --git a/wear/src/main/res/drawable/baseline_app_settings_alt_48.xml b/wear/src/main/res/drawable/baseline_app_settings_alt_48.xml
new file mode 100644
index 0000000..b4e6f1e
--- /dev/null
+++ b/wear/src/main/res/drawable/baseline_app_settings_alt_48.xml
@@ -0,0 +1,5 @@
+<vector android:height="48dp" android:tint="#FFFFFF"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M21.81,12.74l-0.82,-0.63v-0.22l0.8,-0.63c0.16,-0.12 0.2,-0.34 0.1,-0.51l-0.85,-1.48c-0.07,-0.13 -0.21,-0.2 -0.35,-0.2 -0.05,0 -0.1,0.01 -0.15,0.03l-0.95,0.38c-0.08,-0.05 -0.11,-0.07 -0.19,-0.11l-0.15,-1.01c-0.03,-0.21 -0.2,-0.36 -0.4,-0.36h-1.71c-0.2,0 -0.37,0.15 -0.4,0.34l-0.14,1.01c-0.03,0.02 -0.07,0.03 -0.1,0.05l-0.09,0.06 -0.95,-0.38c-0.05,-0.02 -0.1,-0.03 -0.15,-0.03 -0.14,0 -0.27,0.07 -0.35,0.2l-0.85,1.48c-0.1,0.17 -0.06,0.39 0.1,0.51l0.8,0.63v0.23l-0.8,0.63c-0.16,0.12 -0.2,0.34 -0.1,0.51l0.85,1.48c0.07,0.13 0.21,0.2 0.35,0.2 0.05,0 0.1,-0.01 0.15,-0.03l0.95,-0.37c0.08,0.05 0.12,0.07 0.2,0.11l0.15,1.01c0.03,0.2 0.2,0.34 0.4,0.34h1.71c0.2,0 0.37,-0.15 0.4,-0.34l0.15,-1.01c0.03,-0.02 0.07,-0.03 0.1,-0.05l0.09,-0.06 0.95,0.38c0.05,0.02 0.1,0.03 0.15,0.03 0.14,0 0.27,-0.07 0.35,-0.2l0.85,-1.48c0.1,-0.17 0.06,-0.39 -0.1,-0.51zM18,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM17,17h2v4c0,1.1 -0.9,2 -2,2H7c-1.1,0 -2,-0.9 -2,-2V3c0,-1.1 0.9,-2 2,-2h10c1.1,0 2,0.9 2,2v4h-2V6H7v12h10v-1z"/>
+</vector>
diff --git a/wear/src/main/res/drawable/baseline_arrow_back_24.xml b/wear/src/main/res/drawable/baseline_arrow_back_24.xml
new file mode 100644
index 0000000..31e7df2
--- /dev/null
+++ b/wear/src/main/res/drawable/baseline_arrow_back_24.xml
@@ -0,0 +1,5 @@
+<vector android:autoMirrored="true" android:height="24dp"
+ android:tint="#FFFFFF" android:viewportHeight="24"
+ android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
+</vector>
diff --git a/wear/src/main/res/drawable/baseline_check_24.xml b/wear/src/main/res/drawable/baseline_check_24.xml
new file mode 100644
index 0000000..2501e9f
--- /dev/null
+++ b/wear/src/main/res/drawable/baseline_check_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>
diff --git a/wear/src/main/res/drawable/baseline_check_48.xml b/wear/src/main/res/drawable/baseline_check_48.xml
new file mode 100644
index 0000000..bf13aaa
--- /dev/null
+++ b/wear/src/main/res/drawable/baseline_check_48.xml
@@ -0,0 +1,5 @@
+<vector android:height="48dp" android:tint="#FFFFFF"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>
diff --git a/wear/src/main/res/drawable/baseline_home_24.xml b/wear/src/main/res/drawable/baseline_home_24.xml
new file mode 100644
index 0000000..4c5e854
--- /dev/null
+++ b/wear/src/main/res/drawable/baseline_home_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
+</vector>
diff --git a/wear/src/main/res/drawable/baseline_switch_account_24.xml b/wear/src/main/res/drawable/baseline_switch_account_24.xml
new file mode 100644
index 0000000..5731e87
--- /dev/null
+++ b/wear/src/main/res/drawable/baseline_switch_account_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM14,4c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM20,16L8,16v-1.5c0,-1.99 4,-3 6,-3s6,1.01 6,3L20,16z"/>
+</vector>
diff --git a/wear/src/main/res/drawable/baseline_sync_problem_48.xml b/wear/src/main/res/drawable/baseline_sync_problem_48.xml
new file mode 100644
index 0000000..22f4528
--- /dev/null
+++ b/wear/src/main/res/drawable/baseline_sync_problem_48.xml
@@ -0,0 +1,5 @@
+<vector android:height="48dp" android:tint="#FFFFFF"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M3,12c0,2.21 0.91,4.2 2.36,5.64L3,20h6v-6l-2.24,2.24C5.68,15.15 5,13.66 5,12c0,-2.61 1.67,-4.83 4,-5.65L9,4.26C5.55,5.15 3,8.27 3,12zM11,17h2v-2h-2v2zM21,4h-6v6l2.24,-2.24C18.32,8.85 19,10.34 19,12c0,2.61 -1.67,4.83 -4,5.65v2.09c3.45,-0.89 6,-4.01 6,-7.74 0,-2.21 -0.91,-4.2 -2.36,-5.64L21,4zM11,13h2L13,7h-2v6z"/>
+</vector>
diff --git a/wear/src/main/res/drawable/baseline_warning_24.xml b/wear/src/main/res/drawable/baseline_warning_24.xml
new file mode 100644
index 0000000..3c9a4b3
--- /dev/null
+++ b/wear/src/main/res/drawable/baseline_warning_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
+</vector>
diff --git a/wear/src/main/res/drawable/baseline_warning_48.xml b/wear/src/main/res/drawable/baseline_warning_48.xml
new file mode 100644
index 0000000..c67884b
--- /dev/null
+++ b/wear/src/main/res/drawable/baseline_warning_48.xml
@@ -0,0 +1,5 @@
+<vector android:height="48dp" android:tint="#FFFFFF"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
+</vector>
diff --git a/wear/src/main/res/drawable/ic_launcher_background.xml b/wear/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..13b174c
--- /dev/null
+++ b/wear/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,17 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="256"
+ android:viewportHeight="256">
+ <group android:scaleX="0.9"
+ android:scaleY="0.9"
+ android:translateX="12.8"
+ android:translateY="12.8">
+ <path
+ android:pathData="M128,128m-128,0a128,128 0,1 1,256 0a128,128 0,1 1,-256 0"
+ android:fillColor="#673A3B"/>
+ <path
+ android:pathData="M0,0h256v256h-256z"
+ android:fillColor="#673A3B"/>
+ </group>
+</vector>
diff --git a/wear/src/main/res/drawable/ic_launcher_foreground.xml b/wear/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..0253566
--- /dev/null
+++ b/wear/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,17 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="256"
+ android:viewportHeight="256">
+ <group android:scaleX="0.76153845"
+ android:scaleY="0.76153845"
+ android:translateX="30.523077"
+ android:translateY="30.523077">
+ <path
+ android:pathData="M103.3,219.1l-10,-36.4c-9,-5.2 -16.2,-12.6 -21.6,-22.2c-5.4,-9.6 -8.1,-20.4 -8.1,-32.2c0,-11.8 2.7,-22.5 8.1,-32.2c5.4,-9.6 12.6,-17 21.6,-22.2l10,-36.4h50.4l10,36.4c9,5.2 16.2,12.6 21.6,22.2c5.4,9.6 8.1,20.4 8.1,32.2c0,11.8 -2.7,22.5 -8.1,32.2c-5.4,9.6 -12.6,17 -21.6,22.2l-10,36.4H103.3zM128.4,182c15,0 27.6,-5.2 38,-15.6c10.4,-10.4 15.6,-23.1 15.6,-38c0,-15 -5.2,-27.6 -15.6,-38s-23.1,-15.6 -38,-15.6s-27.6,5.2 -38,15.6c-10.4,10.4 -15.6,23.1 -15.6,38c0,15 5.2,27.6 15.6,38C100.8,176.8 113.5,182 128.4,182zM107.2,67.5c3.8,-1.2 7.4,-2 10.8,-2.6c3.4,-0.6 6.9,-0.9 10.3,-0.9c3.5,0 6.9,0.3 10.3,0.9c3.4,0.6 7,1.5 10.8,2.6l-4.5,-18.5h-33.4L107.2,67.5zM111.7,207.7h33.4l4.5,-18.5c-3.8,1 -7.4,1.8 -10.8,2.4c-3.4,0.6 -6.9,0.9 -10.3,0.9c-3.5,0 -6.9,-0.3 -10.3,-0.9c-3.4,-0.6 -7,-1.4 -10.8,-2.4L111.7,207.7zM107.2,49.1h42.4H107.2zM111.7,207.7h-4.5h42.4h-4.5H111.7z"
+ android:fillColor="#FFDAD9"/>
+ <path
+ android:pathData="M82.4,149.4v-4.2c0,-2.5 1.3,-4.5 4,-6s6.1,-2.3 10.3,-2.3c0.8,0 1.6,0 2.3,0.1c0.7,0.1 1.4,0.1 2,0.2c-0.4,1.1 -0.7,2.1 -1,3.2c-0.3,1.1 -0.4,2.2 -0.4,3.4v5.7H82.4zM106.4,149.4v-5.5c0,-3.9 2,-6.9 6.1,-9.2c4.1,-2.3 9.4,-3.4 15.9,-3.4c6.6,0 11.9,1.1 15.9,3.4c4,2.3 6,5.4 6,9.2v5.5H106.4zM157.2,149.4v-5.7c0,-1.2 -0.1,-2.3 -0.3,-3.4c-0.2,-1.1 -0.6,-2.1 -1,-3.2c0.7,-0.1 1.4,-0.1 2.1,-0.2c0.7,-0.1 1.5,-0.1 2.2,-0.1c4.3,0 7.8,0.8 10.4,2.3c2.6,1.6 3.9,3.6 3.9,6v4.2H157.2zM128.4,135.7c-5.3,0 -9.5,0.8 -12.7,2.3c-3.2,1.5 -4.8,3.5 -4.7,6v0.8h35v-0.9c0.1,-2.4 -1.5,-4.4 -4.7,-5.9C138,136.4 133.8,135.7 128.4,135.7zM96.7,133.7c-1.7,0 -3.1,-0.6 -4.3,-1.8c-1.2,-1.2 -1.8,-2.7 -1.8,-4.4c0,-1.7 0.6,-3.1 1.8,-4.2c1.2,-1.2 2.6,-1.7 4.3,-1.7c1.7,0 3.1,0.6 4.3,1.7c1.2,1.2 1.8,2.6 1.8,4.3c0,1.7 -0.6,3.1 -1.8,4.3C99.8,133.1 98.4,133.7 96.7,133.7zM160.1,133.7c-1.7,0 -3.1,-0.6 -4.3,-1.8s-1.8,-2.7 -1.8,-4.4c0,-1.7 0.6,-3.1 1.8,-4.2c1.2,-1.2 2.6,-1.7 4.3,-1.7c1.7,0 3.2,0.6 4.3,1.7c1.2,1.2 1.7,2.6 1.7,4.3c0,1.7 -0.6,3.1 -1.7,4.3C163.3,133.1 161.9,133.7 160.1,133.7zM128.4,128.2c-2.9,0 -5.3,-1 -7.3,-3c-2,-2 -3,-4.5 -3,-7.3c0,-2.9 1,-5.4 3,-7.4c2,-2 4.5,-3 7.3,-3c2.9,0 5.4,1 7.4,3c2,2 3,4.5 3,7.4c0,2.9 -1,5.3 -3,7.3C133.8,127.1 131.4,128.2 128.4,128.2zM128.4,112c-1.6,0 -3,0.5 -4.1,1.6c-1.1,1.1 -1.7,2.5 -1.7,4.1c0,1.6 0.5,3 1.6,4.1c1.1,1.2 2.5,1.7 4.2,1.7c1.6,0 2.9,-0.6 4,-1.7c1.1,-1.1 1.6,-2.5 1.6,-4.2c0,-1.7 -0.5,-3 -1.6,-4.1C131.5,112.5 130.1,112 128.4,112z"
+ android:fillColor="#FFDAD9"/>
+ </group>
+</vector>
diff --git a/wear/src/main/res/drawable/tile_preview.png b/wear/src/main/res/drawable/tile_preview.png
new file mode 100644
index 0000000..8cbe3ed
--- /dev/null
+++ b/wear/src/main/res/drawable/tile_preview.png
Binary files differ
diff --git a/wear/src/main/res/font/product_sans_bold.ttf b/wear/src/main/res/font/product_sans_bold.ttf
new file mode 100644
index 0000000..d847195
--- /dev/null
+++ b/wear/src/main/res/font/product_sans_bold.ttf
Binary files differ
diff --git a/wear/src/main/res/font/product_sans_bold_italic.ttf b/wear/src/main/res/font/product_sans_bold_italic.ttf
new file mode 100644
index 0000000..129d12d
--- /dev/null
+++ b/wear/src/main/res/font/product_sans_bold_italic.ttf
Binary files differ
diff --git a/wear/src/main/res/font/product_sans_italic.ttf b/wear/src/main/res/font/product_sans_italic.ttf
new file mode 100644
index 0000000..5fc56d4
--- /dev/null
+++ b/wear/src/main/res/font/product_sans_italic.ttf
Binary files differ
diff --git a/wear/src/main/res/font/product_sans_regular.ttf b/wear/src/main/res/font/product_sans_regular.ttf
new file mode 100644
index 0000000..c0442ee
--- /dev/null
+++ b/wear/src/main/res/font/product_sans_regular.ttf
Binary files differ
diff --git a/wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..bbd3e02
--- /dev/null
+++ b/wear/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?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"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/wear/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/wear/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..bbd3e02
--- /dev/null
+++ b/wear/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?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"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/wear/src/main/res/mipmap-hdpi/ic_launcher.png b/wear/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..f9691d5
--- /dev/null
+++ b/wear/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/wear/src/main/res/mipmap-hdpi/ic_launcher_round.png b/wear/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..f9691d5
--- /dev/null
+++ b/wear/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/wear/src/main/res/mipmap-mdpi/ic_launcher.png b/wear/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..48b320d
--- /dev/null
+++ b/wear/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/wear/src/main/res/mipmap-mdpi/ic_launcher_round.png b/wear/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..48b320d
--- /dev/null
+++ b/wear/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/wear/src/main/res/mipmap-xhdpi/ic_launcher.png b/wear/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..d2ab9f2
--- /dev/null
+++ b/wear/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/wear/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/wear/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..d2ab9f2
--- /dev/null
+++ b/wear/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png b/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..51c1dc4
--- /dev/null
+++ b/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..51c1dc4
--- /dev/null
+++ b/wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/wear/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/wear/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..388e2bf
--- /dev/null
+++ b/wear/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..388e2bf
--- /dev/null
+++ b/wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/wear/src/main/res/values-round/strings.xml b/wear/src/main/res/values-round/strings.xml
new file mode 100644
index 0000000..7ce6bd8
--- /dev/null
+++ b/wear/src/main/res/values-round/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="hello_world">From the Round world, Hello, %1$s!</string>
+</resources> \ No newline at end of file
diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml
new file mode 100644
index 0000000..19a38a7
--- /dev/null
+++ b/wear/src/main/res/values/strings.xml
@@ -0,0 +1,19 @@
+<resources>
+ <string name="app_name">Pluralwear</string>
+ <!--
+ This string is used for square devices and overridden by hello_world in
+ values-round/strings.xml for round devices.
+ -->
+ <string name="hello_world">From the Round world, Hello, %1$s!</string>
+ <string name="front_tile_label">Fronter</string>
+ <string name="title_activity_presentation._main">presentation.MainActivity</string>
+ <string name="title_activity_main">MainActivity</string>
+ <string name="complication_label">Example complication</string>
+ <string name="wait_app">Waiting for mobile app…</string>
+ <string name="load_system">Loading System…</string>
+ <string name="please_wait">Please wait.</string>
+ <string name="greeting">Good %1$s</string>
+ <string name="greeting_morning">morning</string>
+ <string name="greeting_afternoon">afternoon</string>
+ <string name="greeting_evening">evening</string>
+</resources> \ No newline at end of file
diff --git a/wear/src/main/res/values/wear.xml b/wear/src/main/res/values/wear.xml
new file mode 100644
index 0000000..7fc962d
--- /dev/null
+++ b/wear/src/main/res/values/wear.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools"
+ tools:keep="@array/android_wear_capabilities">
+ <string-array name="android_wear_capabilities">
+ <item>pluralwear_wear_app</item>
+ </string-array>
+</resources> \ No newline at end of file