diff options
author | Cloudburst <us@conep.one> | 2023-01-22 13:23:59 +0000 |
---|---|---|
committer | Cloudburst <us@conep.one> | 2023-01-22 13:33:33 +0000 |
commit | 47001ee2725669fbc251a943bac63bab1ecbab16 (patch) | |
tree | 0e6fc42baa29160d3ff53e1ef4cd1a9893e1fb78 | |
parent | 18572b0c1540d7ad467373011286180c444e5308 (diff) | |
download | pluralwear-47001ee2725669fbc251a943bac63bab1ecbab16.tar.gz pluralwear-47001ee2725669fbc251a943bac63bab1ecbab16.tar.bz2 pluralwear-47001ee2725669fbc251a943bac63bab1ecbab16.zip |
Init
125 files changed, 4131 insertions, 0 deletions
@@ -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 Binary files differnew file mode 100644 index 0000000..e708b1c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar 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 @@ -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 Binary files differnew file mode 100644 index 0000000..d847195 --- /dev/null +++ b/mobile/src/main/res/font/product_sans_bold.ttf diff --git a/mobile/src/main/res/font/product_sans_bold_italic.ttf b/mobile/src/main/res/font/product_sans_bold_italic.ttf Binary files differnew file mode 100644 index 0000000..129d12d --- /dev/null +++ b/mobile/src/main/res/font/product_sans_bold_italic.ttf diff --git a/mobile/src/main/res/font/product_sans_italic.ttf b/mobile/src/main/res/font/product_sans_italic.ttf Binary files differnew file mode 100644 index 0000000..5fc56d4 --- /dev/null +++ b/mobile/src/main/res/font/product_sans_italic.ttf diff --git a/mobile/src/main/res/font/product_sans_regular.ttf b/mobile/src/main/res/font/product_sans_regular.ttf Binary files differnew file mode 100644 index 0000000..c0442ee --- /dev/null +++ b/mobile/src/main/res/font/product_sans_regular.ttf 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 Binary files differnew file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/mobile/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/mobile/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/mobile/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..62b611d --- /dev/null +++ b/mobile/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..948a307 --- /dev/null +++ b/mobile/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/mobile/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..28d4b77 --- /dev/null +++ b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..9287f50 --- /dev/null +++ b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..aa7d642 --- /dev/null +++ b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..9126ae3 --- /dev/null +++ b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp 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 Binary files differnew file mode 100644 index 0000000..0fe5924 --- /dev/null +++ b/wear/src/main/ic_launcher-playstore.png 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 Binary files differnew file mode 100644 index 0000000..474fac4 --- /dev/null +++ b/wear/src/main/res/drawable-round/tile_preview.png 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 Binary files differnew file mode 100644 index 0000000..8cbe3ed --- /dev/null +++ b/wear/src/main/res/drawable/tile_preview.png diff --git a/wear/src/main/res/font/product_sans_bold.ttf b/wear/src/main/res/font/product_sans_bold.ttf Binary files differnew file mode 100644 index 0000000..d847195 --- /dev/null +++ b/wear/src/main/res/font/product_sans_bold.ttf diff --git a/wear/src/main/res/font/product_sans_bold_italic.ttf b/wear/src/main/res/font/product_sans_bold_italic.ttf Binary files differnew file mode 100644 index 0000000..129d12d --- /dev/null +++ b/wear/src/main/res/font/product_sans_bold_italic.ttf diff --git a/wear/src/main/res/font/product_sans_italic.ttf b/wear/src/main/res/font/product_sans_italic.ttf Binary files differnew file mode 100644 index 0000000..5fc56d4 --- /dev/null +++ b/wear/src/main/res/font/product_sans_italic.ttf diff --git a/wear/src/main/res/font/product_sans_regular.ttf b/wear/src/main/res/font/product_sans_regular.ttf Binary files differnew file mode 100644 index 0000000..c0442ee --- /dev/null +++ b/wear/src/main/res/font/product_sans_regular.ttf 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 Binary files differnew file mode 100644 index 0000000..f9691d5 --- /dev/null +++ b/wear/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/wear/src/main/res/mipmap-hdpi/ic_launcher_round.png b/wear/src/main/res/mipmap-hdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000..f9691d5 --- /dev/null +++ b/wear/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/wear/src/main/res/mipmap-mdpi/ic_launcher.png b/wear/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..48b320d --- /dev/null +++ b/wear/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/wear/src/main/res/mipmap-mdpi/ic_launcher_round.png b/wear/src/main/res/mipmap-mdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000..48b320d --- /dev/null +++ b/wear/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/wear/src/main/res/mipmap-xhdpi/ic_launcher.png b/wear/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..d2ab9f2 --- /dev/null +++ b/wear/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/wear/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/wear/src/main/res/mipmap-xhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000..d2ab9f2 --- /dev/null +++ b/wear/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png b/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..51c1dc4 --- /dev/null +++ b/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000..51c1dc4 --- /dev/null +++ b/wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/wear/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/wear/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..388e2bf --- /dev/null +++ b/wear/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000..388e2bf --- /dev/null +++ b/wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png 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 |