Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 216204fac5 | |||
| 01e01dcd36 | |||
| 6f77eb0881 | |||
| 15949b6405 | |||
| 83d304e215 | |||
| 240e23743b | |||
| 3a99049165 | |||
| 9373aecc86 | |||
| 50d79cfc40 | |||
| 4d2f363f8a | |||
| 2b0109468c | |||
| ed86c83d82 | |||
| b900123a09 | |||
| 580b8bdc2f | |||
| 81363beb47 | |||
| 3a214d2589 | |||
| 664d150ca4 | |||
| 095e332ccb | |||
| 33718976f9 | |||
| 7089e480ae | |||
| 2313b3cded | |||
| b0c5a2f458 | |||
| ba0caaade3 | |||
| fa69fc92a2 | |||
| 492320e4d6 | |||
| 00e55b44cb | |||
| 3ad22b4a9c | |||
| 3b735645e0 | |||
| ed1fdd34df | |||
| dba4c6c7d0 | |||
| 75c9ef4a85 | |||
| bd5b5704d1 | |||
| 885a3064eb | |||
| 9385eb908f | |||
| 656930cb7c | |||
| 2888765b39 | |||
| 23635612a1 | |||
| f82ac09fd3 | |||
| 5378764e37 | |||
| 91e58ab328 | |||
| a267d00d59 | |||
| 5b0a0a80f3 | |||
| 1e44b0548c | |||
| 64319e16d9 | |||
| e4d25a2dde | |||
| d36358fa7f | |||
| fcf280dfca | |||
| b98c99a2da | |||
| 0a50eb6dd8 | |||
| fcf3c6d312 | |||
| 1b8ba32c3f | |||
| 57e5eca0f9 | |||
| 65eeea22e8 | |||
| e55736d4d5 | |||
| 53a3616d5c | |||
| 7c626b4f82 | |||
| 3cd18e883d | |||
| da9fb487d2 | |||
| d797af3c57 | |||
| 4231bc6da1 | |||
| 803696e2a8 | |||
| ba88ea0a0a | |||
| 97f1fc6fae | |||
| 5529e5422c | |||
| 8daa15f24c | |||
| 3bb39b0027 | |||
| ddc47e8679 | |||
| 3c1c05d40e | |||
| 98025265a8 | |||
| dea555d5c2 | |||
| 8183907366 | |||
| 0de6aeef80 | |||
| b41c95320e | |||
| 2437c52faf | |||
| e5772a9705 | |||
| 40e2ba1f7b | |||
| 2b9e54f485 | |||
| 253c7641da | |||
| ab95dbfe46 | |||
| 6e82749142 | |||
| 31b224190b | |||
| b8aedfc28c | |||
| 4a601dae66 | |||
| b263824429 | |||
| f1b8b794d3 | |||
| dcd94fc25c | |||
| bf2be6442a | |||
| 550b0356fb | |||
| d67ba1671e | |||
| 782ebe8200 | |||
| 6ecf5f91d9 | |||
| 32a29bab38 | |||
| 6b0302a91d | |||
| 0b88dfe371 | |||
| 39578d4c61 | |||
| 8a90b216fd | |||
| 76e9daa384 | |||
| 7c893f0ae7 | |||
| 2bda42b36d | |||
| 6aed3cf6b4 | |||
| 9d4f4c212b | |||
| d84d65f28f | |||
| f0c32d408d | |||
| ddc8a459e0 | |||
| 75c3a6fe23 | |||
| e8da4650db | |||
| 7f2a34a99d | |||
| b564b1a836 | |||
| 4be170e203 | |||
| cb7845499d | |||
| e9387de879 | |||
| 9c21cb61e3 | |||
| ff9844ae61 | |||
| fdfd5b0985 | |||
| 58a7fe300c | |||
| 50b26b190e | |||
| b7fce3bf31 | |||
| e84fb87cd0 | |||
| 30f7a61b7e | |||
| 438bcf922e | |||
| a42093a0f0 | |||
| b4e2c4ebca | |||
| 1dc05b84cf | |||
| e0fb3ced68 | |||
| 9ce66652ab | |||
| c8484d4cc1 | |||
| cdbc7a5dd4 | |||
| 7a94b3fbf8 | |||
| 49576828b6 | |||
| 26aa07f4b1 | |||
| b05064e87b | |||
| 4f31b80707 | |||
| da4ea70b05 | |||
| 4c5f806dd2 | |||
| 6d7b19a478 | |||
| acfbf6e0da | |||
| 9f9d5751f6 | |||
| d362b5cd3e | |||
| 160c96047c | |||
| 90ba42d7f7 | |||
| fb2b92c381 | |||
| b758087a96 | |||
| 599fc8a125 | |||
| 54eabb62d2 | |||
| 7471f71212 | |||
| 137ee91845 | |||
| f8f01c0bcb | |||
| 4256eb45e8 | |||
| 8ae533d801 | |||
| 6df3790e51 | |||
| 52d5691b5b | |||
| 05cb6f9003 | |||
| 419726f94e | |||
| 606461d75a | |||
| e636e80a82 | |||
| dda2d48ea0 | |||
| 3129dc5b80 |
@@ -0,0 +1,3 @@
|
||||
[submodule "Backend"]
|
||||
path = Backend
|
||||
url = https://github.com/VergeDX/clock_api
|
||||
@@ -1,37 +0,0 @@
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
@@ -1,45 +0,0 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("org.springframework.boot") version "2.4.1"
|
||||
id("io.spring.dependency-management") version "1.0.10.RELEASE"
|
||||
kotlin("jvm") version "1.4.21"
|
||||
kotlin("plugin.spring") version "1.4.21"
|
||||
// kotlin("plugin.jpa") version "1.4.21"
|
||||
}
|
||||
|
||||
group = "org.hydev.ios"
|
||||
version = "0.0.1.1"
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
implementation("org.springframework.boot", "spring-boot-starter-validation", "2.4.1")
|
||||
|
||||
// MongoDB
|
||||
implementation(group = "org.springframework.data", name = "spring-data-mongodb", version = "3.1.2")
|
||||
implementation(group = "org.springframework.boot", name = "spring-boot-starter-data-mongodb")
|
||||
|
||||
// MariaDB
|
||||
// implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
// runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -1,185 +0,0 @@
|
||||
#!/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" "$@"
|
||||
@@ -1,89 +0,0 @@
|
||||
@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
|
||||
@@ -1 +0,0 @@
|
||||
rootProject.name = "alarm-clock"
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.hydev.ios.alarmclock
|
||||
|
||||
import org.hydev.ios.alarmclock.data.UserRepo
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableMongoRepositories(basePackageClasses = [UserRepo::class])
|
||||
class AlarmClockApplication
|
||||
|
||||
fun main(args: Array<String>)
|
||||
{
|
||||
runApplication<AlarmClockApplication>(*args)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.hydev.ios.alarmclock
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import java.security.SecureRandom
|
||||
import java.security.spec.KeySpec
|
||||
import java.util.*
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
|
||||
/**
|
||||
* Generate "Bad Request" response entity
|
||||
*
|
||||
* @param msg Message
|
||||
* @return Response entity
|
||||
*/
|
||||
fun bad(msg: String): ResponseEntity<String> = ResponseEntity.badRequest().body(msg)
|
||||
|
||||
/**
|
||||
* Generate random salt
|
||||
*
|
||||
* @param len Length of the salt in bytes
|
||||
* @return Random byte array of size len
|
||||
*/
|
||||
fun randSalt(len: Int = 16): ByteArray
|
||||
{
|
||||
val random = SecureRandom()
|
||||
val salt = ByteArray(len)
|
||||
random.nextBytes(salt)
|
||||
return salt
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a password
|
||||
*
|
||||
* @return <Hash, Salt>
|
||||
*/
|
||||
fun String.passwordHash(salt: String = randSalt().base64): Pair<String, String>
|
||||
{
|
||||
val spec: KeySpec = PBEKeySpec(toCharArray(), salt.base64, 32767, 512)
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
return Pair(factory.generateSecret(spec).encoded.base64, salt)
|
||||
}
|
||||
|
||||
val ByteArray.base64: String
|
||||
get() = Base64.getEncoder().encodeToString(this)
|
||||
|
||||
val String.base64: ByteArray
|
||||
get() = Base64.getDecoder().decode(this)
|
||||
|
||||
|
||||
fun main(args: Array<String>)
|
||||
{
|
||||
val (hash, salt) = "password".passwordHash()
|
||||
println(hash)
|
||||
println(salt)
|
||||
val (hash2, salt2) = "password".passwordHash(salt)
|
||||
assert(hash == hash2 && salt == salt2)
|
||||
println("Hash matches")
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.hydev.ios.alarmclock.data
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* TODO: Write a description for this class!
|
||||
*
|
||||
* @author HyDEV Team (https://github.com/HyDevelop)
|
||||
* @author Hykilpikonna (https://github.com/hykilpikonna)
|
||||
* @author Vanilla (https://github.com/VergeDX)
|
||||
* @since 2021-01-12 09:28
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
class Test
|
||||
{
|
||||
@GetMapping("/echo")
|
||||
fun echo(message: String?) = message
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package org.hydev.ios.alarmclock.data
|
||||
|
||||
import org.hydev.ios.alarmclock.bad
|
||||
import org.hydev.ios.alarmclock.passwordHash
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.domain.Example
|
||||
import org.springframework.data.domain.ExampleMatcher
|
||||
import org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.ignoreCase
|
||||
import org.springframework.data.mongodb.core.index.Indexed
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import javax.validation.constraints.Email
|
||||
|
||||
/**
|
||||
* The database model for an user
|
||||
*
|
||||
* @author HyDEV Team (https://github.com/HyDevelop)
|
||||
* @author Hykilpikonna (https://github.com/hykilpikonna)
|
||||
* @author Vanilla (https://github.com/VergeDX)
|
||||
* @since 2021-01-09 10:48
|
||||
*/
|
||||
@Document(collation = "user")
|
||||
data class User(
|
||||
@Id
|
||||
var id: Long = 0,
|
||||
|
||||
var name: String,
|
||||
|
||||
@Indexed(unique = true)
|
||||
var email: String,
|
||||
|
||||
var passHash: String = "",
|
||||
|
||||
var passSalt: String = ""
|
||||
)
|
||||
{
|
||||
constructor(name: String, email: String, pass: String) : this(name = name, email = email)
|
||||
{
|
||||
val (h, s) = pass.passwordHash()
|
||||
passHash = h
|
||||
passSalt = s
|
||||
}
|
||||
}
|
||||
|
||||
interface UserRepo: MongoRepository<User, String>
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
class UserApi(val repo: UserRepo)
|
||||
{
|
||||
val em = ExampleMatcher.matching().withIgnorePaths("id", "passHash", "passSalt", "name").withMatcher("email", ignoreCase())
|
||||
|
||||
@GetMapping("/register")
|
||||
fun register(@RequestParam name: String, @RequestParam pass: String, @RequestParam @Email email: String): Any
|
||||
{
|
||||
// Check name length
|
||||
if (name.length !in 1..32) return bad("Name length not in range 1 to 32")
|
||||
|
||||
// Check if email exists
|
||||
val user = User(name, email, pass)
|
||||
if (repo.exists(Example.of(user, em))) return bad("Email is already registered")
|
||||
|
||||
// Check password strength
|
||||
if (pass.length < 8) return bad("Password must be longer than 8 chars")
|
||||
|
||||
// Register
|
||||
repo.save(user)
|
||||
return user
|
||||
}
|
||||
|
||||
@GetMapping("/delete")
|
||||
fun delete(@RequestParam email: String, @RequestParam pass: String): Any
|
||||
{
|
||||
// Check if username exists
|
||||
val users = repo.findAll(Example.of(User("", email, pass), em))
|
||||
if (users.isEmpty()) return bad("User doesn't exist")
|
||||
|
||||
// Delete
|
||||
users.forEach { repo.delete(it) }
|
||||
return "Deleted"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
#spring.datasource.url=jdbc:mariadb://192.168.0.22:3306/clock
|
||||
#spring.datasource.username=root
|
||||
#spring.datasource.password=<insert password here>
|
||||
#spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
|
||||
#spring.jpa.hibernate.ddl-auto=update
|
||||
|
||||
spring.data.mongodb.uri=mongodb://hykilp:<insert-password-here>@192.168.0.22:27017/?authSource=admin&retryWrites=true
|
||||
spring.data.mongodb.database=clock
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.hydev.ios.alarmclock
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class AlarmClockApplicationTests
|
||||
{
|
||||
|
||||
@Test
|
||||
fun contextLoads()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,13 +15,15 @@
|
||||
4FD642DB25B4B7F60069171E /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD642DA25B4B7F60069171E /* Utils.swift */; };
|
||||
4FD642E025B4D5F30069171E /* AlarmActivationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD642DF25B4D5F30069171E /* AlarmActivationViewController.swift */; };
|
||||
4FF0683F25A5F18700304E6B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF0683E25A5F18700304E6B /* AppDelegate.swift */; };
|
||||
4FF0684325A5F18700304E6B /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF0684225A5F18700304E6B /* AccountViewController.swift */; };
|
||||
4FF0684325A5F18700304E6B /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF0684225A5F18700304E6B /* Account.swift */; };
|
||||
4FF0684625A5F18700304E6B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4FF0684425A5F18700304E6B /* Main.storyboard */; };
|
||||
4FF0684825A5F18800304E6B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4FF0684725A5F18800304E6B /* Assets.xcassets */; };
|
||||
4FF0684B25A5F18800304E6B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4FF0684925A5F18800304E6B /* LaunchScreen.storyboard */; };
|
||||
7C12BC7825BE25C000E5659C /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C12BC7725BE25C000E5659C /* Notification.swift */; };
|
||||
7C5DAE9C25AF812200E44C52 /* clock.png in Resources */ = {isa = PBXBuildFile; fileRef = 7C5DAE9B25AF812200E44C52 /* clock.png */; };
|
||||
7C83963625AF375B0027A94C /* NotificationLogic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C83963525AF375B0027A94C /* NotificationLogic.swift */; };
|
||||
7C83963925AF68980027A94C /* TestingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C83963825AF68980027A94C /* TestingViewController.swift */; };
|
||||
7C60741E25C11DC000B0A154 /* AlarmLogo1.png in Resources */ = {isa = PBXBuildFile; fileRef = 7C60741D25C11DC000B0A154 /* AlarmLogo1.png */; };
|
||||
7C83963925AF68980027A94C /* DebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C83963825AF68980027A94C /* DebugViewController.swift */; };
|
||||
7CD385A625BE4649007E9890 /* notif.caf in Resources */ = {isa = PBXBuildFile; fileRef = 7CD385A525BE4649007E9890 /* notif.caf */; };
|
||||
C7E638E825B88F8B00799512 /* MathExpressions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7E638E725B88F8B00799512 /* MathExpressions.swift */; };
|
||||
F0DF7C0725BCD9FC0064A26B /* StopwatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DF7C0625BCD9FC0064A26B /* StopwatchViewController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -34,18 +36,20 @@
|
||||
4FD642D225B48C380069171E /* AlarmActivator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmActivator.swift; sourceTree = "<group>"; };
|
||||
4FD642DA25B4B7F60069171E /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
|
||||
4FD642DF25B4D5F30069171E /* AlarmActivationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmActivationViewController.swift; sourceTree = "<group>"; };
|
||||
4FF0683B25A5F18700304E6B /* ProjectClock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ProjectClock.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
4FF0683B25A5F18700304E6B /* GetGoing.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GetGoing.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
4FF0683E25A5F18700304E6B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
4FF0684225A5F18700304E6B /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = "<group>"; };
|
||||
4FF0684225A5F18700304E6B /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
|
||||
4FF0684525A5F18700304E6B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
4FF0684725A5F18800304E6B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
4FF0684A25A5F18800304E6B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
4FF0684C25A5F18800304E6B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
7C12BC7725BE25C000E5659C /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
|
||||
7C5DAE9B25AF812200E44C52 /* clock.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = clock.png; sourceTree = "<group>"; };
|
||||
7C60741D25C11DC000B0A154 /* AlarmLogo1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = AlarmLogo1.png; sourceTree = "<group>"; };
|
||||
7C83962D25AF34F00027A94C /* ProjectClock.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ProjectClock.entitlements; sourceTree = "<group>"; };
|
||||
7C83962F25AF34F10027A94C /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; };
|
||||
7C83963525AF375B0027A94C /* NotificationLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationLogic.swift; sourceTree = "<group>"; };
|
||||
7C83963825AF68980027A94C /* TestingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingViewController.swift; sourceTree = "<group>"; };
|
||||
7C83963825AF68980027A94C /* DebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewController.swift; sourceTree = "<group>"; };
|
||||
7CD385A525BE4649007E9890 /* notif.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = notif.caf; sourceTree = "<group>"; };
|
||||
C7E638E725B88F8B00799512 /* MathExpressions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MathExpressions.swift; sourceTree = "<group>"; };
|
||||
F0DF7C0625BCD9FC0064A26B /* StopwatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopwatchViewController.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -73,7 +77,7 @@
|
||||
4FF0683C25A5F18700304E6B /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4FF0683B25A5F18700304E6B /* ProjectClock.app */,
|
||||
4FF0683B25A5F18700304E6B /* GetGoing.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -81,24 +85,26 @@
|
||||
4FF0683D25A5F18700304E6B /* ProjectClock */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7C5DAE9B25AF812200E44C52 /* clock.png */,
|
||||
4FF0684C25A5F18800304E6B /* Info.plist */,
|
||||
7C83962D25AF34F00027A94C /* ProjectClock.entitlements */,
|
||||
4FF0684925A5F18800304E6B /* LaunchScreen.storyboard */,
|
||||
4FF0684725A5F18800304E6B /* Assets.xcassets */,
|
||||
4FF0683E25A5F18700304E6B /* AppDelegate.swift */,
|
||||
4FF0684225A5F18700304E6B /* AccountViewController.swift */,
|
||||
4FA419AE25AF93EC004CE0FC /* AlarmViewController.swift */,
|
||||
7CD385A425BE4639007E9890 /* Sounds */,
|
||||
4FF0684225A5F18700304E6B /* Account.swift */,
|
||||
4F8A607025A9160400D88DC3 /* AddAlarmViewController.swift */,
|
||||
7C83963825AF68980027A94C /* TestingViewController.swift */,
|
||||
4FD642DF25B4D5F30069171E /* AlarmActivationViewController.swift */,
|
||||
4FD642D225B48C380069171E /* AlarmActivator.swift */,
|
||||
4FA419AE25AF93EC004CE0FC /* AlarmViewController.swift */,
|
||||
4FF0683E25A5F18700304E6B /* AppDelegate.swift */,
|
||||
4FF0684725A5F18800304E6B /* Assets.xcassets */,
|
||||
7C5DAE9B25AF812200E44C52 /* clock.png */,
|
||||
7C60741D25C11DC000B0A154 /* AlarmLogo1.png */,
|
||||
7C83963825AF68980027A94C /* DebugViewController.swift */,
|
||||
4FF0684C25A5F18800304E6B /* Info.plist */,
|
||||
4FF0684925A5F18800304E6B /* LaunchScreen.storyboard */,
|
||||
4FF0684425A5F18700304E6B /* Main.storyboard */,
|
||||
F0DF7C0625BCD9FC0064A26B /* StopwatchViewController.swift */,
|
||||
7C83963525AF375B0027A94C /* NotificationLogic.swift */,
|
||||
4F98955125A9260400F51321 /* Net.swift */,
|
||||
4F509BD125AE22D100726227 /* Models.swift */,
|
||||
C7E638E725B88F8B00799512 /* MathExpressions.swift */,
|
||||
4F509BD125AE22D100726227 /* Models.swift */,
|
||||
4F98955125A9260400F51321 /* Net.swift */,
|
||||
7C12BC7725BE25C000E5659C /* Notification.swift */,
|
||||
7C83962D25AF34F00027A94C /* ProjectClock.entitlements */,
|
||||
F0DF7C0625BCD9FC0064A26B /* StopwatchViewController.swift */,
|
||||
4FD642DA25B4B7F60069171E /* Utils.swift */,
|
||||
);
|
||||
path = ProjectClock;
|
||||
@@ -112,12 +118,20 @@
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7CD385A425BE4639007E9890 /* Sounds */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7CD385A525BE4649007E9890 /* notif.caf */,
|
||||
);
|
||||
path = Sounds;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
4FF0683A25A5F18700304E6B /* ProjectClock */ = {
|
||||
4FF0683A25A5F18700304E6B /* GetGoing */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 4FF0684F25A5F18800304E6B /* Build configuration list for PBXNativeTarget "ProjectClock" */;
|
||||
buildConfigurationList = 4FF0684F25A5F18800304E6B /* Build configuration list for PBXNativeTarget "GetGoing" */;
|
||||
buildPhases = (
|
||||
4FF0683725A5F18700304E6B /* Sources */,
|
||||
4FF0683825A5F18700304E6B /* Frameworks */,
|
||||
@@ -127,9 +141,9 @@
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = ProjectClock;
|
||||
name = GetGoing;
|
||||
productName = ProjectClock;
|
||||
productReference = 4FF0683B25A5F18700304E6B /* ProjectClock.app */;
|
||||
productReference = 4FF0683B25A5F18700304E6B /* GetGoing.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
@@ -149,7 +163,7 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 4FF0683625A5F18700304E6B /* Build configuration list for PBXProject "ProjectClock" */;
|
||||
buildConfigurationList = 4FF0683625A5F18700304E6B /* Build configuration list for PBXProject "GetGoing" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
@@ -162,7 +176,7 @@
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
4FF0683A25A5F18700304E6B /* ProjectClock */,
|
||||
4FF0683A25A5F18700304E6B /* GetGoing */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -172,10 +186,12 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7C60741E25C11DC000B0A154 /* AlarmLogo1.png in Resources */,
|
||||
4FF0684B25A5F18800304E6B /* LaunchScreen.storyboard in Resources */,
|
||||
7C5DAE9C25AF812200E44C52 /* clock.png in Resources */,
|
||||
4FF0684825A5F18800304E6B /* Assets.xcassets in Resources */,
|
||||
4FF0684625A5F18700304E6B /* Main.storyboard in Resources */,
|
||||
7CD385A625BE4649007E9890 /* notif.caf in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -188,16 +204,16 @@
|
||||
files = (
|
||||
4F8A607125A9160400D88DC3 /* AddAlarmViewController.swift in Sources */,
|
||||
4F98955225A9260400F51321 /* Net.swift in Sources */,
|
||||
7C83963925AF68980027A94C /* TestingViewController.swift in Sources */,
|
||||
4FF0684325A5F18700304E6B /* AccountViewController.swift in Sources */,
|
||||
7C83963925AF68980027A94C /* DebugViewController.swift in Sources */,
|
||||
4FF0684325A5F18700304E6B /* Account.swift in Sources */,
|
||||
F0DF7C0725BCD9FC0064A26B /* StopwatchViewController.swift in Sources */,
|
||||
4FF0683F25A5F18700304E6B /* AppDelegate.swift in Sources */,
|
||||
4FD642D325B48C380069171E /* AlarmActivator.swift in Sources */,
|
||||
4FD642DB25B4B7F60069171E /* Utils.swift in Sources */,
|
||||
4FA419AF25AF93EC004CE0FC /* AlarmViewController.swift in Sources */,
|
||||
4F509BD225AE22D100726227 /* Models.swift in Sources */,
|
||||
7C12BC7825BE25C000E5659C /* Notification.swift in Sources */,
|
||||
C7E638E825B88F8B00799512 /* MathExpressions.swift in Sources */,
|
||||
7C83963625AF375B0027A94C /* NotificationLogic.swift in Sources */,
|
||||
4FD642E025B4D5F30069171E /* AlarmActivationViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -353,7 +369,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.hydev.ProjectClock;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.hydev.GetGoing;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -373,7 +389,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.hydev.ProjectClock;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.hydev.GetGoing;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -383,7 +399,7 @@
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
4FF0683625A5F18700304E6B /* Build configuration list for PBXProject "ProjectClock" */ = {
|
||||
4FF0683625A5F18700304E6B /* Build configuration list for PBXProject "GetGoing" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
4FF0684D25A5F18800304E6B /* Debug */,
|
||||
@@ -392,7 +408,7 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
4FF0684F25A5F18800304E6B /* Build configuration list for PBXNativeTarget "ProjectClock" */ = {
|
||||
4FF0684F25A5F18800304E6B /* Build configuration list for PBXNativeTarget "GetGoing" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
4FF0685025A5F18800304E6B /* Debug */,
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1230"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "4FF0683A25A5F18700304E6B"
|
||||
BuildableName = "GetGoing.app"
|
||||
BlueprintName = "GetGoing"
|
||||
ReferencedContainer = "container:GetGoing.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "4FF0683A25A5F18700304E6B"
|
||||
BuildableName = "GetGoing.app"
|
||||
BlueprintName = "GetGoing"
|
||||
ReferencedContainer = "container:GetGoing.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "4FF0683A25A5F18700304E6B"
|
||||
BuildableName = "GetGoing.app"
|
||||
BlueprintName = "GetGoing"
|
||||
ReferencedContainer = "container:GetGoing.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Hykilpikonna
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>ProjectClock.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,542 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// ProjectClock
|
||||
//
|
||||
// Created by Hykilpikonna on 1/6/21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
Account view controller controlling the two separate view controllers
|
||||
*/
|
||||
class AccountViewController: UIViewController
|
||||
{
|
||||
@IBOutlet var vLogin: UIView!
|
||||
@IBOutlet var vManage: UIView!
|
||||
|
||||
// For instance references
|
||||
static var this: AccountViewController!
|
||||
|
||||
/**
|
||||
Called when the user switch to this tab
|
||||
*/
|
||||
override func viewDidLoad()
|
||||
{
|
||||
// Static instance reference
|
||||
AccountViewController.this = self
|
||||
|
||||
// Check if already registered/logged in
|
||||
if localStorage.string(forKey: "id") != nil { login() }
|
||||
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
/**
|
||||
Login from the account page
|
||||
*/
|
||||
func login()
|
||||
{
|
||||
vLogin.hide()
|
||||
vManage.show()
|
||||
ManageVC.this.display()
|
||||
}
|
||||
|
||||
/**
|
||||
Logout
|
||||
*/
|
||||
func logout()
|
||||
{
|
||||
// Remove login info
|
||||
["id", "user", "pass", "family"].forEach { localStorage.removeObject(forKey: $0) }
|
||||
|
||||
// Switch UI
|
||||
vLogin.show()
|
||||
vManage.hide()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
View controller for registration and login
|
||||
*/
|
||||
class LoginVC: EndEditingOnReturn
|
||||
{
|
||||
@IBOutlet weak var username: UITextField!
|
||||
@IBOutlet weak var password: UITextField!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
username.delegate = self
|
||||
password.delegate = self
|
||||
}
|
||||
|
||||
/**
|
||||
Send user login/registration request
|
||||
|
||||
- Parameter login: True: Login, False: Register
|
||||
*/
|
||||
func userRequest(login: Bool)
|
||||
{
|
||||
// Verify username and password
|
||||
guard let name = username.text, name ~= "[A-Za-z0-9_-]{3,16}" else
|
||||
{
|
||||
msg("Username Invalid", "Username must be 3 to 16 characters long, and must only contain a-z, 0-9, underscore, and minus signs (-).")
|
||||
return
|
||||
}
|
||||
guard let pass = password.text, pass.count >= 8 else
|
||||
{
|
||||
msg("Password Invalid", "Password must be more than or equal to 8 characters long")
|
||||
return
|
||||
}
|
||||
|
||||
// Error messages
|
||||
let errors = ["409 - [\"A0111\"]": "Account already exists, please login instead.",
|
||||
"401 -": "Incorrect username/password",
|
||||
"404 -": "Username does not exist in the database",
|
||||
"406 - [\"A0101\"]": "Username invalid."
|
||||
]
|
||||
|
||||
// Send register request
|
||||
sendReq(login ? APIs.login : APIs.register,
|
||||
title: login ? "Logging in..." : "Registering...", errors: errors,
|
||||
params: ["username": name, "password": pass.sha256])
|
||||
{
|
||||
// Store username and password
|
||||
localStorage["name"] = name
|
||||
localStorage["pass"] = pass.sha256
|
||||
localStorage["id"] = $0
|
||||
|
||||
send(APIs.familyGet)
|
||||
{
|
||||
$0.localSave()
|
||||
self.loginSuccess(login)
|
||||
}
|
||||
err: { it in print(it); self.loginSuccess(login) }
|
||||
}
|
||||
}
|
||||
|
||||
private func loginSuccess(_ login: Bool)
|
||||
{
|
||||
ui
|
||||
{
|
||||
// Send feedback
|
||||
if login { self.msg("Login success!", "Now you can use account features, yay!") }
|
||||
else { self.msg("Registration success!", "Now you have an account, yay!") }
|
||||
|
||||
// Hide registration and show account detail view
|
||||
AccountViewController.this.login()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks register
|
||||
*/
|
||||
@IBAction func register(_ sender: Any)
|
||||
{
|
||||
self.view.endEditing(true)
|
||||
userRequest(login: false)
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks login
|
||||
*/
|
||||
@IBAction func login(_ sender: Any)
|
||||
{
|
||||
self.view.endEditing(true)
|
||||
userRequest(login: true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Account manage view controller
|
||||
*/
|
||||
class ManageVC: UIViewController
|
||||
{
|
||||
static var this: ManageVC!
|
||||
|
||||
@IBOutlet weak var lUsername: UILabel!
|
||||
@IBOutlet weak var lJoinDate: UILabel!
|
||||
|
||||
@IBOutlet weak var lCurrentFamily: UILabel!
|
||||
|
||||
/**
|
||||
Called when the user switched to the account tab (whether the view container is hidden or not)
|
||||
*/
|
||||
override func viewDidLoad()
|
||||
{
|
||||
// Static reference
|
||||
ManageVC.this = self
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
/**
|
||||
Display account info
|
||||
*/
|
||||
func display()
|
||||
{
|
||||
lUsername.text = localStorage.string(forKey: "name")
|
||||
// TODO: Implement join date (not important)
|
||||
lJoinDate.text = localStorage.string(forKey: "id")
|
||||
|
||||
// Display family name
|
||||
if let family = Family.fromLocal()
|
||||
{
|
||||
lCurrentFamily.text = family.name
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks the upload backup button
|
||||
*/
|
||||
@IBAction func uploadBackup(_ sender: Any)
|
||||
{
|
||||
sendReq(APIs.uploadConfig, title: "Uploading...", params: ["config": localStorage.string(forKey: "alarms")!])
|
||||
{ it in self.msg("Success!", "You're backed up.") }
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks the download backup button
|
||||
*/
|
||||
@IBAction func downloadBackup(_ sender: Any)
|
||||
{
|
||||
sendReq(APIs.downloadConfig, title: "Downloading...")
|
||||
{
|
||||
// Make sure backup exists and is parseable
|
||||
if $0.isEmpty { self.msg("Nope", "No backups found"); return }
|
||||
guard JSON.parse([Alarm].self, $0) != nil else { self.msg("Failed", "Backup restoration failed."); return }
|
||||
|
||||
// Save backup
|
||||
localStorage.setValue($0, forKey: "alarms")
|
||||
|
||||
// Update UI
|
||||
AlarmViewController.staticTable?.reloadData()
|
||||
|
||||
// Schedule any missing notifications
|
||||
Notification.bulkScheduleNotifications(alarms: Alarms.fromLocal().list)
|
||||
|
||||
self.msg("Success!", "You're restored your backup.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks the logout button
|
||||
*/
|
||||
@IBAction func logout(_ sender: Any)
|
||||
{
|
||||
AccountViewController.this.logout()
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks the delete account button
|
||||
*/
|
||||
@IBAction func deleteAccount(_ sender: Any)
|
||||
{
|
||||
enterPin("Are you sure?", "Enter 1234 to continue deleting your account, you can't undo this.")
|
||||
{
|
||||
guard $0 == "1234" else { return }
|
||||
|
||||
self.sendReq(APIs.delete, title: "Deleting...")
|
||||
{
|
||||
print("Deleted! \($0)")
|
||||
self.msg("Deleted!", "You are erased from our database, you no longer exist.")
|
||||
self.logout(sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Family view controller that displays family info or create/join family buttons
|
||||
*/
|
||||
class FamilyVC: UIViewController
|
||||
{
|
||||
static var this: FamilyVC!
|
||||
|
||||
// No family view - prompt to create/join a family
|
||||
@IBOutlet weak var noFamilyView: UIView!
|
||||
var createMode: Bool!
|
||||
|
||||
@IBAction func btnCreate(_ sender: Any)
|
||||
{
|
||||
createMode = true
|
||||
performSegue(withIdentifier: "family-create-join", sender: nil)
|
||||
}
|
||||
|
||||
@IBAction func btnJoin(_ sender: Any)
|
||||
{
|
||||
createMode = false
|
||||
performSegue(withIdentifier: "family-create-join", sender: nil)
|
||||
}
|
||||
|
||||
@IBSegueAction func segueCreateJoin(_ coder: NSCoder) -> FamilyCreateJoinVC?
|
||||
{
|
||||
return FamilyCreateJoinVC(coder: coder, create: createMode)
|
||||
}
|
||||
|
||||
// Family view - Display family information and controls
|
||||
@IBOutlet weak var familyView: UIView!
|
||||
@IBOutlet weak var table: UITableView!
|
||||
|
||||
/**
|
||||
Called when information is updated
|
||||
*/
|
||||
override func viewDidLoad()
|
||||
{
|
||||
FamilyVC.this = self
|
||||
|
||||
if let _ = Family.fromLocal()
|
||||
{
|
||||
// Family exists
|
||||
noFamilyView.hide()
|
||||
familyView.show()
|
||||
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
}
|
||||
else
|
||||
{
|
||||
// Family doesn't exist
|
||||
noFamilyView.show()
|
||||
familyView.hide()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks the refresh button
|
||||
*/
|
||||
@IBAction func btnRefresh(_ sender: Any)
|
||||
{
|
||||
sendReq(APIs.familyGet, title: "Updating family...")
|
||||
{
|
||||
$0.localSave()
|
||||
self.table.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks the change pin button
|
||||
*/
|
||||
@IBAction func btnChangePin(_ sender: Any)
|
||||
{
|
||||
self.enterPin("Change Pin", "Enter your OLD pin:") { oldPin in
|
||||
|
||||
self.enterPin("Change Pin", "Enter your NEW pin:") { newPin in
|
||||
|
||||
guard newPin.count >= 4 else { self.msg("Pin Too Weak", "Your family pin must be 4 numbers or more."); return }
|
||||
|
||||
self.sendReq(APIs.familyChangePin, title: "Updating Pin...", params: ["oldPin": oldPin, "newPin": newPin]) { it in
|
||||
|
||||
self.msg("Update Success!", "Your family pin is updated.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks the leave or delete family button
|
||||
*/
|
||||
@IBAction func btnLeave(_ sender: UIButton)
|
||||
{
|
||||
let i = sender.tag
|
||||
let action = ["Leave", "Delete"][i]
|
||||
let title = ["Leaving...", "Deleting..."][i]
|
||||
let msg = ["You left the family.", "You deleted the family."][i]
|
||||
|
||||
enterPin()
|
||||
{
|
||||
self.sendReq(APIs.familyAction, title: title, params: ["pin": $0, "action": action]) { it in
|
||||
|
||||
// Leave or delete, clear local storage's family section
|
||||
if i == 0 || i == 1 { localStorage.removeObject(forKey: "family") }
|
||||
|
||||
self.msg("\(action) Success!", msg)
|
||||
{
|
||||
self.viewDidLoad()
|
||||
AccountViewController.this.login()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Table data source
|
||||
*/
|
||||
extension FamilyVC: UITableViewDelegate, UITableViewDataSource
|
||||
{
|
||||
static var selectedUser: String = ""
|
||||
|
||||
/**
|
||||
Define row count
|
||||
*/
|
||||
func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int
|
||||
{
|
||||
guard let family = Family.fromLocal() else { return 0 }
|
||||
return family.membersList.count
|
||||
}
|
||||
|
||||
/**
|
||||
Set cell at i
|
||||
*/
|
||||
func tableView(_ view: UITableView, cellForRowAt i: IndexPath) -> UITableViewCell
|
||||
{
|
||||
let cell = view.dequeueReusableCell(withIdentifier: "family-member-cell", for: i)
|
||||
cell.textLabel?.text = Family.fromLocal()!.membersList[i.row]
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks on the cell at i
|
||||
*/
|
||||
func tableView(_ view: UITableView, didSelectRowAt i: IndexPath)
|
||||
{
|
||||
FamilyVC.selectedUser = Family.fromLocal()!.membersList[i.row]
|
||||
view.deselectRow(at: i, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Create or join a family
|
||||
*/
|
||||
class FamilyCreateJoinVC: EndEditingOnReturn
|
||||
{
|
||||
let createMode: Bool
|
||||
@IBOutlet weak var lFamilyNameOrId: UILabel!
|
||||
@IBOutlet weak var bCreateJoin: UIButton!
|
||||
@IBOutlet weak var tNameOrId: UITextField!
|
||||
@IBOutlet weak var tPin: UITextField!
|
||||
|
||||
/**
|
||||
Pass in create mode from FamilyVC
|
||||
*/
|
||||
init?(coder: NSCoder, create: Bool)
|
||||
{
|
||||
createMode = create
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
/**
|
||||
On load
|
||||
*/
|
||||
override func viewDidLoad()
|
||||
{
|
||||
// Set UI according to createMode
|
||||
lFamilyNameOrId.text = createMode ? "Family Name" : "Family ID"
|
||||
bCreateJoin.setTitle(createMode ? "Create" : "Join", for: .normal)
|
||||
tNameOrId.keyboardType = createMode ? .default : .numberPad
|
||||
|
||||
// Default name
|
||||
if createMode
|
||||
{
|
||||
tNameOrId.text = "\(localStorage.string(forKey: "name")!)'s Family"
|
||||
}
|
||||
|
||||
// End editing on return
|
||||
tNameOrId.delegate = self
|
||||
tPin.delegate = self
|
||||
}
|
||||
|
||||
/**
|
||||
Called after successfully joining or creating a family.
|
||||
*/
|
||||
func afterFamilyChange()
|
||||
{
|
||||
self.dismiss()
|
||||
FamilyVC.this.viewDidLoad()
|
||||
AccountViewController.this.login()
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks create or join button
|
||||
*/
|
||||
@IBAction func btnCreateOrJoin(_ sender: Any)
|
||||
{
|
||||
// Check pin
|
||||
guard let pin = tPin.text, pin.count >= 4 else { msg("Pin Too Weak", "Your family pin must be 4 numbers or more."); return }
|
||||
|
||||
if createMode
|
||||
{
|
||||
guard let name = tNameOrId.text, !name.isEmpty else { msg("Name Empty", "You must enter a family name"); return }
|
||||
|
||||
// Create family
|
||||
sendReq(APIs.familyCreate, title: "Creating...", params: ["name": name, "pin": pin])
|
||||
{
|
||||
// Save
|
||||
$0.localSave()
|
||||
|
||||
// Send success message
|
||||
self.msg("Created!", "Your family ID is \($0.fid)") { self.afterFamilyChange() }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
guard let idString = tNameOrId.text, !idString.isEmpty, let id = Int(idString) else
|
||||
{ msg("ID Incorrect", "Please make sure your ID is an positive integer."); return }
|
||||
|
||||
// Join family
|
||||
sendReq(APIs.familyAction, title: "Joining...", params: ["fid": String(id), "pin": pin, "action": "join"])
|
||||
{
|
||||
// Save
|
||||
$0.localSave()
|
||||
|
||||
// Send success message
|
||||
self.msg("Joined!", "Your family ID is \($0.fid)") { self.afterFamilyChange() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
View controller for adding an alarm to a fmaily member
|
||||
*/
|
||||
class FamilyAddAlarmVC: UIViewController, UITableViewDelegate, UITableViewDataSource
|
||||
{
|
||||
@IBOutlet weak var table: UITableView!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
table.delegate = self
|
||||
table.dataSource = self
|
||||
}
|
||||
|
||||
/**
|
||||
Define row count
|
||||
*/
|
||||
func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int
|
||||
{
|
||||
return Alarms.fromLocal().list.count
|
||||
}
|
||||
|
||||
/**
|
||||
Set cell at i
|
||||
*/
|
||||
func tableView(_ view: UITableView, cellForRowAt i: IndexPath) -> UITableViewCell
|
||||
{
|
||||
let cell = view.dequeueReusableCell(withIdentifier: "family-alarm-cell", for: i)
|
||||
cell.textLabel?.text = Alarms.fromLocal().list[i.row].timeText
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks on the cell at i
|
||||
*/
|
||||
func tableView(_ view: UITableView, didSelectRowAt i: IndexPath)
|
||||
{
|
||||
view.deselectRow(at: i, animated: true)
|
||||
enterPin()
|
||||
{
|
||||
self.sendReq(APIs.familyAddAlarm, title: "Adding...", params: ["to": FamilyVC.selectedUser, "pin": $0, "alarm": JSON.stringify(Alarms.fromLocal().list[i.row])!])
|
||||
{
|
||||
print($0)
|
||||
self.msg("Added!", "The member will receive the alarm after opening the app.")
|
||||
{
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// ProjectClock
|
||||
//
|
||||
// Created by Hykilpikonna on 1/6/21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
Account view controller controlling the two separate view controllers
|
||||
*/
|
||||
class AccountViewController: UIViewController
|
||||
{
|
||||
@IBOutlet var vLogin: UIView!
|
||||
@IBOutlet var vManage: UIView!
|
||||
|
||||
// For instance references
|
||||
static var this: AccountViewController!
|
||||
|
||||
/**
|
||||
Called when the user switch to this tab
|
||||
*/
|
||||
override func viewDidLoad()
|
||||
{
|
||||
// Static instance reference
|
||||
AccountViewController.this = self
|
||||
|
||||
// Check if already registered/logged in
|
||||
if localStorage.string(forKey: "id") != nil { login() }
|
||||
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
/**
|
||||
Login from the account page
|
||||
*/
|
||||
func login()
|
||||
{
|
||||
vLogin.isHidden = true
|
||||
vManage.isHidden = false
|
||||
ManageVC.this.display()
|
||||
}
|
||||
|
||||
/**
|
||||
Logout
|
||||
*/
|
||||
func logout()
|
||||
{
|
||||
// Remove login info
|
||||
["id", "user", "pass"].forEach { localStorage.removeObject(forKey: $0) }
|
||||
|
||||
// Switch UI
|
||||
vLogin.isHidden = false
|
||||
vManage.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
View controller for registration and login
|
||||
*/
|
||||
class LoginVC: UIViewController
|
||||
{
|
||||
@IBOutlet weak var username: UITextField!
|
||||
@IBOutlet weak var password: UITextField!
|
||||
|
||||
/**
|
||||
Send user login/registration request
|
||||
|
||||
- Parameter login: True: Login, False: Register
|
||||
*/
|
||||
func userRequest(login: Bool)
|
||||
{
|
||||
// Verify username and password
|
||||
guard let name = username.text, name ~= "[A-Za-z0-9_-]{3,16}" else
|
||||
{
|
||||
msg("Username Invalid", "Username must be 3 to 16 characters long, and must only contain a-z, 0-9, underscore, and minus signs (-).")
|
||||
return
|
||||
}
|
||||
guard let pass = password.text, pass.count >= 8 else
|
||||
{
|
||||
msg("Password Invalid", "Password must be more than or equal to 8 characters long")
|
||||
return
|
||||
}
|
||||
|
||||
// Error messages
|
||||
let errors = ["409 - [\"A0111\"]": "Account already exists, please login instead.",
|
||||
"401 -": "Incorrect username/password",
|
||||
"404 -": "Username does not exist in the database",
|
||||
"406 - [\"A0101\"]": "Username invalid."
|
||||
]
|
||||
|
||||
// Send register request
|
||||
sendReq(login ? APIs.login : APIs.register,
|
||||
title: login ? "Logging in..." : "Registering...", errors: errors,
|
||||
params: ["username": name, "password": pass.sha256])
|
||||
{
|
||||
// Store username and password
|
||||
localStorage["name"] = name
|
||||
localStorage["pass"] = pass.sha256
|
||||
localStorage["id"] = $0
|
||||
|
||||
// Send feedback
|
||||
if login { self.msg("Login success!", "Now you can use account features, yay!") }
|
||||
else { self.msg("Registration success!", "Now you have an account, yay!") }
|
||||
|
||||
// Hide registration and show account detail view
|
||||
AccountViewController.this.login()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks register
|
||||
*/
|
||||
@IBAction func register(_ sender: Any)
|
||||
{
|
||||
userRequest(login: false)
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks login
|
||||
*/
|
||||
@IBAction func login(_ sender: Any)
|
||||
{
|
||||
userRequest(login: true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Account manage view controller
|
||||
*/
|
||||
class ManageVC: UIViewController
|
||||
{
|
||||
static var this: ManageVC!
|
||||
|
||||
@IBOutlet weak var lUsername: UILabel!
|
||||
@IBOutlet weak var lJoinDate: UILabel!
|
||||
|
||||
/**
|
||||
Called when the user switched to the account tab (whether the view container is hidden or not)
|
||||
*/
|
||||
override func viewDidLoad()
|
||||
{
|
||||
// Static reference
|
||||
ManageVC.this = self
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
/**
|
||||
Display account info
|
||||
*/
|
||||
func display()
|
||||
{
|
||||
lUsername.text = localStorage.string(forKey: "name")
|
||||
// TODO: Correct join date
|
||||
lJoinDate.text = localStorage.string(forKey: "id")
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks the logout button
|
||||
*/
|
||||
@IBAction func logout(_ sender: Any)
|
||||
{
|
||||
AccountViewController.this.logout()
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user clicks the delete account button
|
||||
*/
|
||||
@IBAction func deleteAccount(_ sender: Any)
|
||||
{
|
||||
sendReq(APIs.delete, title: "Deleting...")
|
||||
{
|
||||
print("Deleted! \($0)")
|
||||
self.msg("Deleted!", "You are erased from our database, you no longer exist.")
|
||||
self.logout(sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,42 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class AddAlarmViewController: UIViewController
|
||||
class AddAlarmViewController: EndEditingOnReturn
|
||||
{
|
||||
// Editing variables
|
||||
var alarmCell: AlarmTableCell? = nil
|
||||
var editMode: Bool { alarmCell != nil }
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
// End edit on return
|
||||
alarmNameTextField.delegate = self
|
||||
|
||||
// Load alarm to edit if in edit mode
|
||||
if let alarmCell = alarmCell, let alarm = alarmCell.alarm
|
||||
{
|
||||
// Toggle editing mode
|
||||
viewTitle.text = "Edit Alarm"
|
||||
|
||||
// Set all the original values to be edited
|
||||
let (y,m,d) = Date().getYMD()
|
||||
timePicker.date = Date.create(y, m, d, alarm.hour, alarm.minute)
|
||||
|
||||
// Toggle proper repeats
|
||||
repeatWeekdaysSwitch.isOn = alarm.repeats[1...5].allSatisfy { $0 }
|
||||
repeatWeekendsSwitch.isOn = alarm.repeats[0] && alarm.repeats[6]
|
||||
|
||||
alarmNameTextField.text = alarm.text
|
||||
updateETA()
|
||||
|
||||
// Sets the WVM
|
||||
wvmPicker.selectRow(alarm.wakeMethod.index, inComponent: 0, animated: true)
|
||||
|
||||
// Sets alarm tone
|
||||
ringtonePicker.selectRow(ringtones.firstIndex { $0.tone == alarm.alarmTone }!, inComponent: 0, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// UI: Make scroll view scrollable
|
||||
@IBOutlet weak var scrollView: UIScrollView!
|
||||
@IBOutlet weak var scrollViewInner: UIView!
|
||||
@@ -17,30 +51,50 @@ class AddAlarmViewController: UIViewController
|
||||
scrollView.addSubview(scrollViewInner)
|
||||
scrollView.contentSize = scrollViewInner.frame.size
|
||||
}
|
||||
|
||||
|
||||
// Pickers
|
||||
@IBOutlet weak var timePicker: UIDatePicker!
|
||||
@IBOutlet weak var wvmPicker: UIPickerView!
|
||||
@IBOutlet weak var ringtonePicker: UIPickerView!
|
||||
|
||||
// UI Elements
|
||||
@IBOutlet weak var repeatWeekdaysSwitch: UISwitch!
|
||||
@IBOutlet weak var repeatWeekendsSwitch: UISwitch!
|
||||
@IBOutlet weak var alarmNameTextField: UITextField!
|
||||
@IBOutlet weak var timeTillAlarmLabel: UILabel!
|
||||
@IBOutlet weak var viewTitle: UILabel!
|
||||
|
||||
@IBAction func defaultRingtonesButton(_ sender: Any)
|
||||
/**
|
||||
Removes the currently selcted alarm.
|
||||
Returns the removed Alarm object.
|
||||
*/
|
||||
@discardableResult
|
||||
func removeCurrentAlarm() -> Alarm?
|
||||
{
|
||||
guard let alarm = alarmCell?.alarm else { return nil }
|
||||
|
||||
// Removes the alarm from stored alarms
|
||||
let alarms = Alarms.fromLocal()
|
||||
alarms.list = alarms.list.filter { $0 != alarmCell?.alarm }
|
||||
alarms.localSave()
|
||||
|
||||
return alarm
|
||||
}
|
||||
|
||||
@IBAction func soundLibraryButton(_ sender: Any)
|
||||
{
|
||||
|
||||
}
|
||||
/**
|
||||
Called when the time for the alarm is changed.
|
||||
Sets the time away at the top of the View.
|
||||
*/
|
||||
@IBAction func alarmTimeUpdated(_ sender: Any) { updateETA() }
|
||||
|
||||
/**
|
||||
Called when the user clicks the remove button and brings them back to the home page
|
||||
*/
|
||||
@IBAction func cancelAlarmButton(_ sender: Any) {
|
||||
if editMode {
|
||||
removeCurrentAlarm()
|
||||
}
|
||||
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
//might need to reset all UI elements
|
||||
}
|
||||
@@ -48,24 +102,68 @@ class AddAlarmViewController: UIViewController
|
||||
/**
|
||||
Called when the user clicks Add Alarm
|
||||
*/
|
||||
@IBAction func addAlarmButton(_ sender: Any) {
|
||||
@IBAction func addAlarmButton(_ sender: Any)
|
||||
{
|
||||
var oldAlarm: Alarm? = nil
|
||||
let alarm = createAlarm()
|
||||
let alarms = Alarms.fromLocal()
|
||||
|
||||
// Check if editing alarm
|
||||
if (editMode)
|
||||
{
|
||||
oldAlarm = removeCurrentAlarm()
|
||||
}
|
||||
// Check for existing alarm
|
||||
else
|
||||
{
|
||||
if (alarms.list.contains { $0 == alarm })
|
||||
{
|
||||
msg("Sorry", "An identical or similar alarm already exists, please try again")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add the alarm to the list and save the list
|
||||
Alarms.fromLocal().apply { $0.list.append(alarm) }.localSave();
|
||||
|
||||
//Schedules notification for the alarm
|
||||
if editMode
|
||||
{
|
||||
Notification.removeNotification(alarm: oldAlarm!)
|
||||
}
|
||||
Notification.scheduleNotification(alarm: alarm)
|
||||
|
||||
// Dismiss this view
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
/**
|
||||
Create alarm, but it doesn't add the alarm to the list
|
||||
*/
|
||||
func createAlarm() -> Alarm
|
||||
{
|
||||
let (h, m, _) = timePicker.date.getHMS()
|
||||
|
||||
// Create the alarm
|
||||
let alarm = Alarm(hour: h, minute: m,
|
||||
text: alarmNameTextField.text ?? "Alarm",
|
||||
wakeMethod: wvms[wvmPicker.selectedRow(inComponent: 0)],
|
||||
lastActivate: Date())
|
||||
lastActivate: Date(), alarmTone: ringtones[ringtonePicker.selectedRow(inComponent: 0)].tone, toneName: ringtones[ringtonePicker.selectedRow(inComponent: 0)].name)
|
||||
|
||||
// TODO: Set alarm.repeats to correspond with what the user selects
|
||||
// Set alarm.repeats to correspond with what the user selects
|
||||
(0...6).forEach { alarm.repeats[$0] = false }
|
||||
if repeatWeekdaysSwitch.isOn { (1...5).forEach { alarm.repeats[$0] = true } }
|
||||
if repeatWeekendsSwitch.isOn { [0, 6].forEach { alarm.repeats[$0] = true } }
|
||||
|
||||
|
||||
// Add the alarm to the list and save the list
|
||||
Alarms.fromLocal().apply { $0.list.append(alarm) }.localSave();
|
||||
|
||||
// Dismiss this view
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
return alarm
|
||||
}
|
||||
|
||||
/**
|
||||
Dynamically the ETA label for the alarm
|
||||
*/
|
||||
func updateETA() {
|
||||
let timeTill = createAlarm().nextActivate!.timeIntervalSince(Date()).str()
|
||||
timeTillAlarmLabel.text = "Going off in \(timeTill)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,3 +191,31 @@ class WVMDataSource: UIPickerView, UIPickerViewDelegate, UIPickerViewDataSource
|
||||
return wvms[r].name + " - " + wvms[r].desc
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RingtonesDataSource: UIPickerView, UIPickerViewDelegate, UIPickerViewDataSource
|
||||
{
|
||||
required init?(coder: NSCoder)
|
||||
{
|
||||
super.init(coder: coder)
|
||||
delegate = self
|
||||
dataSource = self
|
||||
}
|
||||
|
||||
func numberOfComponents(in pickerView: UIPickerView) -> Int
|
||||
{
|
||||
return 1
|
||||
}
|
||||
|
||||
func pickerView(_ v: UIPickerView, numberOfRowsInComponent: Int) -> Int
|
||||
{
|
||||
return ringtones.count
|
||||
}
|
||||
|
||||
func pickerView(_ v: UIPickerView, titleForRow r: Int, forComponent: Int) -> String?
|
||||
{
|
||||
return ringtones[r].name
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,115 +7,185 @@
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
import CoreMotion
|
||||
|
||||
class AlarmActivationViewController: UIViewController
|
||||
var motion = CMMotionManager()
|
||||
var alarmStarted = false
|
||||
|
||||
/**
|
||||
View controlling alarm activation and dismissal
|
||||
*/
|
||||
class AlarmActivationViewController: EndEditingOnReturn
|
||||
{
|
||||
var timer: Timer?
|
||||
var currentAlarm: Alarm?
|
||||
var currentAlarm: Alarm
|
||||
|
||||
//Puzzle outlets
|
||||
// Puzzle outlets
|
||||
@IBOutlet weak var puzzleView: UIView!
|
||||
@IBOutlet weak var puzzleQuestionLabel: UILabel!
|
||||
@IBOutlet weak var puzzleAnswerInput: UITextField!
|
||||
var puzzleAnswers: [Int] = []
|
||||
|
||||
//RPS Outlets
|
||||
// RPS Outlets
|
||||
@IBOutlet weak var rpsView: UIView!
|
||||
@IBOutlet weak var rpsResult: UILabel!
|
||||
|
||||
// Shake Outlets
|
||||
@IBOutlet weak var shakeView: UIView!
|
||||
|
||||
// Other Outlets
|
||||
@IBOutlet weak var timeLabel: UILabel!
|
||||
@IBOutlet weak var dateLabel: UILabel!
|
||||
|
||||
var solved = false
|
||||
|
||||
/**
|
||||
Constructor to receive alarm data from segue
|
||||
*/
|
||||
init?(coder: NSCoder, currentAlarm: Alarm)
|
||||
{
|
||||
self.currentAlarm = currentAlarm
|
||||
//print(currentAlarm.wakeMethod)
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
/**
|
||||
Unused init
|
||||
*/
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
/**
|
||||
Called when the alarm activates
|
||||
*/
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
//Hide all inactive wakemethods
|
||||
puzzleView.isHidden = true
|
||||
rpsView.isHidden = true
|
||||
|
||||
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(AlarmActivationViewController.playSound), userInfo: nil, repeats: true)
|
||||
setAlarmType()
|
||||
//print(MathExpression.random())
|
||||
// Set the time and date
|
||||
dateLabel.text = Date().str("MMM d, Y")
|
||||
timeLabel.text = currentAlarm.timeText
|
||||
|
||||
// Hide all inactive wakemethods
|
||||
puzzleView.hide()
|
||||
rpsView.hide()
|
||||
shakeView.hide()
|
||||
|
||||
// Play sound
|
||||
playSound()
|
||||
vibrate()
|
||||
|
||||
// Run alarm
|
||||
runAlarm()
|
||||
|
||||
// End edit on return
|
||||
puzzleAnswerInput.delegate = self
|
||||
}
|
||||
|
||||
@objc func playSound()
|
||||
/**
|
||||
Play alarm sound
|
||||
*/
|
||||
func playSound()
|
||||
{
|
||||
AudioServicesPlayAlertSound(SystemSoundID(1005))
|
||||
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
|
||||
}
|
||||
|
||||
func setAlarmType()
|
||||
{
|
||||
if let alarm = currentAlarm
|
||||
{
|
||||
switch alarm.wakeMethod.name {
|
||||
case "Walk":
|
||||
walkAction()
|
||||
case "Jump":
|
||||
jumpAction()
|
||||
case "Factor":
|
||||
self.puzzleAnswers = factorAction(puzzleQuestionLabel: puzzleQuestionLabel)
|
||||
puzzleView.isHidden = false
|
||||
case "Smash":
|
||||
print("")
|
||||
case "RPS":
|
||||
rpsView.isHidden = false
|
||||
//Get Choice here
|
||||
//rpsAction(choice: choice)
|
||||
default:
|
||||
print("Invalid alarm type")
|
||||
}
|
||||
AudioServicesPlayAlertSoundWithCompletion(currentAlarm.alarmTone) {
|
||||
if alarmStarted { self.playSound() }
|
||||
}
|
||||
}
|
||||
|
||||
//Verfies and ends factoring WVM
|
||||
@IBAction func checkBinomialSolution(_ sender: Any) {
|
||||
if let input = puzzleAnswerInput.text {
|
||||
if let numericalInput = Int(input) {
|
||||
if puzzleAnswers.contains(numericalInput) {
|
||||
endAlarm()
|
||||
|
||||
func vibrate()
|
||||
{
|
||||
AudioServicesPlayAlertSoundWithCompletion(kSystemSoundID_Vibrate) {
|
||||
if alarmStarted { self.vibrate() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Run alarm dismissal logic
|
||||
*/
|
||||
func runAlarm()
|
||||
{
|
||||
// Check if the device has accelerometer
|
||||
var wvm = currentAlarm.wakeMethod.name
|
||||
if wvm == "Shake" && !motion.isDeviceMotionAvailable
|
||||
{
|
||||
msg("Error", "Accelerometer is not available on your device, so shaking your phone wouldn't work.")
|
||||
wvm = "Factor"
|
||||
}
|
||||
|
||||
// Initialize alarm
|
||||
switch wvm
|
||||
{
|
||||
case "Math 1", "Math 2", "Math 3":
|
||||
let q = MathExpProblem(size: Int(wvm[5...5])!)
|
||||
puzzleQuestionLabel.text = "Solve: \(q.prob.replacingOccurrences(of: "**", with: "^"))"
|
||||
puzzleAnswers = [q.ans]
|
||||
puzzleView.show()
|
||||
case "Factor":
|
||||
initFactorProblem()
|
||||
puzzleView.show()
|
||||
case "RPS":
|
||||
rpsView.show()
|
||||
case "Shake":
|
||||
shakeView.show()
|
||||
|
||||
// Start motion detection
|
||||
let q = OperationQueue()
|
||||
motion.deviceMotionUpdateInterval = 0.2
|
||||
motion.startDeviceMotionUpdates(to: q) { data, error in
|
||||
if let a = data?.userAcceleration, sqrt(pow(a.x, 2) + pow(a.y, 2) + pow(a.z, 2)) > 4
|
||||
{
|
||||
ui { self.endAlarm() }
|
||||
motion.stopDeviceMotionUpdates()
|
||||
q.cancelAllOperations()
|
||||
}
|
||||
}
|
||||
default:
|
||||
print("Invalid alarm type")
|
||||
}
|
||||
|
||||
alarmStarted = true
|
||||
}
|
||||
|
||||
//Gets RPS choice
|
||||
@IBAction func rockChoice(_ sender: Any) {
|
||||
if rpsAction(choice: .rock)! {
|
||||
func initFactorProblem()
|
||||
{
|
||||
let problem = QuadraticProb()
|
||||
puzzleAnswers = problem.getAnswer()
|
||||
|
||||
puzzleQuestionLabel.text = "Solve: \(problem.getProblem())"
|
||||
print("Answer: \(puzzleAnswers)")
|
||||
}
|
||||
|
||||
/**
|
||||
Verfies and ends factoring WVM
|
||||
*/
|
||||
@IBAction func checkBinomialSolution(_ sender: Any)
|
||||
{
|
||||
if let input = puzzleAnswerInput.text,
|
||||
let numericalInput = Int(input),
|
||||
puzzleAnswers.contains(numericalInput)
|
||||
{
|
||||
endAlarm()
|
||||
} else {
|
||||
rpsResult.text = "Paper: You lost, try again"
|
||||
}
|
||||
}
|
||||
@IBAction func paperChoice(_ sender: Any) {
|
||||
if rpsAction(choice: .paper)! {
|
||||
|
||||
/**
|
||||
Gets RPS choice
|
||||
*/
|
||||
@IBAction func rpsChoice(_ sender: UIButton)
|
||||
{
|
||||
if RPS.playRPS(you: [.rock, .paper, .scissors][sender.tag], computer: RPS.choices.randomElement()!)
|
||||
{
|
||||
endAlarm()
|
||||
} else {
|
||||
rpsResult.text = "Scissors: You lost, try again"
|
||||
}
|
||||
}
|
||||
@IBAction func scissorChoice(_ sender: Any) {
|
||||
if rpsAction(choice: .scissors)! {
|
||||
endAlarm()
|
||||
} else {
|
||||
rpsResult.text = "Rock: You lost, try again"
|
||||
else
|
||||
{
|
||||
rpsResult.text = "\(["Paper", "Scissors", "Rock"][sender.tag]): You lost, try again"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Standard way to turn off and close the alarm
|
||||
func endAlarm() {
|
||||
timer?.invalidate()
|
||||
/**
|
||||
Standard way to turn off and close the alarm
|
||||
*/
|
||||
func endAlarm()
|
||||
{
|
||||
alarmStarted = false
|
||||
print("Alarm solved")
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@@ -22,9 +22,24 @@ class AlarmActivator: UITabBarController
|
||||
var timer: Timer?
|
||||
var alarm: Alarm?
|
||||
|
||||
/// Timer for getting family alarm updates
|
||||
var familyTimer: Timer?
|
||||
|
||||
/**
|
||||
Called when the app started
|
||||
*/
|
||||
override func viewDidLoad()
|
||||
{
|
||||
start()
|
||||
|
||||
// Get notification permissions from user
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
|
||||
if success {
|
||||
print("All set!")
|
||||
} else if let error = error {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,6 +49,7 @@ class AlarmActivator: UITabBarController
|
||||
{
|
||||
if timer != nil { return }
|
||||
timer = Timer.scheduledTimer(timeInterval: AlarmActivator.interval, target: self, selector: #selector(AlarmActivator.check), userInfo: nil, repeats: true)
|
||||
familyTimer = Timer.scheduledTimer(timeInterval: 20.0, target: self, selector: #selector(AlarmActivator.checkFamily), userInfo: nil, repeats: true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,17 +76,57 @@ class AlarmActivator: UITabBarController
|
||||
alarm.apply {
|
||||
$0.lastActivate = Date()
|
||||
if $0.oneTime { $0.enabled = false }
|
||||
else {
|
||||
Notification.scheduleNotification(alarm: alarm)
|
||||
}
|
||||
}
|
||||
|
||||
alarms.localSave()
|
||||
self.alarm = alarm
|
||||
// Segue
|
||||
//NSLog(JSON.stringify(alarm)!)
|
||||
performSegue(withIdentifier: "activate-alarm", sender: alarm)
|
||||
|
||||
// Avoid starting duplicate alarms
|
||||
if !alarmStarted { performSegue(withIdentifier: "activate-alarm", sender: alarm) }
|
||||
}
|
||||
|
||||
@IBSegueAction func sendAlarm(_ coder: NSCoder) -> AlarmActivationViewController? {
|
||||
@IBSegueAction func sendAlarm(_ coder: NSCoder) -> AlarmActivationViewController?
|
||||
{
|
||||
return AlarmActivationViewController(coder: coder, currentAlarm: alarm!)
|
||||
}
|
||||
|
||||
/**
|
||||
Check family alarm updates
|
||||
*/
|
||||
@objc func checkFamily()
|
||||
{
|
||||
guard localStorage.string(forKey: "family") != nil else { return }
|
||||
|
||||
send(APIs.familyAlarmUpdates)
|
||||
{
|
||||
guard $0 != "" else { return }
|
||||
|
||||
// Update alarms list
|
||||
var changed = false
|
||||
let alarms = Alarms.fromLocal()
|
||||
$0.csv.forEach
|
||||
{
|
||||
guard let alarm = JSON.parse(Alarm.self, $0) else { return }
|
||||
if (!alarms.list.contains { $0.timeText == alarm.timeText })
|
||||
{
|
||||
alarms.list.append(alarm)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
alarms.localSave()
|
||||
|
||||
// Update UI
|
||||
guard changed else { return }
|
||||
ui
|
||||
{
|
||||
self.msg("New alarm!", "A family member added an alarm for you!")
|
||||
{
|
||||
AlarmViewController.staticTable?.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 3.0 MiB |
@@ -8,7 +8,7 @@ class AlarmViewController: UIViewController
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
// Assign table delegate and data source
|
||||
AlarmViewController.staticTable = table
|
||||
table.delegate = self
|
||||
@@ -46,6 +46,14 @@ extension AlarmViewController: UITableViewDelegate, UITableViewDataSource
|
||||
|
||||
/// IDK what this does (TODO: Find out what this does)
|
||||
func tableView(_ v: UITableView, didSelectRowAt i: IndexPath) { v.deselectRow(at: i, animated: true) }
|
||||
|
||||
/// Sends the selected alarm to be edited
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if segue.identifier == "edit-alarm" {
|
||||
let vc = segue.destination as! AddAlarmViewController
|
||||
vc.alarmCell = (sender as! AlarmTableCell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,35 +67,70 @@ class AlarmTableCell: UITableViewCell
|
||||
@IBOutlet weak var enable: UISwitch!
|
||||
@IBOutlet weak var repeatText: UILabel!
|
||||
@IBOutlet weak var goingOffText: UILabel!
|
||||
@IBOutlet weak var wvmText: UILabel!
|
||||
@IBOutlet weak var toneLabel: UILabel!
|
||||
|
||||
/// Update information on the cell to information in the alarm object
|
||||
//WARNING:Terrible code lies ahead! You WILL be dissapointed! But it works, and that is all that matters.
|
||||
|
||||
var alarm: Alarm!
|
||||
|
||||
/**
|
||||
Update information on the cell to information in the alarm object
|
||||
*/
|
||||
func setData(_ alarm: Alarm)
|
||||
{
|
||||
self.alarm = alarm
|
||||
descriptionText.text = "- " + alarm.text
|
||||
enable.isOn = alarm.enabled
|
||||
wvmText.text = alarm.wakeMethod.name
|
||||
toneLabel.text = ringtones.first { $0.tone.description == alarm.alarmTone.description }?.name
|
||||
|
||||
// Display Hour, Minute, and AM or PM
|
||||
ampm.text = alarm.hour < 12 || alarm.hour == 24 ? "AM" : "PM"
|
||||
let hour = alarm.hour <= 12 ? alarm.hour : alarm.hour - 12
|
||||
time.text = alarm.minute < 10 ? "\(hour):0\(alarm.minute)" : "\(hour):\(alarm.minute)"
|
||||
var hour = alarm.hour <= 12 ? alarm.hour : alarm.hour - 12
|
||||
hour = alarm.hour == 0 ? 12 : hour
|
||||
time.text = String(format: "%i:%02i", hour, alarm.minute)
|
||||
|
||||
// displays the specific days alarm is activated
|
||||
let daysDict = ["Sun", "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat"]
|
||||
var daysActive : [String] = []
|
||||
if alarm.oneTime {repeatText.text = "No Repeat"}
|
||||
if alarm.oneTime {repeatText.text = "One-time Alarm"}
|
||||
else {
|
||||
for (index, element) in alarm.repeats.enumerated() {
|
||||
if element {
|
||||
daysActive.append(daysDict[index])
|
||||
}
|
||||
}
|
||||
repeatText.text = daysActive.joined(separator: ", ")
|
||||
if daysDict == daysActive {
|
||||
repeatText.text = "Repeats: Daily"
|
||||
} else if daysActive == ["Sun", "Sat"] {
|
||||
repeatText.text = "Repeats: Weekends"
|
||||
} else if daysActive == ["Mon", "Tues", "Wed", "Thurs", "Fri"] {
|
||||
repeatText.text = "Repeats: Weekdays"
|
||||
} else {
|
||||
repeatText.text = daysActive.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
updateActivationTime()
|
||||
}
|
||||
|
||||
func updateActivationTime()
|
||||
{
|
||||
// Show next activation date
|
||||
if alarm.enabled, let n = alarm.nextActivate {
|
||||
goingOffText.text = "(Going off in \(n.timeIntervalSince(Date()).str()))"
|
||||
}
|
||||
else {
|
||||
goingOffText.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Called when the user switches the switch
|
||||
*/
|
||||
@IBAction func switchChange(_ sender: Any)
|
||||
{
|
||||
Alarms.fromLocal().apply {
|
||||
$0.list.first { $0.hour == self.alarm.hour && $0.minute == self.alarm.minute }?.enabled = enable.isOn
|
||||
}.localSave()
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -1,91 +1,109 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120-1.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "40-1.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58-1.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "40-2.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "80-1.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "appstore.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// TestingViewController.swift
|
||||
// ProjectClock
|
||||
//
|
||||
// Created by Aaron Saporito on 1/13/21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
class DebugViewController: EndEditingOnReturn
|
||||
{
|
||||
@IBOutlet weak var userModeButton: UIButton!
|
||||
var darkMode = false
|
||||
|
||||
@IBOutlet weak var wvmInput: UITextField!
|
||||
@IBOutlet weak var wvmStepper: UIStepper!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
wvmStepper.maximumValue = Double(wvms.count - 1)
|
||||
|
||||
// End editing on return
|
||||
wvmInput.delegate = self
|
||||
}
|
||||
|
||||
//Sends a test notification
|
||||
@IBAction func sendNotification(_ sender: Any)
|
||||
{
|
||||
Notification.scheduleNotification(alarm: Alarms.fromLocal().listEnabled[0])
|
||||
}
|
||||
|
||||
@IBAction func addAlarm(_ sender: Any)
|
||||
{
|
||||
let (h, m, _) = Date().getHMS()
|
||||
let alarm = Alarm(hour: h, minute: m, text: "Test alarm - \(h * m)", wakeMethod: wvms[Int(wvmStepper.value)], repeats: [true, true, true, true, true, true, true], lastActivate: Date().added(.minute, -1))
|
||||
|
||||
Alarms.fromLocal().apply { $0.list.append(alarm) }.localSave()
|
||||
Notification.scheduleNotification(alarm: alarm)
|
||||
}
|
||||
|
||||
@IBAction func deleteAlarm(_ sender: Any)
|
||||
{
|
||||
Alarms.fromLocal().apply { $0.list.removeAll() }.localSave()
|
||||
}
|
||||
|
||||
@IBAction func wvmStepperChange(_ sender: Any)
|
||||
{
|
||||
wvmInput.text = String(Int(wvmStepper.value))
|
||||
}
|
||||
|
||||
@IBAction func switchViewingMode(_ sender: Any) {
|
||||
if !darkMode {
|
||||
view.window?.overrideUserInterfaceStyle = .dark
|
||||
userModeButton.setTitle("Switch to Light Mode", for: .normal)
|
||||
darkMode = true
|
||||
} else {
|
||||
view.window?.overrideUserInterfaceStyle = .light
|
||||
userModeButton.setTitle("Switch to Dark Mode", for: .normal)
|
||||
darkMode = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreMotion
|
||||
|
||||
/**
|
||||
Math element for problem generation (Credit: https://stackoverflow.com/a/43132311/7346633)
|
||||
@@ -39,11 +40,10 @@ enum MathOperator : String {
|
||||
case plus = "+"
|
||||
case minus = "-"
|
||||
case multiply = "*"
|
||||
case divide = "/"
|
||||
case power = "**"
|
||||
|
||||
static func random() -> MathOperator {
|
||||
let allMathOperators: [MathOperator] = [.plus, .minus, .multiply, .divide, .power]
|
||||
let allMathOperators: [MathOperator] = [.plus, .minus, .multiply, .power]
|
||||
let index = Int(arc4random_uniform(UInt32(allMathOperators.count)))
|
||||
|
||||
return allMathOperators[index]
|
||||
@@ -82,50 +82,42 @@ class MathExpression : CustomStringConvertible {
|
||||
return "\(leftString) \(self.op.rawValue) \(rightString)"
|
||||
}
|
||||
|
||||
var result : Int? {
|
||||
var result: Int {
|
||||
let format = "\(lhs.nsExpressionFormatString) \(op.rawValue) \(rhs.nsExpressionFormatString)"
|
||||
let expr = NSExpression(format: format)
|
||||
return expr.expressionValue(with: nil, context: nil) as? Int
|
||||
let result = expr.expressionValue(with: nil, context: nil)
|
||||
return Int(round(result as! Double))
|
||||
}
|
||||
|
||||
static func random() -> MathExpression {
|
||||
let op: MathOperator = .random()
|
||||
let lhs = MathElement.Integer(value: Int(arc4random_uniform(10)))
|
||||
let rhs = MathElement.Integer(value: Int(arc4random_uniform(10)))
|
||||
let rhs = MathElement.Integer(value: Int(arc4random_uniform(op == .power ? 3 : 10)))
|
||||
|
||||
return MathExpression(lhs: lhs, rhs: rhs, op: .random())
|
||||
return MathExpression(lhs: lhs, rhs: rhs, op: op)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Generate simple problem - 2 expressions
|
||||
*/
|
||||
class AlgProb2 : MathExpression {
|
||||
let a = MathExpression.random()
|
||||
let b = MathExpression.random()
|
||||
class MathExpProblem
|
||||
{
|
||||
let prob: String
|
||||
let ans: Int
|
||||
|
||||
func getProblem() -> String {
|
||||
return "\(a) + \(b)"
|
||||
}
|
||||
|
||||
func getAnswer() -> String {
|
||||
return "\(a.result! + b.result!)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Generate simple problem - 3 expressions
|
||||
*/
|
||||
class AlgProb3 : MathExpression {
|
||||
let a = MathExpression.random()
|
||||
let b = MathExpression.random()
|
||||
let c = MathExpression.random()
|
||||
|
||||
func getProblem() -> String {
|
||||
return "\(a) + \(b) + \(c)"
|
||||
}
|
||||
|
||||
func getAnswer() -> String {
|
||||
return "\(a.result! + b.result! + c.result!)"
|
||||
init(size: Int)
|
||||
{
|
||||
var expressions: [String] = []
|
||||
var answer = 0
|
||||
for _ in 1...size
|
||||
{
|
||||
let exp = MathExpression.random()
|
||||
expressions.append(exp.description)
|
||||
answer += exp.result
|
||||
}
|
||||
prob = expressions.joined(separator: " + ")
|
||||
ans = answer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,43 +148,25 @@ class QuadraticProb {
|
||||
}
|
||||
}
|
||||
|
||||
class RPS {
|
||||
//@IBOutlet weak var resultsLabel: UILabel!
|
||||
/**
|
||||
Rock paper scissors
|
||||
*/
|
||||
class RPS
|
||||
{
|
||||
static let choices: [Choice] = [.rock, .paper, .scissors]
|
||||
|
||||
enum Choice: String {
|
||||
case rock = "ROCK"
|
||||
case paper = "PAPER"
|
||||
case scissors = "SCISSORS"
|
||||
enum Choice: String
|
||||
{
|
||||
case rock = "Rock"
|
||||
case paper = "Paper"
|
||||
case scissors = "Scissors"
|
||||
}
|
||||
|
||||
static func randomComputerChoice() -> Choice {
|
||||
let choices: [Choice] = [.rock, .paper, .scissors]
|
||||
return choices[Int.random(in: 0...2)]
|
||||
static func playRPS(you: Choice, computer: Choice) -> Bool
|
||||
{
|
||||
return you == .rock && computer == .scissors ||
|
||||
you == .paper && computer == .rock ||
|
||||
you == .scissors && computer == .paper
|
||||
}
|
||||
|
||||
func playRPS(you: Choice, computer: Choice) -> Bool? {
|
||||
if you == .rock && computer == .scissors { return true }
|
||||
else if you == .paper && computer == .rock { return true}
|
||||
else if you == .scissors && computer == .paper { return true }
|
||||
else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@IBAction func rock(_ sender: UIButton) {
|
||||
let computerChoice = Choice.randomComputerChoice()
|
||||
resultsLabel.text = playRPS(you: .rock, computer: computerChoice)
|
||||
}
|
||||
|
||||
@IBAction func paper(_ sender: UIButton) {
|
||||
let computerChoice = Choice.randomComputerChoice()
|
||||
resultsLabel.text = playRPS(you: .paper, computer: computerChoice)
|
||||
}
|
||||
|
||||
@IBAction func scissors(_ sender: UIButton) {
|
||||
let computerChoice = Choice.randomComputerChoice()
|
||||
resultsLabel.text = playRPS(you: .scissors, computer: computerChoice)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
@@ -6,51 +6,89 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct User: Codable
|
||||
{
|
||||
var id: Int
|
||||
var name: String
|
||||
var email: String
|
||||
var pass: String
|
||||
}
|
||||
import AVFoundation
|
||||
|
||||
struct Family: Codable
|
||||
{
|
||||
var fid: Int
|
||||
var fname: String
|
||||
var members: [String]
|
||||
var name: String
|
||||
var members: String
|
||||
// And a hidden field: admin pin
|
||||
|
||||
var membersList: [String] { members.csv }
|
||||
|
||||
/// Save family to local storage
|
||||
func localSave()
|
||||
{
|
||||
localStorage.setValue(JSON.stringify(self)!, forKey: "family")
|
||||
}
|
||||
|
||||
/// Read family object from local storage
|
||||
static func fromLocal() -> Family?
|
||||
{
|
||||
guard let f = localStorage.string(forKey: "family") else { return nil }
|
||||
return JSON.parse(Family.self, f)
|
||||
}
|
||||
}
|
||||
|
||||
struct WVM: Codable
|
||||
{
|
||||
let index: Int
|
||||
let name: String
|
||||
let desc: String
|
||||
}
|
||||
|
||||
|
||||
let wvms = [
|
||||
WVM(name: "Factor", desc: "Factor a binomial"),
|
||||
WVM(name: "RPS", desc: "Win a game of rock paper scissors"),
|
||||
WVM(name: "Smash", desc: "It'll never turn off"),
|
||||
WVM(name: "Walk", desc: "Walk a few steps"),
|
||||
WVM(name: "Jump", desc: "Make a few jumps")
|
||||
WVM(index: 0, name: "Shake", desc: "Shake your phone... aggresively!"),
|
||||
WVM(index: 1, name: "Math 1", desc: "Easy math expression"),
|
||||
WVM(index: 2, name: "Math 2", desc: "Medium math expression"),
|
||||
WVM(index: 3, name: "Math 3", desc: "Hard math expression"),
|
||||
WVM(index: 4, name: "Factor", desc: "Factor a binomial"),
|
||||
WVM(index: 5, name: "RPS", desc: "Win a game of rock paper scissors"),
|
||||
//WVM(name: "Smash", desc: "It'll never turn off"),
|
||||
//WVM(name: "Walk", desc: "Walk a few steps"),
|
||||
//WVM(name: "Jump", desc: "Make a few jumps")
|
||||
]
|
||||
|
||||
class Alarm: Codable
|
||||
|
||||
struct Tone: Codable{
|
||||
|
||||
let name: String
|
||||
let tone: SystemSoundID
|
||||
|
||||
}
|
||||
|
||||
let ringtones = [
|
||||
Tone(name: "News Flash", tone: SystemSoundID(1028)),
|
||||
Tone(name: "Sherwood Forest", tone: SystemSoundID(1030)),
|
||||
Tone(name: "Ladder", tone: SystemSoundID(1326)),
|
||||
Tone(name: "Minuet", tone: SystemSoundID(1327)),
|
||||
Tone(name: "Tock", tone: SystemSoundID(1306)),
|
||||
Tone(name: "Bloom", tone: SystemSoundID(1321)),
|
||||
Tone(name: "Calypso", tone: SystemSoundID(1322)),
|
||||
Tone(name: "Train", tone: SystemSoundID(1323)),
|
||||
Tone(name: "Fanfare", tone: SystemSoundID(1325))
|
||||
]
|
||||
|
||||
class Alarm: Codable, Equatable
|
||||
{
|
||||
static func == (lhs: Alarm, rhs: Alarm) -> Bool {
|
||||
return lhs.hour == rhs.hour && lhs.minute == rhs.minute && lhs.text == rhs.text &&
|
||||
lhs.alarmTone == rhs.alarmTone && lhs.repeats == rhs.repeats
|
||||
}
|
||||
|
||||
var enabled: Bool
|
||||
var hour: Int // Hour (24)
|
||||
var minute: Int
|
||||
var text: String
|
||||
var wakeMethod: WVM
|
||||
var alarmTone: SystemSoundID
|
||||
var notificationID: String
|
||||
|
||||
/// What days does it repeat (Sun, Mon, Tue, Wed, Thu, Fri, Sat)
|
||||
var repeats: [Bool]
|
||||
|
||||
/// Does it automatically disable after activating once
|
||||
var oneTime: Bool
|
||||
|
||||
/// When is the last time that the alarm went off
|
||||
var lastActivate: Date
|
||||
|
||||
@@ -58,7 +96,10 @@ class Alarm: Codable
|
||||
init(enabled: Bool = true,
|
||||
hour: Int, minute: Int, text: String, wakeMethod: WVM,
|
||||
repeats: [Bool] = [false, true, true, true, true, true, false],
|
||||
oneTime: Bool = false, lastActivate: Date = Date()
|
||||
lastActivate: Date = Date(),
|
||||
alarmTone: SystemSoundID = ringtones[0].tone,
|
||||
toneName: String = ""
|
||||
|
||||
)
|
||||
{
|
||||
self.enabled = enabled
|
||||
@@ -67,10 +108,17 @@ class Alarm: Codable
|
||||
self.text = text
|
||||
self.wakeMethod = wakeMethod
|
||||
self.repeats = repeats
|
||||
self.oneTime = oneTime
|
||||
self.lastActivate = lastActivate
|
||||
self.alarmTone = alarmTone
|
||||
self.notificationID = "notification.id.\(Int.random(in: 1...Int.max))"
|
||||
}
|
||||
|
||||
/// Does it automatically disable after activating once
|
||||
var oneTime: Bool { repeats.allSatisfy { !$0 } }
|
||||
|
||||
/// Get time in h:mm format
|
||||
var timeText: String { String(format: "%i:%02i", hour, minute) }
|
||||
|
||||
/// When should the alarm activate next since lastActivate?
|
||||
var nextActivate: Date?
|
||||
{
|
||||
@@ -103,6 +151,7 @@ class Alarms: Codable
|
||||
/// Save alarms to local storage
|
||||
func localSave()
|
||||
{
|
||||
list.sort { ($0.hour * 60 + $0.minute) < ($1.hour * 60 + $1.minute) }
|
||||
localStorage.setValue(JSON.stringify(list)!, forKey: "alarms")
|
||||
|
||||
// Reload table view
|
||||
|
||||
@@ -84,7 +84,7 @@ class APIs
|
||||
## Returns
|
||||
Success or error
|
||||
*/
|
||||
static let uploadConfig = API<String>(loc: "/backup/upload")
|
||||
static let uploadConfig = API<String>(loc: "/user/backup/upload")
|
||||
|
||||
/**
|
||||
Download the config from the cloud.
|
||||
@@ -95,13 +95,24 @@ class APIs
|
||||
## Returns
|
||||
Config Json
|
||||
*/
|
||||
static let downloadConfig = API<String>(loc: "/backup/download")
|
||||
static let downloadConfig = API<String>(loc: "/user/backup/download")
|
||||
|
||||
/**
|
||||
Get family info for this account
|
||||
|
||||
## Parameters (Besides from username and password)
|
||||
None
|
||||
|
||||
## Returns
|
||||
Family object
|
||||
*/
|
||||
static let familyGet = API<Family>(loc: "/family/get")
|
||||
|
||||
/**
|
||||
Create a family
|
||||
|
||||
## Parameters (Besides from username and password)
|
||||
- fname: Family name
|
||||
- name: Family name
|
||||
- pin: Admin pin
|
||||
|
||||
## Returns
|
||||
@@ -109,54 +120,56 @@ class APIs
|
||||
*/
|
||||
static let familyCreate = API<Family>(loc: "/family/create")
|
||||
|
||||
/**
|
||||
Delete a family
|
||||
|
||||
## Parameters (Besides from username and password)
|
||||
- fid: Family ID
|
||||
- pin: Admin pin
|
||||
|
||||
## Returns
|
||||
Success or not
|
||||
*/
|
||||
static let familyDelete = API<String>(loc: "/family/delete")
|
||||
|
||||
/**
|
||||
Change a family's admin pin
|
||||
|
||||
## Parameters (Besides from username and password)
|
||||
- fid: Family ID
|
||||
- orig_pin: Original admin pin
|
||||
- new_pin: New admin pin
|
||||
- oldPin: Original admin pin
|
||||
- newPin: New admin pin
|
||||
|
||||
## Returns
|
||||
Success or not
|
||||
Updated family object
|
||||
*/
|
||||
static let familyChangePin = API<String>(loc: "/family/update_pin")
|
||||
static let familyChangePin = API<Family>(loc: "/family/update_pin")
|
||||
|
||||
/**
|
||||
Join family
|
||||
Family-related action
|
||||
|
||||
## Parameters (Besides from username and password)
|
||||
- fid: Family ID
|
||||
- pin: Admin pin
|
||||
- action: Join / Leave / Delete
|
||||
|
||||
## Returns
|
||||
Family object
|
||||
*/
|
||||
static let familyJoin = API<Family>(loc: "/family/join")
|
||||
static let familyAction = API<Family>(loc: "/family/action")
|
||||
|
||||
/**
|
||||
Leave family
|
||||
Get updates about alarms that other family members added
|
||||
|
||||
## Parameters (Besides from username and password)
|
||||
None
|
||||
|
||||
## Returns
|
||||
Alarm updates
|
||||
*/
|
||||
static let familyAlarmUpdates = API<String>(loc: "/family/get_alarm_updates")
|
||||
|
||||
/**
|
||||
Add alarm to a family member
|
||||
|
||||
## Parameters (Besides from username and password)
|
||||
- fid: Family ID
|
||||
- pin: Admin pin
|
||||
- to: Family member's username
|
||||
- alarm: Alarm json
|
||||
|
||||
## Returns
|
||||
Success or not
|
||||
Success message
|
||||
*/
|
||||
static let familyLeave = API<String>(loc: "/family/leave")
|
||||
static let familyAddAlarm = API<String>(loc: "/family/add_alarm")
|
||||
|
||||
private init() {}
|
||||
}
|
||||
@@ -197,6 +210,7 @@ func send<T: Decodable>(_ api: API<T>, _ params: [String: String]? = [:], _ succ
|
||||
{
|
||||
if params!["username"] == nil { params!["username"] = localStorage.string(forKey: "name") }
|
||||
if params!["password"] == nil { params!["password"] = localStorage.string(forKey: "pass") }
|
||||
if params!["fid"] == nil, let f = Family.fromLocal() { params!["fid"] = String(f.fid) }
|
||||
}
|
||||
|
||||
// Create task
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Notification.swift
|
||||
// ProjectClock
|
||||
//
|
||||
// Created by Aaron Saporito on 1/24/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
|
||||
class Notification {
|
||||
|
||||
//Removes the notifiation
|
||||
static func removeNotification(alarm: Alarm) {
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests { (notificationRequests) in
|
||||
var identifiers: [String] = []
|
||||
for notification:UNNotificationRequest in notificationRequests {
|
||||
if notification.identifier == alarm.notificationID {
|
||||
identifiers.append(notification.identifier)
|
||||
}
|
||||
}
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicates and adds any missing notifications
|
||||
static func bulkScheduleNotifications(alarms: [Alarm]) {
|
||||
var ids: [String] = []
|
||||
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests { (notificationRequests) in
|
||||
for notification:UNNotificationRequest in notificationRequests {
|
||||
ids.append(notification.identifier)
|
||||
}
|
||||
for alarm in alarms {
|
||||
if !ids.contains(alarm.notificationID) {
|
||||
scheduleNotification(alarm: alarm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func scheduleNotification(alarm: Alarm) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
//Date formatting to string
|
||||
let today = Date()
|
||||
let formatter1 = DateFormatter()
|
||||
formatter1.dateFormat = "MMM d, h:mm"
|
||||
|
||||
//Notification content
|
||||
content.title = alarm.text
|
||||
content.subtitle = formatter1.string(from: today)
|
||||
content.body = "Wake method: \(alarm.wakeMethod.name)"
|
||||
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "notif.caf"))
|
||||
|
||||
// Notification image content
|
||||
let imageName = "AlarmLogo1"
|
||||
guard let imageURL = Bundle.main.url(forResource: imageName, withExtension: "png") else { return }
|
||||
let attachment = try! UNNotificationAttachment(identifier: imageName, url: imageURL, options: .none)
|
||||
content.attachments = [attachment]
|
||||
|
||||
// Scheduels alarm notification for proper time
|
||||
let interval = alarm.nextActivate!.timeIntervalSince(Date())
|
||||
guard interval > 0 else { return }
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: alarm.nextActivate!.timeIntervalSince(Date()), repeats: false)
|
||||
let request = UNNotificationRequest(identifier: alarm.notificationID, content: content, trigger: trigger)
|
||||
|
||||
// Sends notification
|
||||
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// NotificationLogic.swift
|
||||
// ProjectClock
|
||||
//
|
||||
// Created by Aaron Saporito on 1/13/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreMotion
|
||||
import UserNotifications
|
||||
import UIKit
|
||||
|
||||
func walkAction() {
|
||||
|
||||
}
|
||||
|
||||
func jumpAction() {
|
||||
|
||||
}
|
||||
|
||||
func rpsAction(choice: RPS.Choice) -> Bool? {
|
||||
let rps = RPS()
|
||||
return rps.playRPS(you: choice, computer: RPS.randomComputerChoice())
|
||||
|
||||
}
|
||||
|
||||
// Handles the core logic behind the factoring alarm
|
||||
func factorAction(puzzleQuestionLabel: UILabel) -> [Int] {
|
||||
let problem = QuadraticProb()
|
||||
|
||||
let answer = problem.getAnswer()
|
||||
let problemString = problem.getProblem()
|
||||
|
||||
puzzleQuestionLabel.text = "Solve: \(problemString)"
|
||||
print("Answer: \(answer)")
|
||||
return answer
|
||||
}
|
||||
|
||||
func smashAction() {
|
||||
|
||||
}
|
||||
|
||||
@@ -7,123 +7,149 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class StopwatchViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var hourLabel: UILabel!
|
||||
@IBOutlet weak var minuteLabel: UILabel!
|
||||
@IBOutlet weak var secondLabel: UILabel!
|
||||
|
||||
/**
|
||||
Stopwatch feature
|
||||
*/
|
||||
class StopwatchViewController: UIViewController
|
||||
{
|
||||
// UI Components
|
||||
@IBOutlet weak var timeLabel: UILabel!
|
||||
@IBOutlet weak var startButton: UIButton!
|
||||
@IBOutlet weak var stopButton: UIButton!
|
||||
@IBOutlet weak var resetButton: UIButton!
|
||||
@IBOutlet weak var lapButton: UIButton!
|
||||
|
||||
@IBOutlet weak var tableView: UITableView!
|
||||
|
||||
// Time Components
|
||||
var hours = 0
|
||||
var minutes = 0
|
||||
var seconds = 0
|
||||
var started = false
|
||||
|
||||
var lappedTimes: [String] = []
|
||||
var timer = Timer()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
//lapButton.isHidden = true
|
||||
/**
|
||||
Start/stop stopwatch
|
||||
*/
|
||||
@IBAction func start(_ sender: UIButton)
|
||||
{
|
||||
if !started
|
||||
{
|
||||
// Start timer
|
||||
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(count), userInfo: nil, repeats: true)
|
||||
started = true
|
||||
startButton.setTitle("Stop", for: .normal)
|
||||
resetButton.setTitle("Lap", for: .normal)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stop timer
|
||||
timer.invalidate()
|
||||
started = false
|
||||
startButton.setTitle("Start", for: .normal)
|
||||
resetButton.setTitle("Reset", for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func start(_ sender: UIButton) {
|
||||
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(count), userInfo: nil, repeats: true)
|
||||
//startButton.isHidden = true
|
||||
//lapButton.isHidden = false
|
||||
}
|
||||
|
||||
@objc fileprivate func count() {
|
||||
@objc fileprivate func count()
|
||||
{
|
||||
// Add time (If it goes longer than 24 hours, the hour count should go to 25)
|
||||
seconds += 1
|
||||
|
||||
if seconds == 60 {
|
||||
if seconds == 60
|
||||
{
|
||||
minutes += 1
|
||||
seconds = 0
|
||||
}
|
||||
if minutes == 60 {
|
||||
if minutes == 60
|
||||
{
|
||||
hours += 1
|
||||
minutes = 0
|
||||
}
|
||||
if hours == 24 {
|
||||
resetTimes()
|
||||
|
||||
// Set label text
|
||||
timeLabel.text = String(format: "%02i:%02i:%02i", hours, minutes, seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
Lap/reset button
|
||||
*/
|
||||
@IBAction func lapOrReset(_ sender: UIButton)
|
||||
{
|
||||
if started
|
||||
{
|
||||
// Insert lap
|
||||
lappedTimes.insert(timeLabel.text!, at: 0)
|
||||
tableView.reloadData()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reset
|
||||
seconds = 0
|
||||
minutes = 0
|
||||
seconds = 0
|
||||
lappedTimes = []
|
||||
timeLabel.text = "00:00:00"
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
if seconds >= 10 { secondLabel.text = "\(seconds)" }
|
||||
else { secondLabel.text = "0\(seconds)" }
|
||||
if minutes >= 10 { minuteLabel.text = "\(minutes)" }
|
||||
else { minuteLabel.text = "0\(minutes)" }
|
||||
if hours >= 10 { hourLabel.text = "\(hours)" }
|
||||
else { hourLabel.text = "0\(hours)" }
|
||||
}
|
||||
|
||||
@IBAction func stop(_ sender: UIButton) {
|
||||
timer.invalidate()
|
||||
//startButton.isHidden = false
|
||||
}
|
||||
|
||||
@IBAction func reset(_ sender: UIButton) {
|
||||
resetTimes()
|
||||
}
|
||||
|
||||
func resetTimes() {
|
||||
seconds = 0
|
||||
minutes = 0
|
||||
seconds = 0
|
||||
lappedTimes = []
|
||||
timer.invalidate()
|
||||
secondLabel.text = "00"
|
||||
minuteLabel.text = "00"
|
||||
hourLabel.text = "00"
|
||||
tableView.reloadData()
|
||||
//startButton.isHidden = false
|
||||
//lapButton.isHidden = true
|
||||
}
|
||||
|
||||
@IBAction func lap(_ sender: UIButton) {
|
||||
var currentSec = ""
|
||||
if seconds >= 10 { currentSec = "\(seconds)" }
|
||||
else { currentSec = "0\(seconds)" }
|
||||
|
||||
var currentMin = ""
|
||||
if minutes >= 10 { currentMin = "\(minutes)" }
|
||||
else { currentMin = "0\(minutes)" }
|
||||
|
||||
var currentHour = ""
|
||||
if hours >= 10 { currentHour = "\(hours)" }
|
||||
else { currentHour = "0\(hours)" }
|
||||
|
||||
let currentTime = "\(currentHour):\(currentMin):\(currentSec)" //CHECK THIS
|
||||
lappedTimes.append(currentTime)
|
||||
|
||||
let indexPath = IndexPath(row: lappedTimes.count - 1, section: 0)
|
||||
tableView.insertRows(at: [indexPath], with: .automatic)
|
||||
}
|
||||
}
|
||||
|
||||
extension StopwatchViewController: UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
/**
|
||||
Table data source
|
||||
*/
|
||||
extension StopwatchViewController: UITableViewDelegate, UITableViewDataSource
|
||||
{
|
||||
/**
|
||||
Define row count
|
||||
*/
|
||||
func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int
|
||||
{
|
||||
return lappedTimes.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "lapCell", for: indexPath)
|
||||
cell.textLabel?.text = lappedTimes[indexPath.row]
|
||||
/**
|
||||
Set cell at i
|
||||
*/
|
||||
func tableView(_ view: UITableView, cellForRowAt i: IndexPath) -> UITableViewCell
|
||||
{
|
||||
let cell = view.dequeueReusableCell(withIdentifier: "lapCell", for: i)
|
||||
cell.textLabel?.text = lappedTimes[i.row]
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
||||
if editingStyle == .delete {
|
||||
lappedTimes.remove(at: indexPath.row)
|
||||
|
||||
tableView.deleteRows(at: [indexPath], with: .automatic)
|
||||
/**
|
||||
Swipe left to delete cells at i
|
||||
*/
|
||||
func tableView(_ view: UITableView, commit: UITableViewCell.EditingStyle, forRowAt i: IndexPath)
|
||||
{
|
||||
if commit == .delete
|
||||
{
|
||||
lappedTimes.remove(at: i.row)
|
||||
view.deleteRows(at: [i], with: .automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Class to set relative font size for the stopwatch
|
||||
*/
|
||||
class StopwatchText: UILabel
|
||||
{
|
||||
@IBInspectable var iPhoneFontSize: CGFloat = 0
|
||||
{
|
||||
didSet
|
||||
{
|
||||
overrideFontSize(iPhoneFontSize)
|
||||
}
|
||||
}
|
||||
|
||||
func overrideFontSize(_ fontSize: CGFloat)
|
||||
{
|
||||
let size = UIScreen.main.bounds.size
|
||||
let width = UIDevice.current.orientation.isPortrait ? size.width : size.height
|
||||
|
||||
// ViewWidth-based font size
|
||||
font = font.withSize(0.22 * width)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
class TestingViewController: UIViewController
|
||||
class DebugViewController: UIViewController
|
||||
{
|
||||
override func viewDidLoad()
|
||||
{
|
||||
@@ -27,38 +27,13 @@ class TestingViewController: UIViewController
|
||||
//Sends a test notification
|
||||
@IBAction func sendNotification(_ sender: Any)
|
||||
{
|
||||
let alarm = Alarm(hour: 7, minute: 20, text: "Good morning!", wakeMethod: WVM(name: "walking", desc: "Walk"))
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
//Date formatting to string
|
||||
let today = Date()
|
||||
let formatter1 = DateFormatter()
|
||||
formatter1.dateStyle = .long
|
||||
|
||||
//Notification content
|
||||
content.title = alarm.text
|
||||
content.subtitle = formatter1.string(from: today)
|
||||
content.body = "Wake method: \(alarm.wakeMethod.name)"
|
||||
|
||||
// Notification image content
|
||||
let imageName = "clock"
|
||||
guard let imageURL = Bundle.main.url(forResource: imageName, withExtension: "png") else { return }
|
||||
let attachment = try! UNNotificationAttachment(identifier: imageName, url: imageURL, options: .none)
|
||||
content.attachments = [attachment]
|
||||
|
||||
// Readies notification to be sent
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
|
||||
let request = UNNotificationRequest(identifier: "notification.id.01", content: content, trigger: trigger)
|
||||
|
||||
// Sends notification
|
||||
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
|
||||
Notification(alarm: Alarms.fromLocal().listEnabled[0]).scheduleNotification()
|
||||
}
|
||||
|
||||
@IBAction func addAlarm(_ sender: Any)
|
||||
{
|
||||
let (h, m, _) = Date().getHMS()
|
||||
Alarms.fromLocal().apply { $0.list.append(Alarm(hour: h, minute: m, text: "Test alarm - \(h * m)", wakeMethod: wvms[1], repeats: [true, true, true, true, true, true, true], oneTime: true, lastActivate: Date().added(.minute, -1))) }.localSave()
|
||||
Alarms.fromLocal().apply { $0.list.append(Alarm(hour: h, minute: m, text: "Test alarm - \(h * m)", wakeMethod: wvms[1], repeats: [true, true, true, true, true, true, true], lastActivate: Date().added(.minute, -1))) }.localSave()
|
||||
}
|
||||
|
||||
@IBAction func deleteAlarm(_ sender: Any)
|
||||
|
||||
@@ -13,10 +13,10 @@ import UIKit
|
||||
extension Date
|
||||
{
|
||||
/// Add toString to Date
|
||||
func str() -> String
|
||||
func str(_ format: String = "yyyy-MM-dd hh:mm:ss") -> String
|
||||
{
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "yyyy-MM-dd hh:mm:ss"
|
||||
f.dateFormat = format
|
||||
return f.string(from: self)
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ extension TimeInterval
|
||||
{
|
||||
if days != 0 { return "\(days)d \(hours)h \(minutes)m \(seconds)s" }
|
||||
else if hours != 0 { return "\(hours)h \(minutes)m \(seconds)s" }
|
||||
else if days != 0 && hours == 0 { return "\(days)d \(minutes)m \(seconds)s"}
|
||||
else if minutes != 0 { return "\(minutes)m \(seconds)s" }
|
||||
else { return "\(seconds)s" }
|
||||
}
|
||||
@@ -107,6 +108,7 @@ extension Digest
|
||||
extension String
|
||||
{
|
||||
var sha256: String { SHA256.hash(data: self.data(using: .utf8)!).b64 }
|
||||
var csv: [String] { components(separatedBy: ";") }
|
||||
}
|
||||
|
||||
|
||||
@@ -121,13 +123,13 @@ extension UIViewController
|
||||
- Parameter okayable: Whether the alert can be okayed
|
||||
*/
|
||||
@discardableResult
|
||||
func alert(_ title: String, _ message: String, okayable: Bool = false) -> UIAlertController
|
||||
func alert(_ title: String, _ message: String, okayable: Bool = false, _ completion: (() -> Void)? = nil) -> UIAlertController
|
||||
{
|
||||
// Create alert
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
|
||||
// Add okay button if it's okayable
|
||||
if okayable { alert.addAction(UIAlertAction(title: "OK", style: .default)) }
|
||||
if okayable { alert.addAction(UIAlertAction(title: "OK", style: .default) { it in if let c = completion { c() } }) }
|
||||
|
||||
// Display alert
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
@@ -136,7 +138,10 @@ extension UIViewController
|
||||
|
||||
/// A message is an okayable alert
|
||||
@discardableResult
|
||||
func msg(_ title: String, _ message: String) -> UIAlertController { alert(title, message, okayable: true) }
|
||||
func msg(_ title: String, _ message: String, _ completion: (() -> Void)? = nil) -> UIAlertController
|
||||
{
|
||||
alert(title, message, okayable: true, completion)
|
||||
}
|
||||
|
||||
/// More convenient dismiss function
|
||||
func dismiss(_ completion: (() -> Void)? = nil) { ui { self.dismiss(animated: false, completion: completion) } }
|
||||
@@ -144,24 +149,59 @@ extension UIViewController
|
||||
/**
|
||||
Send a http request even more conveniently
|
||||
*/
|
||||
func sendReq<T: Decodable>(_ api: API<T>, title: String, errors: [String: String] = [:], params: [String: String]? = [:], _ success: @escaping (T) -> Void, err: @escaping (String) -> Void = {it in})
|
||||
func sendReq<T: Decodable>(_ api: API<T>, title: String, errors: [String: String] = [:], params: [String: String]? = [:], _ success: @escaping (T) -> Void, err: ((String) -> Void)? = nil)
|
||||
{
|
||||
// Send request
|
||||
let a = alert(title, "Please Wait")
|
||||
send(api, params) { it in a.dismiss { success(it) } }
|
||||
err:
|
||||
{
|
||||
// Call callback error function
|
||||
if let err = err { err($0); return }
|
||||
|
||||
// Display error message
|
||||
print("===== Error: \($0) =====")
|
||||
let message = errors[$0.trimmingCharacters(in: .whitespaces)]
|
||||
?? "Maybe the server is on fire, just wait a few hours."
|
||||
?? "Maybe the server is on fire, just wait a few hours. (Error: \($0))"
|
||||
a.dismiss { self.msg("An error occurred", message) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Asks the user to enter a pin
|
||||
*/
|
||||
func enterPin(_ title: String = "Enter Pin", _ message: String = "Please enter your family pin.", _ then: @escaping (String) -> Void)
|
||||
{
|
||||
// Create alert
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
|
||||
|
||||
// Add next button
|
||||
alert.addAction(UIAlertAction(title: "Next", style: UIAlertAction.Style.default) { it in
|
||||
let t = alert.textFields![0] as UITextField
|
||||
then(t.text!)
|
||||
})
|
||||
|
||||
// Add pin text field
|
||||
alert.addTextField(configurationHandler: { (t: UITextField!) in
|
||||
t.placeholder = "Enter Pin"
|
||||
t.isSecureTextEntry = true
|
||||
})
|
||||
|
||||
// Present alert
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView
|
||||
{
|
||||
func hide(_ hidden: Bool = true) { isHidden = hidden }
|
||||
func show(_ shown: Bool = true) { hide(!shown) }
|
||||
}
|
||||
|
||||
|
||||
/// Regex Matching (Credit: https://www.hackingwithswift.com/articles/108/how-to-use-regular-expressions-in-swift)
|
||||
/**
|
||||
Regex Matching (Credit: https://www.hackingwithswift.com/articles/108/how-to-use-regular-expressions-in-swift)
|
||||
*/
|
||||
extension NSRegularExpression
|
||||
{
|
||||
convenience init(_ pattern: String)
|
||||
@@ -177,6 +217,9 @@ extension NSRegularExpression
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
String convenience functions
|
||||
*/
|
||||
extension String
|
||||
{
|
||||
static func ~= (lhs: String, rhs: String) -> Bool
|
||||
@@ -185,12 +228,29 @@ extension String
|
||||
let range = NSRange(location: 0, length: lhs.utf16.count)
|
||||
return regex.firstMatch(in: lhs, options: [], range: range) != nil
|
||||
}
|
||||
|
||||
// Better subscripting from: https://stackoverflow.com/a/46627527
|
||||
subscript (bounds: CountableClosedRange<Int>) -> String
|
||||
{
|
||||
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||
let end = index(startIndex, offsetBy: bounds.upperBound)
|
||||
return String(self[start...end])
|
||||
}
|
||||
|
||||
subscript (bounds: CountableRange<Int>) -> String
|
||||
{
|
||||
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||
let end = index(startIndex, offsetBy: bounds.upperBound)
|
||||
return String(self[start..<end])
|
||||
}
|
||||
}
|
||||
|
||||
/// More convenient ui update closure
|
||||
func ui(closure: @escaping () -> Void) { DispatchQueue.main.async { closure() } }
|
||||
|
||||
/// More convenient UserDefaults access (Credit: https://gist.github.com/Otbivnoe/04b8bd7984fba0cb58ca7f136fd95582)
|
||||
/**
|
||||
More convenient UserDefaults access (Credit: https://gist.github.com/Otbivnoe/04b8bd7984fba0cb58ca7f136fd95582)
|
||||
*/
|
||||
extension UserDefaults
|
||||
{
|
||||
subscript<T>(key: String) -> T?
|
||||
@@ -209,3 +269,15 @@ extension UserDefaults
|
||||
set { self[key] = newValue?.rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
class EndEditingOnReturn: UIViewController, UITextFieldDelegate
|
||||
{
|
||||
/**
|
||||
End editing on return
|
||||
*/
|
||||
func textFieldShouldReturn(_ scoreText: UITextField) -> Bool
|
||||
{
|
||||
self.view.endEditing(true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||