├── version.txt ├── settings.gradle ├── gradlew ├── .vscode ├── settings.json └── launch.json ├── src └── main │ ├── java │ ├── scenes │ │ ├── OptionScene.java │ │ ├── SplashScene.java │ │ ├── SceneManager.java │ │ ├── CreditsScene.java │ │ └── GrizzlyScene.java │ ├── exceptions │ │ ├── OpenCvLoadFailureException.java │ │ ├── JsonKeyHasNoDataException.java │ │ ├── ConnectToWorksheetException.java │ │ └── CancelledUserCreationException.java │ ├── Main.java │ ├── databases │ │ ├── BatchUpdateData.java │ │ ├── JSONHelper.java │ │ ├── DatabaseUtils.java │ │ └── DatabaseProcess.java │ ├── activities │ │ ├── KeyActivity.java │ │ ├── LoginActivity.java │ │ ├── LocalDbActivity.java │ │ ├── LogoutActivity.java │ │ └── UserActivity.java │ ├── helpers │ │ ├── CommonUtils.java │ │ ├── LoggingUtils.java │ │ ├── Constants.java │ │ └── AlertUtils.java │ ├── notifiers │ │ ├── LoginNotifier.java │ │ └── UpdateNotifier.java │ └── GrizzlyTime.java │ └── resources │ ├── images │ ├── bear.png │ ├── icon.png │ ├── error.png │ └── splash.jpg │ ├── sounds │ └── ding.wav │ ├── credentials │ └── placeholder.txt │ ├── templates │ └── config.json │ └── styles │ └── root.css ├── wiki_images ├── config.png ├── updating.png ├── config_file.png ├── full_screen.png ├── main_screen.png ├── sheets_home.png ├── sheets_menu.png ├── sheets_url.png ├── structure.jpg ├── verification.png ├── sheets_confirm.png ├── sheets_explorer.png ├── sheets_select_file.png ├── confirmation_dialog.png └── registration_screen.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── CI.yml ├── .gitignore ├── license.txt ├── README.md ├── gradlew.bat └── CODE_OF_CONDUCT.md /version.txt: -------------------------------------------------------------------------------- 1 | 2.4.0 -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'GrizzlyTime' 2 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/gradlew -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic" 3 | } -------------------------------------------------------------------------------- /src/main/java/scenes/OptionScene.java: -------------------------------------------------------------------------------- 1 | package scenes; 2 | 3 | public class OptionScene {} 4 | -------------------------------------------------------------------------------- /wiki_images/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/config.png -------------------------------------------------------------------------------- /wiki_images/updating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/updating.png -------------------------------------------------------------------------------- /wiki_images/config_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/config_file.png -------------------------------------------------------------------------------- /wiki_images/full_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/full_screen.png -------------------------------------------------------------------------------- /wiki_images/main_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/main_screen.png -------------------------------------------------------------------------------- /wiki_images/sheets_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/sheets_home.png -------------------------------------------------------------------------------- /wiki_images/sheets_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/sheets_menu.png -------------------------------------------------------------------------------- /wiki_images/sheets_url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/sheets_url.png -------------------------------------------------------------------------------- /wiki_images/structure.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/structure.jpg -------------------------------------------------------------------------------- /wiki_images/verification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/verification.png -------------------------------------------------------------------------------- /wiki_images/sheets_confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/sheets_confirm.png -------------------------------------------------------------------------------- /wiki_images/sheets_explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/sheets_explorer.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/images/bear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/src/main/resources/images/bear.png -------------------------------------------------------------------------------- /src/main/resources/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/src/main/resources/images/icon.png -------------------------------------------------------------------------------- /src/main/resources/sounds/ding.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/src/main/resources/sounds/ding.wav -------------------------------------------------------------------------------- /wiki_images/sheets_select_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/sheets_select_file.png -------------------------------------------------------------------------------- /src/main/resources/images/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/src/main/resources/images/error.png -------------------------------------------------------------------------------- /src/main/resources/images/splash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/src/main/resources/images/splash.jpg -------------------------------------------------------------------------------- /wiki_images/confirmation_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/confirmation_dialog.png -------------------------------------------------------------------------------- /wiki_images/registration_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/HEAD/wiki_images/registration_screen.png -------------------------------------------------------------------------------- /src/main/resources/credentials/placeholder.txt: -------------------------------------------------------------------------------- 1 | # Instructions 2 | * Place your "credentials.json" in this folder. 3 | (obtained at https://console.cloud.google.com/apis/) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/resources/templates/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "sheet": "", 3 | "handsFreeMode": "false", 4 | "updateNotifier": "true", 5 | "idLength": "6", 6 | "idLengthFallback": "7", 7 | "applicationName": "GrizzlyTime_JavaFX_Edition", 8 | "grizzlyVerification": "false" 9 | } -------------------------------------------------------------------------------- /src/main/java/exceptions/OpenCvLoadFailureException.java: -------------------------------------------------------------------------------- 1 | package exceptions; 2 | 3 | /** @author Dalton Smith OpenCvLoadFailureException Exception thrown if opencv fails to load */ 4 | public class OpenCvLoadFailureException extends Exception { 5 | public OpenCvLoadFailureException(String errorMessage) { 6 | super(errorMessage); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | import javafx.application.Application; 2 | 3 | /** 4 | * This is the true main class of GrizzlyTime, it launches GrizzlyTime through some fancy black 5 | * magic to get JFX working on Java 11 6 | */ 7 | public class Main { 8 | public static void main(String[] args) { 9 | GrizzlyTime.main(args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ 2 | --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ 3 | --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ 4 | --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ 5 | --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -------------------------------------------------------------------------------- /src/main/java/exceptions/JsonKeyHasNoDataException.java: -------------------------------------------------------------------------------- 1 | package exceptions; 2 | 3 | /** 4 | * @author Dalton Smith ConnectToWorksheetException Exception thrown if connecting to the worksheet 5 | * fails 6 | */ 7 | public class JsonKeyHasNoDataException extends Exception { 8 | public JsonKeyHasNoDataException(String errorMessage) { 9 | super(errorMessage); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/exceptions/ConnectToWorksheetException.java: -------------------------------------------------------------------------------- 1 | package exceptions; 2 | 3 | /** 4 | * @author Dalton Smith ConnectToWorksheetException Exception thrown if connecting to the worksheet 5 | * fails 6 | */ 7 | public class ConnectToWorksheetException extends Exception { 8 | public ConnectToWorksheetException(String errorMessage) { 9 | super(errorMessage); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/exceptions/CancelledUserCreationException.java: -------------------------------------------------------------------------------- 1 | package exceptions; 2 | 3 | /** 4 | * @author Dalton Smith CancelledUserCreationException Exception thrown if user creation is 5 | * cancelled 6 | */ 7 | public class CancelledUserCreationException extends Exception { 8 | public CancelledUserCreationException(String errorMessage) { 9 | super(errorMessage); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "java", 9 | "name": "Launch Current File", 10 | "request": "launch", 11 | "mainClass": "${file}" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /src/main/java/databases/BatchUpdateData.java: -------------------------------------------------------------------------------- 1 | package databases; 2 | 3 | public class BatchUpdateData { 4 | 5 | private final int first; 6 | private final int second; 7 | private final String third; 8 | 9 | public BatchUpdateData(int row, int column, String data) { 10 | this.first = row + 1; 11 | this.second = column + 1; 12 | this.third = data; 13 | } 14 | 15 | public int getRow() { 16 | return first; 17 | } 18 | 19 | public int getColumn() { 20 | return second; 21 | } 22 | 23 | public String getData() { 24 | return third; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Log File** 24 | Please post a link to your log file here. 25 | 26 | **Desktop (please complete the following information):** 27 | - Version [e.g. 22] 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/java 2 | # Edit at https://www.gitignore.io/?templates=java 3 | 4 | ### Java ### 5 | # Compiled class file 6 | *.class 7 | 8 | # Log file 9 | *.log 10 | 11 | # BlueJ files 12 | *.ctxt 13 | 14 | # Mobile Tools for Java (J2ME) 15 | .mtj.tmp/ 16 | 17 | # Package Files # 18 | *.jar 19 | *.war 20 | *.nar 21 | *.ear 22 | *.zip 23 | *.tar.gz 24 | *.rar 25 | 26 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 27 | hs_err_pid* 28 | 29 | # End of https://www.gitignore.io/api/java 30 | 31 | # credentials directory 32 | src/main/resources/credentials/credentials.json 33 | 34 | # intellij directory 35 | .idea 36 | 37 | # gradle config 38 | .gradle/ 39 | 40 | # project specific 41 | build/* 42 | out/* 43 | logs/* 44 | tokens/* 45 | bin/* 46 | config.json -------------------------------------------------------------------------------- /src/main/java/activities/KeyActivity.java: -------------------------------------------------------------------------------- 1 | package activities; 2 | 3 | import javafx.scene.Scene; 4 | import javafx.scene.input.KeyCode; 5 | import javafx.stage.Stage; 6 | 7 | public class KeyActivity { 8 | /** @author Dalton Smith KeyActivity Manages global keybinds */ 9 | public static boolean isFullscreen = false; 10 | 11 | public void setKeyHandlers(Scene scene, Stage stage) { 12 | // make application fullscreen on f key press 13 | scene.setOnKeyPressed( 14 | event -> { 15 | if (event.getCode() == KeyCode.F11) { 16 | if (isFullscreen) { 17 | stage.setFullScreen(false); 18 | isFullscreen = false; 19 | 20 | } else { 21 | stage.setFullScreen(true); 22 | isFullscreen = true; 23 | } 24 | } 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/scenes/SplashScene.java: -------------------------------------------------------------------------------- 1 | package scenes; 2 | 3 | import helpers.CommonUtils; 4 | import helpers.Constants; 5 | import java.io.File; 6 | import javafx.scene.image.Image; 7 | import javafx.scene.image.ImageView; 8 | import javafx.scene.layout.GridPane; 9 | 10 | public class SplashScene { 11 | /** @author Dalton Smith SplashScene Creates our splash image */ 12 | public void showSplash(GridPane root) { 13 | 14 | Image splash; 15 | File file = 16 | new File( 17 | CommonUtils.getCurrentDir() 18 | + File.separator 19 | + "images" 20 | + File.separator 21 | + "splash.png"); 22 | 23 | // check for custom splash 24 | if (file.exists()) { 25 | splash = new Image(file.toURI().toString()); 26 | 27 | } else { 28 | splash = new Image(Constants.kSplashImage); 29 | } 30 | 31 | ImageView splashViewer = new ImageView(splash); 32 | 33 | root.add(splashViewer, 0, 0); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2018] [Dalton Smith] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | check-format: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-java@v2 11 | with: 12 | distribution: 'temurin' 13 | java-version: '17' 14 | - name: Check Spotless 15 | shell: bash 16 | run: | 17 | chmod +x ./gradlew 18 | ./gradlew spotlessApply 19 | build: 20 | name: "Build on ${{ matrix.os }} with Java ${{ matrix.jvm }}" 21 | runs-on: "${{ matrix.os }}-latest" 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | os: [ubuntu, windows, macos] 26 | jvm: ['11', '17'] 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-java@v2 30 | with: 31 | distribution: 'temurin' 32 | java-version: ${{ matrix.jvm }} 33 | - name: Fix permissions 34 | if: ${{ matrix.os }} == "ubuntu" || ${{ matrix.os }} == "macos" 35 | run: | 36 | chmod +x ./gradlew 37 | - name: Build ShadowJar 38 | run: | 39 | ./gradlew shadowJar 40 | - name: Publish ShadowJar 41 | uses: actions/upload-artifact@v2 42 | with: 43 | name: grizzlytime-ci-jar 44 | path: build/libs/ 45 | -------------------------------------------------------------------------------- /src/main/java/activities/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package activities; 2 | 3 | import databases.BatchUpdateData; 4 | import databases.DatabaseUtils; 5 | import helpers.AlertUtils; 6 | import helpers.CommonUtils; 7 | import helpers.Constants; 8 | import java.util.ArrayList; 9 | import notifiers.LoginNotifier; 10 | 11 | public class LoginActivity { 12 | private CommonUtils utils = new CommonUtils(); 13 | private AlertUtils alertUtils = new AlertUtils(); 14 | private LoginNotifier notifier = new LoginNotifier(); 15 | 16 | private DatabaseUtils dbUtils; 17 | 18 | public static final boolean grizzlyPrompt = LocalDbActivity.kGrizzlyVerification; 19 | 20 | public LoginActivity(DatabaseUtils dbUtils) { 21 | this.dbUtils = dbUtils; 22 | } 23 | 24 | public void loginUser(int userRow, String currentTime) { 25 | ArrayList data = new ArrayList<>(); 26 | 27 | data.add(new BatchUpdateData(userRow, Constants.kLastLoginColumn, currentTime)); 28 | data.add(new BatchUpdateData(userRow, Constants.kLoggedInColumn, "TRUE")); 29 | data.add(new BatchUpdateData(userRow, Constants.kLastLogoutColumn, "LOGGED IN")); 30 | 31 | dbUtils.setCellDataBatch(data, Constants.kMainSheet); 32 | 33 | if (grizzlyPrompt && !notifier.checkNotifier(userRow, dbUtils)) { 34 | utils.playDing(); 35 | 36 | alertUtils.createAlert( 37 | "Registration not complete!", 38 | "Registration not complete!", 39 | "It seems you have not completed your user registration!" 40 | + " Please visit https://ycsrobotics.org/registration to finish your registration"); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/helpers/CommonUtils.java: -------------------------------------------------------------------------------- 1 | package helpers; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.net.URISyntaxException; 6 | import java.util.Scanner; 7 | import java.util.logging.Level; 8 | import javafx.application.Application; 9 | import javafx.scene.media.Media; 10 | import javafx.scene.media.MediaPlayer; 11 | 12 | // methods in this class should not be dependent on anything relative 13 | public class CommonUtils { 14 | /** 15 | * @author Dalton Smith CommonUtils Various utility methods used throughout the application 16 | * https://code.makery.ch/blog/javafx-dialogs-official/ 17 | */ 18 | public static Application application; 19 | 20 | public static String getCurrentDir() { 21 | return System.getProperty("user.dir"); 22 | } 23 | 24 | public static String readFile(String filePath) throws FileNotFoundException { 25 | File file = new File(filePath); 26 | Scanner sc = new Scanner(file); 27 | 28 | StringBuilder result = new StringBuilder(); 29 | 30 | // read the entire json 31 | while (sc.hasNext()) { 32 | result.append(sc.next()); 33 | } 34 | 35 | return result.toString(); 36 | } 37 | 38 | public void playDing() { 39 | 40 | Media sound; 41 | try { 42 | sound = new Media(getClass().getResource("/sounds/ding.wav").toURI().toString()); 43 | 44 | } catch (URISyntaxException e) { 45 | LoggingUtils.log(Level.SEVERE, e); 46 | return; 47 | } 48 | 49 | MediaPlayer mediaPlayer = new MediaPlayer(sound); 50 | mediaPlayer.play(); 51 | } 52 | 53 | public static void exitApplication() { 54 | System.exit(1); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/notifiers/LoginNotifier.java: -------------------------------------------------------------------------------- 1 | package notifiers; 2 | 3 | import databases.DatabaseUtils; 4 | import helpers.Constants; 5 | import helpers.LoggingUtils; 6 | import java.util.ArrayList; 7 | import java.util.logging.Level; 8 | 9 | /** 10 | * @author Dalton Smith LoginNotifier Checks if user account registration is complete, this code is 11 | * Grizzly Robotics specific 12 | */ 13 | public class LoginNotifier { 14 | public boolean checkNotifier(int studentIDRow, DatabaseUtils dbUtils) { 15 | String firstName = 16 | dbUtils.getCellData(studentIDRow, Constants.kFirstNameColumn, Constants.kMainSheet); 17 | String lastName = 18 | dbUtils.getCellData(studentIDRow, Constants.kLastNameColumn, Constants.kMainSheet); 19 | 20 | ArrayList firstNamesListReg = dbUtils.getColumnData(0, Constants.kRegistrationSheet); 21 | ArrayList lastNamesListReg = dbUtils.getColumnData(1, Constants.kRegistrationSheet); 22 | 23 | // check if first and last names are found 24 | if (matchName(firstName, firstNamesListReg)) { 25 | LoggingUtils.log(Level.INFO, firstName + " was detected in user registration"); 26 | if (matchName(lastName, lastNamesListReg)) { 27 | LoggingUtils.log(Level.INFO, lastName + " was detected in user registration"); 28 | return true; 29 | } 30 | 31 | LoggingUtils.log(Level.INFO, lastName + " was not detected in user registration"); 32 | 33 | return false; 34 | } 35 | 36 | LoggingUtils.log(Level.INFO, firstName + " was not detected in user registration"); 37 | 38 | return false; 39 | } 40 | 41 | private boolean matchName(String name, ArrayList nameList) { 42 | name = name.toLowerCase(); 43 | 44 | for (String nameInList : nameList) { 45 | if (nameInList.equalsIgnoreCase(name)) { 46 | return true; 47 | } 48 | } 49 | 50 | return false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GrizzlyTime 2 | 3 | ![Codacy Badge](https://app.codacy.com/project/badge/Grade/526a7badc871467ba68c0d688e87b3c7) 4 | ![Current Version Badge](https://img.shields.io/github/release/ycsrobotics/GrizzlyTime.svg?style=flat) 5 | ![Num Downloads](https://img.shields.io/github/downloads/ycsrobotics/GrizzlyTime/latest/total.svg?style=flat) 6 | ![License](https://img.shields.io/github/license/ycsrobotics/GrizzlyTime.svg?style=flat) 7 | ![Appveyor](https://ci.appveyor.com/api/projects/status/ph074gnnuymhxssw?svg=true) 8 | 9 | ![example image](https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/master/wiki_images/main_screen.png) 10 | 11 | GrizzlyTime is a Java based time logging system that links with Google Sheets. It features fast setup (<5minutes), logging of individual student dates, quick student registration, flexible 6 digit ID system, etc. 12 | 13 | ## Notice 14 | Google Sheets has a maximum column limit of 256 columns. The Date Log WILL break once this limit has been reached! 15 | I'm currently working on a workaround or an alternative means of storing this data. Stay up-to-date! 16 | 17 | ## Features 18 | - Individual logging of days signed in 19 | - Tracking of student/mentor total hours 20 | - Easy 1 minute registration for new users 21 | - Compatible with USB Barcode Scanners 22 | - Graphics and Application name are customizable without code 23 | - Easily extendable 24 | 25 | ## Example uses of data 26 | - Record student/mentor participation 27 | - Gather statistics of student growth over time 28 | 29 | ## Downloads 30 | Currently supported OSes: Windows, Linux 31 | Unsupported OSes: Mac 32 | Note: I would love to support Mac. Unfortunately, I do not own a Mac machine to test GrizzlyTime on. It may 33 | or may not work. 34 | 35 | Download the latest **release** [here](https://github.com/YCSRobotics/GrizzlyTime/releases/latest "here"). 36 | 37 | A setup tutorial can be found [here.](https://www.youtube.com/watch?v=GhDeMjEh9ao "here.") 38 | Or see the [wiki](https://github.com/YCSRobotics/GrizzlyTime/wiki "wiki") 39 | 40 | ## Customizing/Building Source Code 41 | Visit the [wiki](https://github.com/YCSRobotics/GrizzlyTime/wiki "wiki") for information on custom images or code. 42 | 43 | ## Credits 44 | GrizzlyTime is licensed under MIT and uses the following open source software with their respective licenses. 45 | ``` 46 | Google Java API Client 1.23.0 47 | Commons-IO 2.6 48 | Org.Json 49 | ``` 50 | 51 | GrizzlyTime is programmed and maintained by a member of FRC Team 66, Grizzly Robotics, Dalton Smith. All rights are reserved. Grizzly Robotics logo rights reserved. 52 | -------------------------------------------------------------------------------- /src/main/java/notifiers/UpdateNotifier.java: -------------------------------------------------------------------------------- 1 | package notifiers; 2 | 3 | import activities.LocalDbActivity; 4 | import helpers.AlertUtils; 5 | import helpers.CommonUtils; 6 | import helpers.Constants; 7 | import helpers.LoggingUtils; 8 | import java.awt.*; 9 | import java.io.*; 10 | import java.net.URL; 11 | import java.util.Scanner; 12 | import java.util.logging.Level; 13 | 14 | /** @author Dalton Smith UpdateNotifier Displays popup if version is outdated */ 15 | public class UpdateNotifier { 16 | 17 | private AlertUtils alertUtils = new AlertUtils(); 18 | 19 | private void downloadVersion() throws IOException { 20 | 21 | File file = new File("version.txt"); 22 | 23 | if (file.exists()) { 24 | LoggingUtils.log(Level.WARNING, "version.txt already exists"); 25 | 26 | if (file.delete()) { 27 | LoggingUtils.log(Level.INFO, "Delete was successful"); 28 | } else { 29 | LoggingUtils.log(Level.WARNING, "Delete was unsuccessful"); 30 | } 31 | } 32 | 33 | BufferedInputStream inputStream = 34 | new BufferedInputStream(new URL(Constants.kUpdateUrl).openStream()); 35 | FileOutputStream fileOS = 36 | new FileOutputStream(CommonUtils.getCurrentDir() + File.separator + "version.txt"); 37 | 38 | byte[] data = new byte[1024]; 39 | int byteContent; 40 | while ((byteContent = inputStream.read(data, 0, 1024)) != -1) { 41 | fileOS.write(data, 0, byteContent); 42 | } 43 | } 44 | 45 | public void checkUpdates() { 46 | if (!LocalDbActivity.kUpdateNotifier) { 47 | return; 48 | } 49 | 50 | try { 51 | downloadVersion(); 52 | LoggingUtils.log(Level.INFO, "Successfully grabbed version"); 53 | 54 | } catch (IOException e) { 55 | LoggingUtils.log(Level.INFO, "Error checking for updates"); 56 | LoggingUtils.log(Level.WARNING, e); 57 | } 58 | 59 | Scanner scanner; 60 | try { 61 | scanner = new Scanner(new File("version.txt")); 62 | 63 | } catch (FileNotFoundException e) { 64 | LoggingUtils.log(Level.SEVERE, e); 65 | return; 66 | } 67 | 68 | String version; 69 | 70 | if (scanner.hasNext()) { 71 | version = scanner.next(); 72 | LoggingUtils.log(Level.INFO, "Updated version is: " + version); 73 | LoggingUtils.log(Level.INFO, "Current version is: " + Constants.kVersion); 74 | 75 | } else { 76 | LoggingUtils.log(Level.WARNING, "No version detected in version.txt"); 77 | return; 78 | } 79 | 80 | if (!Constants.kVersion.equals(version)) { 81 | boolean confirm = 82 | alertUtils.createAlert( 83 | "New Version Available", 84 | "An update is available!", 85 | "Version " + version + " is available!\n" + Constants.kReleaseUrl); 86 | 87 | if (confirm) { 88 | CommonUtils.application.getHostServices().showDocument(Constants.kReleaseUrl); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/databases/JSONHelper.java: -------------------------------------------------------------------------------- 1 | package databases; 2 | 3 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 4 | 5 | import exceptions.JsonKeyHasNoDataException; 6 | import helpers.AlertUtils; 7 | import helpers.CommonUtils; 8 | import helpers.Constants; 9 | import helpers.LoggingUtils; 10 | import java.io.*; 11 | import java.nio.file.Files; 12 | import java.nio.file.Paths; 13 | import java.util.logging.Level; 14 | import org.json.JSONObject; 15 | 16 | public class JSONHelper { 17 | /*** 18 | * @author Dalton Smith 19 | * JSONHelper 20 | * Utility methods for retrieving configuration stored in the JSON 21 | */ 22 | 23 | private AlertUtils util = new AlertUtils(); 24 | 25 | // grab JSONKey 26 | public String getKey(String key) throws JsonKeyHasNoDataException, FileNotFoundException { 27 | String JSONString; 28 | 29 | // attempt to grab JSONString from config file 30 | JSONString = 31 | CommonUtils.readFile(CommonUtils.getCurrentDir() + File.separator + Constants.kConfigName); 32 | 33 | // create a JSONObject from our string 34 | JSONObject json = new JSONObject(JSONString); 35 | String result; 36 | 37 | // grab the specified key from our json object 38 | result = json.getString(key); 39 | 40 | // confirm that the key was successfully retrieved 41 | if (result.isEmpty()) { 42 | throw new JsonKeyHasNoDataException(key + " has no data!"); 43 | 44 | } else { 45 | // key was successfully retrieved 46 | LoggingUtils.log(Level.INFO, "Successfully retrieved: " + key + ": " + result); 47 | return result; 48 | } 49 | } 50 | 51 | public void editSpreadsheetId(String sheetID) throws FileNotFoundException { 52 | String data = 53 | CommonUtils.readFile(CommonUtils.getCurrentDir() + File.separator + Constants.kConfigName); 54 | 55 | String[] keyData = data.split("\"sheet\":\""); 56 | 57 | String newJson = keyData[0] + "\"sheet\":\"" + sheetID + keyData[1]; 58 | 59 | try { 60 | FileWriter writer = 61 | new FileWriter( 62 | new File(CommonUtils.getCurrentDir() + File.separator + Constants.kConfigName)); 63 | writer.write(newJson); 64 | writer.flush(); 65 | writer.close(); 66 | } catch (IOException e) { 67 | LoggingUtils.log(Level.SEVERE, "Unable to edit sheet value!"); 68 | } 69 | } 70 | 71 | // copy our json outside directory 72 | public void copyTemplateJSON() { 73 | try { 74 | InputStream pathToConfig = 75 | getClass().getClassLoader().getResourceAsStream("templates/config.json"); 76 | 77 | if (pathToConfig == null) { 78 | throw new IOException("Path to config is null!"); 79 | } 80 | 81 | Files.copy(pathToConfig, Paths.get(Constants.kConfigName), REPLACE_EXISTING); 82 | 83 | } catch (IOException e) { 84 | LoggingUtils.log(Level.SEVERE, e.getMessage()); 85 | util.createAlert( 86 | "ERROR", 87 | "Error Copying Json", 88 | "An unspecified error occured while copying the config.json. \n" + e.getMessage()); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/helpers/LoggingUtils.java: -------------------------------------------------------------------------------- 1 | package helpers; 2 | 3 | import java.io.*; 4 | import java.text.SimpleDateFormat; 5 | import java.util.Calendar; 6 | import java.util.logging.FileHandler; 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | import java.util.logging.SimpleFormatter; 10 | 11 | /** @author Dalton Smith LoggingUtils Handles interaction with the log file. */ 12 | public class LoggingUtils { 13 | private static Logger logger = Logger.getLogger(""); 14 | 15 | private static boolean isHandlerSet = false; 16 | 17 | public static void log(Level severity, String text) { 18 | setHandler(); 19 | logger.log(severity, text); 20 | } 21 | 22 | // log full stacktrace 23 | public static void log(Level severity, Throwable error) { 24 | ByteArrayOutputStream out1 = new ByteArrayOutputStream(); 25 | PrintStream out2 = new PrintStream(out1); 26 | error.printStackTrace(out2); 27 | 28 | String message; 29 | try { 30 | message = out1.toString("UTF8"); 31 | } catch (UnsupportedEncodingException e) { 32 | message = error.getMessage(); 33 | } 34 | 35 | setHandler(); 36 | logger.log(severity, message); 37 | } 38 | 39 | private static void setHandler() { 40 | if (!isHandlerSet) { 41 | System.setProperty("java.util.logging.SimpleFormatter.format", "%4$s: %5$s %n"); 42 | 43 | FileHandler fh; 44 | 45 | try { 46 | String currentDate = 47 | new SimpleDateFormat("yyyyMMdd").format(Calendar.getInstance().getTime()); 48 | 49 | File file = 50 | new File(CommonUtils.getCurrentDir() + File.separator + "logs" + File.separator); 51 | 52 | if (!file.exists()) { 53 | boolean success = file.mkdir(); 54 | LoggingUtils.log(Level.INFO, "Directory creation success? " + success); 55 | } 56 | 57 | File logFile = 58 | new File( 59 | CommonUtils.getCurrentDir() 60 | + File.separator 61 | + "logs" 62 | + File.separator 63 | + "log_" 64 | + currentDate 65 | + ".log"); 66 | 67 | int i = 0; 68 | while (logFile.exists()) { 69 | logFile = 70 | new File( 71 | CommonUtils.getCurrentDir() 72 | + File.separator 73 | + "logs" 74 | + File.separator 75 | + "log_" 76 | + i 77 | + "_" 78 | + currentDate 79 | + ".log"); 80 | i++; 81 | } 82 | 83 | String logFilePath = logFile.getAbsolutePath(); 84 | 85 | fh = new FileHandler(logFilePath); 86 | 87 | } catch (IOException e) { 88 | e.printStackTrace(); 89 | return; 90 | } 91 | 92 | logger.addHandler(fh); 93 | SimpleFormatter format = new SimpleFormatter(); 94 | fh.setFormatter(format); 95 | } 96 | 97 | isHandlerSet = true; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/scenes/SceneManager.java: -------------------------------------------------------------------------------- 1 | package scenes; 2 | 3 | import helpers.Constants; 4 | import helpers.LoggingUtils; 5 | import java.util.logging.Level; 6 | import javafx.scene.layout.GridPane; 7 | 8 | public class SceneManager { 9 | private static GrizzlyScene grizzlyScene; 10 | private static CreditsScene creditsScene; 11 | private static SplashScene splashScene; 12 | private static OptionScene optionScene; 13 | 14 | private static boolean initGrizzlyScene = false; 15 | private static boolean initCreditsScene = false; 16 | private static boolean initSplashScene = false; 17 | private static boolean initOptionsScene = false; 18 | 19 | private static boolean shownGrizzlyScene = false; 20 | private static boolean shownCreditsScene = false; 21 | 22 | private static GridPane root; 23 | 24 | public static void updateScene(int scene) { 25 | if (root == null) { 26 | LoggingUtils.log(Level.SEVERE, "Root was not set for scene manager"); 27 | } 28 | 29 | switch (scene) { 30 | case Constants.kSplashSceneState: 31 | loadSplash(); 32 | displaySplash(); 33 | break; 34 | case Constants.kMainSceneState: 35 | root.getChildren().clear(); 36 | if (!shownGrizzlyScene) { 37 | displayMain(); 38 | shownGrizzlyScene = true; 39 | } else { 40 | grizzlyScene.reShowUI(root); 41 | } 42 | 43 | break; 44 | case Constants.kCreditsSceneState: 45 | root.getChildren().clear(); 46 | 47 | if (!shownCreditsScene) { 48 | loadCredits(); 49 | displayCredits(); 50 | shownCreditsScene = true; 51 | } else { 52 | creditsScene.reShowUI(root); 53 | } 54 | 55 | break; 56 | case Constants.kLoadMainScene: 57 | loadMain(); 58 | break; 59 | default: 60 | LoggingUtils.log(Level.WARNING, scene + " is unknown"); 61 | break; 62 | } 63 | } 64 | 65 | private static void loadSplash() { 66 | if (!initSplashScene) { 67 | splashScene = new SplashScene(); 68 | } 69 | } 70 | 71 | private static void displaySplash() { 72 | if (splashScene == null) { 73 | LoggingUtils.log(Level.SEVERE, "Attempted to display scene without initialization"); 74 | return; 75 | } 76 | 77 | splashScene.showSplash(root); 78 | } 79 | 80 | private static void loadMain() { 81 | if (!initGrizzlyScene) { 82 | grizzlyScene = new GrizzlyScene(); 83 | } 84 | } 85 | 86 | private static void displayMain() { 87 | if (grizzlyScene == null) { 88 | LoggingUtils.log(Level.SEVERE, "Attempted to display scene without initialization"); 89 | return; 90 | } 91 | 92 | grizzlyScene.updateInterface(root); 93 | } 94 | 95 | private static void loadCredits() { 96 | if (!initCreditsScene) { 97 | creditsScene = new CreditsScene(); 98 | } 99 | } 100 | 101 | private static void displayCredits() { 102 | if (creditsScene == null) { 103 | LoggingUtils.log(Level.SEVERE, "Attempted to display scene without initialization"); 104 | return; 105 | } 106 | 107 | creditsScene.showCredits(root); 108 | } 109 | 110 | public static void setRoot(GridPane root) { 111 | SceneManager.root = root; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at daltzsmith@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/main/java/activities/LocalDbActivity.java: -------------------------------------------------------------------------------- 1 | package activities; 2 | 3 | import databases.JSONHelper; 4 | import exceptions.JsonKeyHasNoDataException; 5 | import helpers.AlertUtils; 6 | import helpers.CommonUtils; 7 | import helpers.LoggingUtils; 8 | import java.io.FileNotFoundException; 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | import java.util.logging.Level; 11 | import javafx.application.Platform; 12 | import org.json.JSONException; 13 | 14 | public class LocalDbActivity { 15 | public static String kSheetId = ""; 16 | public static boolean kHandsFreeMode = false; 17 | public static boolean kUpdateNotifier = true; 18 | public static int kIdLength = 6; 19 | public static int kIdLengthFallback = 7; 20 | public static String kApplicationName = "GrizzlyTime_JavaFX_Edition"; 21 | public static boolean kGrizzlyVerification = false; 22 | 23 | private JSONHelper jsonHelper = new JSONHelper(); 24 | private AlertUtils alertUtils = new AlertUtils(); 25 | 26 | public void updateLocalDb() { 27 | try { 28 | try { 29 | kSheetId = jsonHelper.getKey("sheet"); 30 | } catch (JsonKeyHasNoDataException e) { 31 | LoggingUtils.log(Level.SEVERE, e); 32 | throw new FileNotFoundException("Blank spreadsheet"); 33 | } 34 | 35 | kHandsFreeMode = jsonHelper.getKey("handsFreeMode").equalsIgnoreCase("true"); 36 | kUpdateNotifier = jsonHelper.getKey("updateNotifier").equalsIgnoreCase("true"); 37 | kGrizzlyVerification = jsonHelper.getKey("grizzlyVerification").equalsIgnoreCase("true"); 38 | kApplicationName = jsonHelper.getKey("applicationName"); 39 | 40 | try { 41 | kIdLength = Integer.parseInt(jsonHelper.getKey("idLength")); 42 | kIdLengthFallback = Integer.parseInt(jsonHelper.getKey("idLengthFallback")); 43 | 44 | } catch (NumberFormatException e) { 45 | LoggingUtils.log(Level.INFO, "Error reading from config, using fallback"); 46 | LoggingUtils.log(Level.SEVERE, e); 47 | } 48 | } catch (FileNotFoundException e) { 49 | LoggingUtils.log(Level.SEVERE, e); 50 | AlertUtils alert = new AlertUtils(); 51 | jsonHelper.copyTemplateJSON(); 52 | 53 | AtomicBoolean set = new AtomicBoolean(false); 54 | if (Platform.isFxApplicationThread()) { 55 | try { 56 | jsonHelper.editSpreadsheetId(alert.showSpreadsheetDialog()); 57 | set.set(true); 58 | } catch (FileNotFoundException ex) { 59 | LoggingUtils.log(Level.SEVERE, "Error using GUI to set spreadsheet!"); 60 | } 61 | } else { 62 | Platform.runLater( 63 | () -> { 64 | try { 65 | jsonHelper.editSpreadsheetId(alert.showSpreadsheetDialog()); 66 | set.set(true); 67 | } catch (FileNotFoundException ex) { 68 | LoggingUtils.log(Level.SEVERE, "Error using GUI to set spreadsheet!"); 69 | } 70 | }); 71 | } 72 | 73 | if (set.get()) { 74 | updateLocalDb(); 75 | } else { 76 | alertUtils.createAlert( 77 | "ERROR", 78 | "Configuration File does not exist!", 79 | "The config.json file does not exist and has been created! Please update the sheet ID"); 80 | 81 | CommonUtils.exitApplication(); 82 | } 83 | 84 | } catch (JsonKeyHasNoDataException e) { 85 | LoggingUtils.log(Level.SEVERE, e); 86 | LoggingUtils.log(Level.WARNING, "Using Fallback"); 87 | 88 | } catch (JSONException e) { 89 | LoggingUtils.log(Level.SEVERE, e); 90 | alertUtils.createAlert( 91 | "ERROR", 92 | "Error reading configuration file!", 93 | "Please try deleting the config.json file and trying again!\n" + e.getMessage()); 94 | CommonUtils.exitApplication(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/scenes/CreditsScene.java: -------------------------------------------------------------------------------- 1 | package scenes; 2 | 3 | import helpers.Constants; 4 | import javafx.geometry.HPos; 5 | import javafx.geometry.Pos; 6 | import javafx.geometry.VPos; 7 | import javafx.scene.control.Button; 8 | import javafx.scene.image.Image; 9 | import javafx.scene.image.ImageView; 10 | import javafx.scene.layout.BorderPane; 11 | import javafx.scene.layout.GridPane; 12 | import javafx.scene.text.Text; 13 | import javafx.scene.text.TextAlignment; 14 | 15 | /** 16 | * @author Dalton Smith GrizzlyTime Credits class This class constructs the UI for displaying 17 | * credits 18 | */ 19 | public class CreditsScene { 20 | // credits panes 21 | private GridPane upperPaneMain = new GridPane(); 22 | private GridPane upperPaneRight = new GridPane(); 23 | private GridPane upperPaneLeft = new GridPane(); 24 | private GridPane bottomPaneMain = new GridPane(); 25 | private GridPane mainContent = new GridPane(); 26 | private BorderPane navMenu = new BorderPane(); 27 | 28 | // nav menu 29 | Button backButton = new Button("Back"); 30 | 31 | // grizzly image 32 | private Image image = new Image(Constants.kErrorImage); 33 | private ImageView imageView = new ImageView(image); 34 | 35 | // upperPaneLeft 36 | private Text summaryTitle = new Text("Summary"); 37 | private Text summaryText = new Text(Constants.kCreditsSummary); 38 | 39 | private Text credits = new Text(Constants.kCreditsList); 40 | 41 | public void showCredits(GridPane root) { 42 | root.setId("creditsRoot"); 43 | 44 | upperPaneMain.setAlignment(Pos.CENTER); 45 | upperPaneMain.setPrefWidth(root.getWidth()); 46 | 47 | navMenu.setId("navMenu"); 48 | backButton.setId("navMenuButton"); 49 | 50 | navMenu.setLeft(backButton); 51 | mainContent.setId("creditsMainContent"); 52 | 53 | mainContent.add(upperPaneMain, 0, 0); 54 | mainContent.add(bottomPaneMain, 0, 1); 55 | 56 | mainContent.setAlignment(Pos.CENTER); 57 | GridPane.setValignment(mainContent, VPos.CENTER); 58 | 59 | root.add(navMenu, 0, 0); 60 | root.add(mainContent, 0, 1); 61 | 62 | linkHandlers(); 63 | 64 | createCreditsUI(root); 65 | } 66 | 67 | public void reShowUI(GridPane root) { 68 | root.setId("creditsRoot"); 69 | 70 | root.add(navMenu, 0, 0); 71 | root.add(mainContent, 0, 1); 72 | } 73 | 74 | private void createCreditsUI(GridPane root) { 75 | GridPane.setHalignment(root, HPos.CENTER); 76 | root.setAlignment(Pos.TOP_CENTER); 77 | 78 | imageView.setPreserveRatio(Constants.kCreditsBearPreserveRatio); 79 | imageView.setFitHeight(Constants.kCreditsBearHeight); 80 | 81 | summaryTitle.setId("title"); 82 | summaryTitle.setTextAlignment(TextAlignment.CENTER); 83 | 84 | // forcibly center 85 | GridPane.setHalignment(summaryTitle, HPos.CENTER); 86 | 87 | summaryText.setId("summaryText"); 88 | summaryText.setWrappingWidth(Constants.kCreditsWrapTextWidth); 89 | 90 | upperPaneLeft.setAlignment(Pos.TOP_CENTER); 91 | upperPaneLeft.setId("upperPaneLeft"); 92 | upperPaneRight.setId("upperPaneRight"); 93 | 94 | GridPane.setHalignment(upperPaneLeft, HPos.RIGHT); 95 | 96 | upperPaneRight.setAlignment(Pos.TOP_CENTER); 97 | upperPaneLeft.add(summaryTitle, 0, 0); 98 | upperPaneLeft.add(summaryText, 0, 1); 99 | 100 | upperPaneRight.add(imageView, 0, 0); 101 | 102 | upperPaneMain.add(upperPaneLeft, 0, 0); 103 | upperPaneMain.add(upperPaneRight, 1, 0); 104 | 105 | bottomPaneMain.setMaxWidth(500); 106 | bottomPaneMain.setAlignment(Pos.CENTER); 107 | bottomPaneMain.setId("creditsMain"); 108 | GridPane.setHalignment(bottomPaneMain, HPos.CENTER); 109 | bottomPaneMain.add(credits, 0, 0); 110 | } 111 | 112 | private void linkHandlers() { 113 | backButton.setOnAction( 114 | event -> { 115 | SceneManager.updateScene(Constants.kMainSceneState); 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/helpers/Constants.java: -------------------------------------------------------------------------------- 1 | package helpers; 2 | 3 | import activities.LocalDbActivity; 4 | 5 | public class Constants { 6 | // spreadsheet layout 7 | public static final int kEthnicityColumn = 11; 8 | public static final int kLoggedInColumn = 10; 9 | public static final int kTotalHoursColumn = 9; 10 | public static final int kHoursColumn = 8; 11 | public static final int kLastLogoutColumn = 7; 12 | public static final int kLastLoginColumn = 6; 13 | public static final int kStudentIdColumn = 0; 14 | public static final int kFirstNameColumn = 1; 15 | public static final int kLastNameColumn = 2; 16 | public static final int kGenderColumn = 5; 17 | public static final int kRoleColumn = 4; 18 | public static final int kEmailColumn = 3; 19 | 20 | // camera feed size 21 | public static final int kCameraHeight = 400; 22 | 23 | // window constants 24 | public static final boolean kWindowResizable = false; 25 | 26 | // sheet codes 27 | public static final int kMainSheet = 0; 28 | public static final int kLogSheet = 1; 29 | public static final int kRegistrationSheet = 2; 30 | 31 | // configuration locations 32 | public static final String kConfigName = "config.json"; 33 | 34 | // resource locations 35 | // resources inside a jar path seperator are NOT OS dependent 36 | public static final String kRootStylesheet = "styles/root.css"; 37 | public static final String kApplicationIcon = "images/icon.png"; 38 | public static final String kBearImage = "images/bear.png"; 39 | public static final String kErrorImage = "images/error.png"; 40 | public static final String kSplashImage = "images/splash.jpg"; 41 | 42 | // alert ui constants 43 | public static final int kBearImageWidth = 50; 44 | public static final boolean kBearPreserveRatio = true; 45 | public static final int kWordWrapWidth = 400; 46 | 47 | // main ui constants 48 | public static final String kUserTutorial = 49 | "Type in your Student ID to login. If you do not have a Student ID," 50 | + "\nenter any " 51 | + LocalDbActivity.kIdLengthFallback 52 | + " digit ID number."; 53 | public static final int kMainStageWidth = 608; 54 | public static final int kMainStageHeight = 630; 55 | public static final String kApplicationName = "GrizzlyTime JavaFX Edition"; 56 | 57 | // credits ui constants 58 | public static final String kCreditsSummary = 59 | "" 60 | + "GrizzlyTime is a JavaFX application programmed originally for FRC Team 66, Grizzly Robotics. " 61 | + "GrizzlyTime was programmed by Grizzly Robotics Team Captain of Year 2018/2019, Dalton Smith. " 62 | + "All rights and permissions are reserved. Content is licensed under MIT. See below for more information."; 63 | public static final String kCreditsList = 64 | "GrizzlyTime uses the following open source projects:\n" 65 | + "Google Java API Client 1.23.0\n" 66 | + "Commons-IO 2.6\n" 67 | + "Org.Json"; 68 | public static final int kCreditsStageWidth = 630; 69 | public static final int kCreditsStageHeight = 550; 70 | public static final int kCreditsBearHeight = 225; 71 | public static final boolean kCreditsBearPreserveRatio = true; 72 | public static final int kCreditsWrapTextWidth = 285; 73 | 74 | public static final String kVersion = "2.4.0"; 75 | 76 | // splash constants 77 | public static final int kSplashWidth = 390; 78 | public static final int kSplashHeight = 190; 79 | 80 | // grizzly robotics specific 81 | public static final boolean kMentorFallback = true; 82 | 83 | public static final String kUpdateUrl = 84 | "https://raw.githubusercontent.com/YCSRobotics/GrizzlyTime/main/version.txt"; 85 | public static final String kReleaseUrl = 86 | "https://github.com/YCSRobotics/GrizzlyTime/wiki/5.-Updating-GrizzlyTime"; 87 | 88 | // user states 89 | public static final int kIdDoesNotExist = -1; 90 | public static final int kIdLoggedIn = 0; 91 | public static final int kIdNotLoggedIn = 1; 92 | 93 | // scene manager states 94 | public static final int kSplashSceneState = 0; 95 | public static final int kMainSceneState = 1; 96 | public static final int kCreditsSceneState = 2; 97 | public static final int kOptionsSceneState = 3; 98 | public static final int kLoadMainScene = 4; 99 | } 100 | -------------------------------------------------------------------------------- /src/main/resources/styles/root.css: -------------------------------------------------------------------------------- 1 | /* 2 | Main Screen CSS 3 | */ 4 | 5 | #main { 6 | -fx-font-family: "Calibri"; 7 | -fx-background-color: #D0BB61; 8 | } 9 | 10 | .button { 11 | -fx-font-size: 16px; 12 | -fx-border-width: 0; 13 | -fx-outline: none; 14 | -fx-background-color: lightgrey; 15 | -fx-background-radius: 0; 16 | } 17 | 18 | .button:hover { 19 | -fx-background-color: #acacac; 20 | } 21 | 22 | #title { 23 | -fx-font-size: 25px; 24 | -fx-text-alignment: center; 25 | -fx-padding: 0px, 20px, 0px, 0px; 26 | 27 | } 28 | 29 | #messageText { 30 | -fx-font-size: 18px; 31 | -fx-text-alignment: center; 32 | -fx-text-fill: #b00805; 33 | } 34 | 35 | #textBox { 36 | -fx-font-size: 18px; 37 | -fx-background-color: WHITE; 38 | -fx-font-weight: BOLD; 39 | -fx-prompt-text-fill: #808080; 40 | -fx-alignment: top-left; 41 | -fx-border-radius: 0px; 42 | -fx-background-radius: 0; 43 | -fx-min-height: 34px; 44 | 45 | } 46 | 47 | #confirmButton { 48 | -fx-font-size: 18px; 49 | -fx-border-width: 0; 50 | -fx-alignment: top-left; 51 | -fx-outline: none; 52 | -fx-background-color: lightgrey; 53 | -fx-background-radius: 0; 54 | } 55 | 56 | #confirmButton:hover { 57 | -fx-background-color: #acacac; 58 | } 59 | 60 | #bottomPane { 61 | -fx-padding: 3px; 62 | } 63 | 64 | #textDescription { 65 | -fx-font-size: 18px; 66 | -fx-padding: 0px; 67 | } 68 | 69 | #bottomView { 70 | -fx-border-width: 3px; 71 | -fx-border-color: black; 72 | -fx-border-style: solid; 73 | -fx-max-width: 250px; 74 | -fx-padding: 15px; 75 | -fx-border-radius: 3px; 76 | -fx-background-color: #d9c575; 77 | } 78 | 79 | #hyperlinkBottom { 80 | -fx-font-fill: #000000; 81 | -fx-font-size: 15px; 82 | -fx-text-fill: #000000; 83 | } 84 | 85 | #creditsButton:hover { 86 | -fx-background-color: lightgrey; 87 | } 88 | 89 | #creditsMainContent { 90 | -fx-padding: 5px; 91 | } 92 | 93 | #title { 94 | -fx-font-size: 30px; 95 | -fx-text-alignment: center; 96 | } 97 | 98 | #navMenu { 99 | -fx-background-color: #d9c575; 100 | -fx-padding: 5px; 101 | } 102 | 103 | #navMenuButton { 104 | -fx-border-radius: 10px; 105 | } 106 | 107 | /* 108 | Credits CSS 109 | */ 110 | #upperPaneLeft { 111 | -fx-padding: 20px; 112 | } 113 | 114 | #upperPaneRight { 115 | -fx-padding: 10px 0px 0px 0px; 116 | } 117 | 118 | #creditsRoot { 119 | -fx-font-family: "Calibri"; 120 | -fx-background-color: #D0BB61; 121 | } 122 | 123 | #summaryText { 124 | -fx-text-alignment: center; 125 | -fx-font-size: 18px; 126 | -fx-padding: 15px; 127 | } 128 | 129 | #creditsMain { 130 | -fx-font-size: 20px; 131 | -fx-border-width:2px; 132 | -fx-border-radius: 3px; 133 | -fx-border-color:black; 134 | -fx-border-style:solid; 135 | -fx-padding: 5px; 136 | } 137 | 138 | /* 139 | Custom Dialog CSS 140 | */ 141 | 142 | #customDialog { 143 | -fx-background-color: #D0BB61; 144 | -fx-padding: 10px; 145 | -fx-font-size: 16px; 146 | } 147 | 148 | .myDialog{ 149 | -fx-background-color: #D0BB61; 150 | } 151 | .myDialog > *.button-bar > *.container{ 152 | -fx-background-color: #D0BB61; 153 | } 154 | 155 | .myDialog > *.label.content{ 156 | -fx-font-size: 20px; 157 | -fx-font-weight: bold; 158 | } 159 | .myDialog:header *.header-panel{ 160 | -fx-background-color: #d9c575; 161 | } 162 | .myDialog:header *.header-panel *.label{ 163 | -fx-font-size: 25px; 164 | -fx-fill: #292929; 165 | } 166 | 167 | /* 168 | Account Dialog CSS 169 | */ 170 | 171 | .accountDialog{ 172 | -fx-background-color: #D0BB61; 173 | } 174 | .accountDialog > *.button-bar > *.container{ 175 | -fx-background-color: #D0BB61; 176 | } 177 | 178 | .accountDialog > *.label.content{ 179 | -fx-font-size: 20px; 180 | -fx-font-weight: bold; 181 | } 182 | .accountDialog:header *.header-panel{ 183 | -fx-background-color: #d9c575; 184 | } 185 | .accountDialog:header *.header-panel *.label{ 186 | -fx-font-size: 23px; 187 | -fx-fill: #292929; 188 | } 189 | 190 | #accountGrid { 191 | -fx-font-size: 16px; 192 | } 193 | 194 | #textField { 195 | -fx-font-size: 15px; 196 | -fx-background-color: WHITE; 197 | -fx-prompt-text-fill: #808080; 198 | -fx-alignment: top-left; 199 | -fx-max-width: 300; 200 | -fx-border-radius: 0px; 201 | -fx-background-radius: 0; 202 | -fx-min-height: 20px; 203 | } -------------------------------------------------------------------------------- /src/main/java/GrizzlyTime.java: -------------------------------------------------------------------------------- 1 | import activities.KeyActivity; 2 | import activities.LocalDbActivity; 3 | import helpers.AlertUtils; 4 | import helpers.CommonUtils; 5 | import helpers.Constants; 6 | import helpers.LoggingUtils; 7 | import java.io.File; 8 | import java.util.logging.Level; 9 | import javafx.application.Application; 10 | import javafx.application.Platform; 11 | import javafx.geometry.Pos; 12 | import javafx.scene.Scene; 13 | import javafx.scene.image.Image; 14 | import javafx.scene.layout.GridPane; 15 | import javafx.stage.Stage; 16 | import notifiers.UpdateNotifier; 17 | import scenes.SceneManager; 18 | 19 | public class GrizzlyTime extends Application { 20 | /** 21 | * @author Dalton Smith GrizzlyTime main application class This class calls our various activities 22 | * and starts the JavaFX application 23 | */ 24 | 25 | // only initializations that don't have freezing constructor instances should be placed here 26 | private KeyActivity keyHandlers = new KeyActivity(); 27 | 28 | private UpdateNotifier updater = new UpdateNotifier(); 29 | 30 | private LocalDbActivity dbActivity = new LocalDbActivity(); 31 | 32 | @Override 33 | public void start(Stage primaryStage) { 34 | // set application in commonutils to access hostservices 35 | CommonUtils.application = this; 36 | 37 | Thread.setDefaultUncaughtExceptionHandler( 38 | (thread, throwable) -> globalExceptionHandler(throwable)); 39 | 40 | dbActivity.updateLocalDb(); 41 | 42 | // check if custom icon 43 | File file = 44 | new File( 45 | CommonUtils.getCurrentDir() + File.separator + "images" + File.separator + "icon.png"); 46 | File stylesheet = 47 | new File( 48 | CommonUtils.getCurrentDir() + File.separator + "styles" + File.separator + "style.css"); 49 | if (file.exists()) { 50 | primaryStage.getIcons().add(new Image(file.toURI().toString())); 51 | 52 | } else { 53 | primaryStage 54 | .getIcons() 55 | .add(new Image(this.getClass().getResourceAsStream(Constants.kApplicationIcon))); 56 | } 57 | 58 | GridPane root = new GridPane(); 59 | SceneManager.setRoot(root); 60 | 61 | Scene scene = new Scene(root, Constants.kSplashWidth, Constants.kSplashHeight); 62 | 63 | scene.getStylesheets().add(Constants.kRootStylesheet); 64 | 65 | if (stylesheet.exists()) { 66 | scene.getStylesheets().add("file:///" + stylesheet.getAbsolutePath().replace("\\", "/")); 67 | LoggingUtils.log(Level.INFO, "Loaded custom stylesheet!"); 68 | } 69 | 70 | root.setId("main"); 71 | root.setAlignment(Pos.CENTER); 72 | 73 | String applicationName = LocalDbActivity.kApplicationName; 74 | 75 | if (applicationName.equals("")) { 76 | primaryStage.setTitle(Constants.kApplicationName); 77 | 78 | } else { 79 | applicationName = applicationName.replaceAll("_", " "); 80 | primaryStage.setTitle(applicationName); 81 | } 82 | 83 | primaryStage.setScene(scene); 84 | primaryStage.setResizable(Constants.kWindowResizable); 85 | primaryStage.show(); 86 | 87 | Platform.runLater( 88 | () -> { 89 | // show our splash 90 | SceneManager.updateScene(Constants.kSplashSceneState); 91 | primaryStage.requestFocus(); 92 | primaryStage.centerOnScreen(); 93 | LoggingUtils.log(Level.INFO, "Run first"); 94 | }); 95 | 96 | // queue our updates 97 | Platform.runLater( 98 | () -> { 99 | // initialize our activities and interface objects AFTER 100 | // we display application 101 | SceneManager.updateScene(Constants.kLoadMainScene); 102 | AlertUtils.stage = primaryStage; 103 | updater.checkUpdates(); 104 | keyHandlers.setKeyHandlers(scene, primaryStage); 105 | LoggingUtils.log(Level.INFO, "Run second"); 106 | }); 107 | 108 | // queue the update to main thread 109 | Platform.runLater( 110 | () -> { 111 | // remove splash screen on load 112 | root.getChildren().clear(); 113 | primaryStage.setWidth(Constants.kMainStageWidth); 114 | primaryStage.setHeight(Constants.kMainStageHeight); 115 | primaryStage.centerOnScreen(); 116 | SceneManager.updateScene(Constants.kMainSceneState); 117 | LoggingUtils.log(Level.INFO, "Run third"); 118 | }); 119 | } 120 | 121 | public static void main(String[] args) { 122 | Application.launch(args); 123 | } 124 | 125 | // catch uncaught exceptions 126 | private static void globalExceptionHandler(Throwable throwable) { 127 | LoggingUtils.log(Level.SEVERE, throwable); 128 | CommonUtils.exitApplication(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/activities/LogoutActivity.java: -------------------------------------------------------------------------------- 1 | package activities; 2 | 3 | import databases.DatabaseUtils; 4 | import helpers.Constants; 5 | import helpers.LoggingUtils; 6 | import java.text.SimpleDateFormat; 7 | import java.time.LocalTime; 8 | import java.time.format.DateTimeParseException; 9 | import java.util.ArrayList; 10 | import java.util.Date; 11 | import java.util.logging.Level; 12 | import javafx.application.Platform; 13 | import scenes.GrizzlyScene; 14 | 15 | public class LogoutActivity { 16 | private DatabaseUtils dbUtils; 17 | 18 | public LogoutActivity(DatabaseUtils dbUtils) { 19 | this.dbUtils = dbUtils; 20 | } 21 | 22 | public void logoutUserWithHours( 23 | String userID, int userRow, LocalTime totalHoursTime, String totalTimeFromDifference) { 24 | // grab the current total hours 25 | String totalHours = 26 | dbUtils.getCellData(userRow, Constants.kTotalHoursColumn, Constants.kMainSheet); 27 | String[] prevTotalTime; 28 | double[] prevTotalTimeNum = new double[3]; 29 | 30 | // manually manipulate the time 31 | try { 32 | prevTotalTime = totalHours.split("\\s*:\\s*"); 33 | 34 | prevTotalTimeNum[0] = Double.parseDouble(prevTotalTime[0]); 35 | prevTotalTimeNum[1] = Double.parseDouble(prevTotalTime[1]); 36 | prevTotalTimeNum[2] = Double.parseDouble(prevTotalTime[2]); 37 | 38 | } catch (DateTimeParseException | NumberFormatException | ArrayIndexOutOfBoundsException e) { 39 | LoggingUtils.log(Level.WARNING, "Error parsing previous total time, using fallback of 0.0"); 40 | prevTotalTimeNum[0] = 0.0; 41 | prevTotalTimeNum[1] = 0.0; 42 | prevTotalTimeNum[2] = 0.0; 43 | } 44 | 45 | // calculate the new total time 46 | double totalHour = prevTotalTimeNum[0] + totalHoursTime.getHour(); 47 | double totalMinute = prevTotalTimeNum[1] + totalHoursTime.getMinute(); 48 | double totalSeconds = prevTotalTimeNum[2] + totalHoursTime.getSecond(); 49 | 50 | while (totalSeconds > 60) { 51 | totalSeconds -= 60; 52 | totalMinute += 1; 53 | } 54 | 55 | while (totalMinute > 60) { 56 | totalMinute -= 60; 57 | totalHour += 1; 58 | } 59 | 60 | String timeTotal = 61 | String.format("%02d:%02d:%02d", (int) totalHour, (int) totalMinute, (int) totalSeconds); 62 | 63 | int userRowLogout = 64 | dbUtils.getCellRowFromColumn(userID, Constants.kStudentIdColumn, Constants.kLogSheet); 65 | 66 | // set cell data 67 | dbUtils.setCellData( 68 | userRow, Constants.kHoursColumn, totalTimeFromDifference, Constants.kMainSheet); 69 | 70 | int userTimeColumn = getCurrentDateColumn(); 71 | 72 | // add together day times 73 | LocalTime prevDayTime; 74 | try { 75 | String prevData = dbUtils.getCellData(userRowLogout, userTimeColumn, Constants.kLogSheet); 76 | prevDayTime = LocalTime.parse(prevData); 77 | 78 | } catch (DateTimeParseException | NullPointerException e) { 79 | prevDayTime = LocalTime.parse("00:00:01"); 80 | LoggingUtils.log( 81 | Level.WARNING, 82 | "There was an issue adding cell data, using fallback. \n" + e.getMessage()); 83 | } 84 | 85 | LocalTime tempTotalDayTime; 86 | try { 87 | tempTotalDayTime = 88 | LocalTime.parse(totalTimeFromDifference) 89 | .plusHours(prevDayTime.getHour()) 90 | .plusMinutes(prevDayTime.getMinute()) 91 | .plusSeconds(prevDayTime.getSecond()); 92 | 93 | } catch (DateTimeParseException e) { 94 | GrizzlyScene.setMessageBoxText( 95 | "There was an error adding together total time. Using fallback."); 96 | tempTotalDayTime = prevDayTime; 97 | } 98 | 99 | String timeTotalDay = 100 | String.format( 101 | "%02d:%02d:%02d", 102 | tempTotalDayTime.getHour(), tempTotalDayTime.getMinute(), tempTotalDayTime.getSecond()); 103 | 104 | dbUtils.setCellData(userRowLogout, userTimeColumn, timeTotalDay, Constants.kLogSheet); 105 | dbUtils.setCellData(userRow, Constants.kTotalHoursColumn, timeTotal, Constants.kMainSheet); 106 | 107 | // show user logout text 108 | Platform.runLater( 109 | () -> { 110 | GrizzlyScene.setMessageBoxText("Logged out user: " + userID); 111 | GrizzlyScene.clearInput(); 112 | }); 113 | } 114 | 115 | // helper method for grabbing the column that contains the current date 116 | public int getCurrentDateColumn() { 117 | ArrayList data; 118 | try { 119 | data = dbUtils.getRowData(1, Constants.kLogSheet); 120 | } catch (NullPointerException e) { 121 | data = new ArrayList<>(); 122 | } 123 | 124 | String currentDate = new SimpleDateFormat("yyyy:MM:dd").format(new Date()); 125 | 126 | // check if our date already exists 127 | int i; 128 | for (i = 0; i < data.size(); i++) { 129 | String checkDate = data.get(i); 130 | checkDate = checkDate.replace("'", ""); 131 | 132 | // return column containing our date 133 | if (checkDate.equals(currentDate)) { 134 | return i; 135 | } 136 | } 137 | 138 | // confirm that the sheet isn't empty 139 | if (data.size() == 0) { 140 | i = 3; 141 | } 142 | 143 | // log the current date and return column 144 | dbUtils.setCellData(0, i, currentDate, Constants.kLogSheet); 145 | return i; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/databases/DatabaseUtils.java: -------------------------------------------------------------------------------- 1 | package databases; 2 | 3 | import activities.LoginActivity; 4 | import helpers.Constants; 5 | import helpers.LoggingUtils; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.logging.Level; 9 | 10 | public class DatabaseUtils { 11 | /** 12 | * @author Dalton Smith DatabaseUtils Helpers to make managing the google sheets API Horribly 13 | * inefficient, but it works :) 14 | */ 15 | private final DatabaseProcess dbProcess = new DatabaseProcess(); 16 | 17 | private List> mainWorksheet; 18 | private List> loggedHours; 19 | private List> currentWorksheet; 20 | private List> registrationData; 21 | 22 | // initial grab of worksheet data 23 | public DatabaseUtils() { 24 | // initial data 25 | currentWorksheet = dbProcess.returnWorksheetData(Constants.kMainSheet); 26 | mainWorksheet = currentWorksheet; 27 | loggedHours = dbProcess.returnWorksheetData(Constants.kLogSheet); 28 | updateStudentRegistrationData(); 29 | } 30 | 31 | // helper method called at beginning of each method to retrieve updated data 32 | public void getUpdatedData() { 33 | currentWorksheet = dbProcess.returnWorksheetData(Constants.kMainSheet); 34 | mainWorksheet = currentWorksheet; 35 | loggedHours = dbProcess.returnWorksheetData(Constants.kLogSheet); 36 | updateStudentRegistrationData(); 37 | } 38 | 39 | public void setCellDataBatch(ArrayList data, int page) { 40 | setPage(page); 41 | 42 | dbProcess.updateSpreadSheetBatch(data, page); 43 | } 44 | 45 | // update registration data sheet 46 | private void updateStudentRegistrationData() { 47 | if (LoginActivity.grizzlyPrompt) { 48 | registrationData = dbProcess.returnWorksheetData(Constants.kRegistrationSheet); 49 | } 50 | } 51 | 52 | // grabs column data from sheet 53 | public ArrayList getColumnData(int column, int page) { 54 | setPage(page); 55 | 56 | if (isWorksheetsNotValid()) { 57 | return null; 58 | } 59 | 60 | ArrayList result = new ArrayList<>(); 61 | 62 | try { 63 | // add various rows to ArrayList 64 | for (List row : mainWorksheet) { 65 | try { 66 | result.add(row.get(column).toString()); 67 | 68 | } catch (Exception e) { 69 | result.add(""); 70 | } 71 | } 72 | } catch (NullPointerException e) { 73 | LoggingUtils.log(Level.WARNING, "IS THERE DATA IN WORKSHEET?"); 74 | } 75 | 76 | return result; 77 | } 78 | 79 | public ArrayList getRowData(int row, int page) { 80 | if (isWorksheetsNotValid()) { 81 | return null; 82 | } 83 | 84 | setPage(page); 85 | 86 | int i = 1; 87 | ArrayList result = new ArrayList<>(); 88 | 89 | for (List currentRow : mainWorksheet) { 90 | if (i == row) { 91 | int x = 0; 92 | while (true) { 93 | try { 94 | result.add(currentRow.get(x).toString()); 95 | 96 | } catch (Exception e) { 97 | break; // no more data; 98 | } 99 | x++; 100 | } 101 | } 102 | 103 | i++; 104 | } 105 | 106 | return result; 107 | } 108 | 109 | // sets cell data 110 | public void setCellData(int row, int column, String data, int page) { 111 | 112 | // set appropriate sheet 113 | setPage(page); 114 | 115 | dbProcess.updateSpreadSheet(row + 1, column + 1, data, page); 116 | } 117 | 118 | // gets specific cell data 119 | public String getCellData(int row, int column, int page) { 120 | if (isWorksheetsNotValid()) { 121 | return null; 122 | } 123 | 124 | setPage(page); 125 | 126 | int i = 0; 127 | 128 | for (String sheetRow : getColumnData(column, page)) { 129 | if (i == row) { 130 | return sheetRow; 131 | } 132 | 133 | i++; 134 | } 135 | 136 | return null; 137 | } 138 | 139 | // grab a a cell row based on its position in a column 140 | public int getCellRowFromColumn(String cellValue, int column, int page) { 141 | 142 | if (isWorksheetsNotValid()) { 143 | return -1; 144 | } 145 | 146 | int i = 0; 147 | for (String value : getColumnData(column, page)) { 148 | if (value.equals(cellValue)) { 149 | return i; 150 | } 151 | i++; 152 | } 153 | 154 | return -1; 155 | } 156 | 157 | // grab the next empty cell from first row 158 | public int nextEmptyCellColumn(int page) { 159 | 160 | setPage(page); 161 | 162 | ArrayList columnData = getColumnData(Constants.kStudentIdColumn, page); 163 | int i; 164 | 165 | for (i = 0; i < columnData.size(); i++) { 166 | if (columnData.get(i).isEmpty()) { 167 | return i; 168 | } 169 | } 170 | 171 | return i; 172 | } 173 | 174 | private boolean isWorksheetsNotValid() { 175 | 176 | if (DatabaseProcess.worksheetIsEmpty) { 177 | return false; 178 | } 179 | 180 | return (mainWorksheet == null || loggedHours == null); 181 | } 182 | 183 | private void setPage(int page) { 184 | switch (page) { 185 | case Constants.kMainSheet: 186 | mainWorksheet = currentWorksheet; 187 | break; 188 | case Constants.kLogSheet: 189 | mainWorksheet = loggedHours; 190 | break; 191 | case Constants.kRegistrationSheet: 192 | mainWorksheet = registrationData; 193 | break; 194 | default: 195 | break; 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/main/java/scenes/GrizzlyScene.java: -------------------------------------------------------------------------------- 1 | package scenes; 2 | 3 | import activities.KeyActivity; 4 | import activities.LocalDbActivity; 5 | import activities.UserActivity; 6 | import exceptions.CancelledUserCreationException; 7 | import exceptions.ConnectToWorksheetException; 8 | import helpers.AlertUtils; 9 | import helpers.CommonUtils; 10 | import helpers.Constants; 11 | import helpers.LoggingUtils; 12 | import java.io.File; 13 | import java.net.NoRouteToHostException; 14 | import java.util.logging.Level; 15 | import javafx.application.Platform; 16 | import javafx.concurrent.Task; 17 | import javafx.geometry.HPos; 18 | import javafx.geometry.Pos; 19 | import javafx.scene.control.Button; 20 | import javafx.scene.control.Hyperlink; 21 | import javafx.scene.control.Label; 22 | import javafx.scene.control.TextField; 23 | import javafx.scene.image.Image; 24 | import javafx.scene.image.ImageView; 25 | import javafx.scene.layout.BorderPane; 26 | import javafx.scene.layout.GridPane; 27 | import javafx.scene.text.Text; 28 | import javafx.scene.text.TextAlignment; 29 | import javafx.stage.Stage; 30 | 31 | public class GrizzlyScene { 32 | /** @author Dalton Smith GrizzlyScene Manages the main interface */ 33 | 34 | // object that should be able to be modified by calling 35 | // this scene directly 36 | private static Label messageText = new Label(""); 37 | 38 | private static TextField studentIDBox = new TextField(); 39 | 40 | // define our scene objects 41 | private Button loginButton = new Button("Login/Logout"); 42 | private UserActivity userActivity = new UserActivity(); 43 | private Text description = new Text(Constants.kUserTutorial); 44 | private Hyperlink creditsLink = new Hyperlink("Credits"); 45 | private Text creditsText = new Text("v" + Constants.kVersion); 46 | private Hyperlink optionsLink = new Hyperlink("Full Screen"); 47 | private BorderPane bottomPane = new BorderPane(); 48 | 49 | private AlertUtils alertUtils = new AlertUtils(); 50 | 51 | private GridPane subRoot = new GridPane(); 52 | 53 | // boolean state variables 54 | private boolean handsFreeMode = LocalDbActivity.kHandsFreeMode; 55 | 56 | // our upper image 57 | private ImageView imageView; 58 | 59 | public GrizzlyScene() { 60 | Image splash; 61 | File file = 62 | new File( 63 | CommonUtils.getCurrentDir() + File.separator + "images" + File.separator + "error.png"); 64 | 65 | // check for custom splash 66 | if (file.exists()) { 67 | splash = new Image(file.toURI().toString()); 68 | 69 | } else { 70 | splash = new Image(Constants.kErrorImage); 71 | } 72 | 73 | imageView = new ImageView(splash); 74 | } 75 | 76 | public void updateInterface(GridPane root) { 77 | 78 | // create the upper image 79 | imageView.setFitHeight(Constants.kCameraHeight); 80 | GridPane.setHalignment(imageView, HPos.CENTER); 81 | root.add(imageView, 0, 0); 82 | 83 | // update CSS IDS 84 | messageText.setId("messageText"); 85 | studentIDBox.setId("textBox"); 86 | loginButton.setId("confirmButton"); 87 | creditsLink.setId("hyperlinkBottom"); 88 | optionsLink.setId("hyperlinkBottom"); 89 | creditsText.setId("hyperlinkBottom"); 90 | 91 | // create our panes 92 | GridPane options = new GridPane(); 93 | GridPane title = new GridPane(); 94 | 95 | subRoot.setId("bottomView"); 96 | 97 | // confirm alignments 98 | subRoot.setAlignment(Pos.CENTER); 99 | options.setAlignment(Pos.CENTER); 100 | title.setAlignment(Pos.CENTER); 101 | messageText.setAlignment(Pos.CENTER); 102 | description.setTextAlignment(TextAlignment.CENTER); 103 | description.setId("textDescription"); 104 | 105 | // manually align message text because Gridpane is weird 106 | GridPane.setHalignment(messageText, HPos.CENTER); 107 | GridPane.setHalignment(description, HPos.CENTER); 108 | GridPane.setHalignment(subRoot, HPos.CENTER); 109 | 110 | // set bottom pane details 111 | bottomPane.setId("bottomPane"); 112 | bottomPane.setLeft(optionsLink); 113 | bottomPane.setCenter(creditsText); 114 | bottomPane.setRight(creditsLink); 115 | bottomPane.setMinWidth(subRoot.getWidth()); 116 | 117 | // add our various nodes to respective panes 118 | title.add(description, 0, 1); 119 | options.add(studentIDBox, 0, 0); 120 | options.add(loginButton, 1, 0); 121 | subRoot.add(title, 0, 0); 122 | subRoot.add(options, 0, 1); 123 | subRoot.add(messageText, 0, 2); 124 | 125 | // sub root details 126 | subRoot.setVgap(10); 127 | 128 | // add to root pane 129 | root.add(subRoot, 0, 1); 130 | root.add(bottomPane, 0, 2); 131 | 132 | // handle our buttons 133 | setEventHandlers(); 134 | } 135 | 136 | public void reShowUI(GridPane root) { 137 | root.setId("main"); 138 | 139 | // add to root pane 140 | root.add(imageView, 0, 0); 141 | root.add(subRoot, 0, 1); 142 | root.add(bottomPane, 0, 2); 143 | } 144 | 145 | // our event handlers for interactivity 146 | private void setEventHandlers() { 147 | // login on enter key press 148 | studentIDBox.setOnAction(event -> confirmLogin()); 149 | 150 | // login button event handler 151 | loginButton.setOnAction(event -> confirmLogin()); 152 | 153 | creditsLink.setOnAction(event -> showCredits()); 154 | 155 | optionsLink.setOnAction( 156 | event -> { 157 | Stage stage = (Stage) optionsLink.getScene().getWindow(); 158 | if (KeyActivity.isFullscreen) { 159 | stage.setFullScreen(false); 160 | KeyActivity.isFullscreen = false; 161 | } else { 162 | stage.setFullScreen(true); 163 | KeyActivity.isFullscreen = true; 164 | } 165 | }); 166 | } 167 | 168 | private void showCredits() { 169 | SceneManager.updateScene(Constants.kCreditsSceneState); 170 | } 171 | 172 | // helper login method 173 | private void confirmLogin() { 174 | setMessageBoxText("Processing..."); 175 | 176 | // confirm the ID is vslid 177 | if (!userActivity.isValidID(studentIDBox.getText())) { 178 | setMessageBoxText("ID " + studentIDBox.getText() + " is invalid."); 179 | 180 | Task wait = 181 | new Task() { 182 | @Override 183 | protected Void call() throws Exception { 184 | Thread.sleep(5000); 185 | return null; 186 | } 187 | }; 188 | 189 | wait.setOnSucceeded(e -> setMessageBoxText("")); 190 | 191 | // no need to set as daemon as will end after x seconds. 192 | new Thread(wait).start(); 193 | return; 194 | } 195 | 196 | if (!handsFreeMode) { 197 | // confirm that the user wants to login/logout 198 | if (alertUtils.confirmInput("Confirm login/logout of user: " + studentIDBox.getText())) { 199 | loginUser(); 200 | } else { 201 | setMessageBoxText(""); 202 | } 203 | 204 | // show no prompts 205 | } else { 206 | loginUser(); 207 | } 208 | } 209 | 210 | // login the user, check if hands free or not 211 | private void loginUser() { 212 | // separate login process on different thread to ensure 213 | // main application does not freeze 214 | // also allows in for multiple users login simultaneously 215 | Runnable loginUser = 216 | () -> { 217 | // ensure that the user typed something in 218 | if (studentIDBox.getText().isEmpty()) { 219 | setMessageBoxText("Nothing was entered!"); 220 | return; 221 | } 222 | 223 | // attempt login/logout and or account creation 224 | // do nothing if account creation was cancelled 225 | try { 226 | // check if the user is logged in, and that user exists 227 | if (!(userActivity.isUserLoggedIn(studentIDBox.getText()))) { 228 | LoggingUtils.log(Level.INFO, "Logging in: " + studentIDBox.getText()); 229 | userActivity.loginUser(studentIDBox.getText()); 230 | 231 | } else { 232 | LoggingUtils.log(Level.INFO, "Logging out: " + studentIDBox.getText()); 233 | userActivity.logoutUser(studentIDBox.getText()); 234 | } 235 | 236 | } catch (CancelledUserCreationException e) { 237 | setMessageBoxText("Cancelled account creation"); 238 | 239 | } catch (ConnectToWorksheetException e) { 240 | setMessageBoxText("There was an error connecting to the database. Please retry."); 241 | 242 | } catch (NoRouteToHostException e) { 243 | setMessageBoxText("Unable to connect to database. Check internet and retry."); 244 | 245 | } catch (Exception e) { 246 | LoggingUtils.log(Level.SEVERE, e); 247 | setMessageBoxText("An unknown error has occurred, see log file."); 248 | } 249 | 250 | // refocus the textbox 251 | Platform.runLater(() -> studentIDBox.requestFocus()); 252 | }; 253 | 254 | // start our thread 255 | Thread t = new Thread(loginUser); 256 | t.setDaemon(true); 257 | t.start(); 258 | } 259 | 260 | // helper methods for setting and clearing text box 261 | public static void setMessageBoxText(String text) { 262 | if (Platform.isFxApplicationThread()) { 263 | messageText.setText(text); 264 | } else { 265 | Platform.runLater(() -> messageText.setText(text)); 266 | } 267 | } 268 | 269 | public static void clearInput() { 270 | studentIDBox.clear(); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/main/java/activities/UserActivity.java: -------------------------------------------------------------------------------- 1 | package activities; 2 | 3 | import databases.BatchUpdateData; 4 | import databases.DatabaseUtils; 5 | import exceptions.CancelledUserCreationException; 6 | import helpers.AlertUtils; 7 | import helpers.Constants; 8 | import helpers.LoggingUtils; 9 | import java.time.LocalDateTime; 10 | import java.time.LocalTime; 11 | import java.time.format.DateTimeFormatter; 12 | import java.util.ArrayList; 13 | import java.util.logging.Level; 14 | import javafx.application.Platform; 15 | import scenes.GrizzlyScene; 16 | 17 | public class UserActivity { 18 | /** 19 | * @author Dalton Smith UserActivity class Contains the various methods for handling user 20 | * login/logout 21 | */ 22 | private DatabaseUtils dbUtils = new DatabaseUtils(); 23 | 24 | private AlertUtils alertUtils = new AlertUtils(); 25 | 26 | private LogoutActivity logoutActivity = new LogoutActivity(dbUtils); 27 | private LoginActivity loginActivity = new LoginActivity(dbUtils); 28 | 29 | private static DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME; 30 | 31 | // check if user is logged in 32 | public boolean isUserLoggedIn(String userID) throws Exception { 33 | dbUtils.getUpdatedData(); 34 | 35 | ArrayList ids = dbUtils.getColumnData(0, Constants.kMainSheet); 36 | 37 | int state = doesIdExist(ids, userID); 38 | 39 | switch (state) { 40 | case Constants.kIdDoesNotExist: 41 | break; 42 | case Constants.kIdLoggedIn: 43 | return true; 44 | case Constants.kIdNotLoggedIn: 45 | return false; 46 | default: 47 | LoggingUtils.log(Level.SEVERE, "Uh oh, isUserLoggedIn received an unknown ID of " + state); 48 | break; 49 | } 50 | 51 | // request users first name and last name 52 | LoggingUtils.log(Level.INFO, "New User Detected"); 53 | ArrayList userData = alertUtils.getUserInfo(); 54 | 55 | // throws CancelledUserException if registration was cancelled 56 | createNewUser(userData, userID); 57 | 58 | return false; 59 | } 60 | 61 | public void createNewUser(ArrayList userData, String userID) 62 | throws CancelledUserCreationException { 63 | // cancel if user cancelled or exited registration dialog 64 | if (("TRUE").equalsIgnoreCase(userData.get(0))) { 65 | // create user then login 66 | ArrayList data = new ArrayList<>(); 67 | 68 | int blankRow = dbUtils.nextEmptyCellColumn(Constants.kMainSheet); 69 | addUserInfoBasic(userData, userID, data, blankRow); 70 | data.add(new BatchUpdateData(blankRow, Constants.kEmailColumn, userData.get(3))); 71 | data.add(new BatchUpdateData(blankRow, Constants.kRoleColumn, userData.get(5))); 72 | data.add(new BatchUpdateData(blankRow, Constants.kGenderColumn, userData.get(4))); 73 | 74 | dbUtils.setCellDataBatch(data, Constants.kMainSheet); 75 | dbUtils.getUpdatedData(); 76 | 77 | ArrayList columnLogged = 78 | dbUtils.getColumnData(Constants.kStudentIdColumn, Constants.kLogSheet); 79 | 80 | int i; 81 | for (i = 1; i < columnLogged.size(); i++) { 82 | if (columnLogged.get(i).equals("")) { 83 | break; 84 | } 85 | } 86 | 87 | data.clear(); 88 | addUserInfoBasic(userData, userID, data, i); 89 | 90 | dbUtils.setCellDataBatch(data, Constants.kLogSheet); 91 | dbUtils.getUpdatedData(); 92 | 93 | // ensure there is a date column 94 | logoutActivity.getCurrentDateColumn(); 95 | 96 | } else { 97 | LoggingUtils.log(Level.INFO, "Account Creation Cancelled"); 98 | throw new CancelledUserCreationException("Cancelled"); 99 | } 100 | } 101 | 102 | private void addUserInfoBasic( 103 | ArrayList userData, String userID, ArrayList data, int i) { 104 | data.add(new BatchUpdateData(i, Constants.kStudentIdColumn, userID)); 105 | data.add(new BatchUpdateData(i, Constants.kFirstNameColumn, userData.get(1))); 106 | data.add(new BatchUpdateData(i, Constants.kLastNameColumn, userData.get(2))); 107 | } 108 | 109 | public int doesIdExist(ArrayList ids, String userID) { 110 | // check if the user ID exists 111 | for (int i = 0; i < ids.size(); i++) { 112 | // if the user exists, check if logged in or logged out and return state 113 | if (ids.get(i).equals(userID)) { 114 | String cellData = dbUtils.getCellData(i, Constants.kLoggedInColumn, Constants.kMainSheet); 115 | try { 116 | cellData = cellData.replaceAll("\\s+", ""); 117 | 118 | } catch (NullPointerException e) { 119 | continue; 120 | // do nothing because the cell doesn't exist? 121 | } 122 | 123 | if (cellData.equals("TRUE")) { 124 | return Constants.kIdLoggedIn; 125 | 126 | } else { 127 | return Constants.kIdNotLoggedIn; 128 | } 129 | } 130 | } 131 | 132 | return Constants.kIdDoesNotExist; 133 | } 134 | 135 | // login our user 136 | public void loginUser(String userID) { 137 | Platform.runLater(() -> GrizzlyScene.setMessageBoxText("Logging in user: " + userID)); 138 | 139 | // grab the current time from system and format it into string 140 | LocalDateTime loginTime = LocalDateTime.now(); 141 | String formattedLoginTime = loginTime.format(formatter); 142 | 143 | int userRow = 144 | dbUtils.getCellRowFromColumn(userID, Constants.kStudentIdColumn, Constants.kMainSheet); 145 | 146 | // log the user in 147 | if (userRow != -1) { 148 | loginActivity.loginUser(userRow, formattedLoginTime); 149 | 150 | Platform.runLater( 151 | () -> { 152 | GrizzlyScene.setMessageBoxText("Successfully logged in user: " + userID); 153 | GrizzlyScene.clearInput(); 154 | }); 155 | } 156 | } 157 | 158 | // logout the user 159 | public void logoutUser(String userID) { 160 | Platform.runLater(() -> GrizzlyScene.setMessageBoxText("Logging out user: " + userID)); 161 | 162 | // grab the row the user is on 163 | int userRow = 164 | dbUtils.getCellRowFromColumn(userID, Constants.kStudentIdColumn, Constants.kMainSheet); 165 | 166 | // grab last logged in time 167 | LocalDateTime logoutTime = LocalDateTime.now(); 168 | LocalDateTime loginTime = 169 | LocalDateTime.parse( 170 | dbUtils.getCellData(userRow, Constants.kLastLoginColumn, Constants.kMainSheet), 171 | formatter); 172 | 173 | String formattedLogoutTime = logoutTime.format(formatter); 174 | 175 | // assuming userRow isn't invalid, calculate difference in time and log hours 176 | if (userRow != -1) { 177 | // update the logout time 178 | dbUtils.setCellData( 179 | userRow, Constants.kLastLogoutColumn, formattedLogoutTime, Constants.kMainSheet); 180 | 181 | int diffHours = logoutTime.getHour() - loginTime.getHour(); 182 | int diffMinutes = logoutTime.getMinute() - loginTime.getMinute(); 183 | int diffSeconds = logoutTime.getSecond() - loginTime.getSecond(); 184 | 185 | boolean err = false; 186 | 187 | if (diffHours < 0) { 188 | LoggingUtils.log( 189 | Level.SEVERE, 190 | "Well this is awkward, difference shouldn't be negative: h:" 191 | + diffHours 192 | + " m:" 193 | + diffMinutes 194 | + " s:" 195 | + diffSeconds); 196 | err = true; 197 | } 198 | 199 | if (diffSeconds < 0) { 200 | diffMinutes -= 1; 201 | diffSeconds = 60 - Math.abs(diffSeconds); 202 | } 203 | 204 | if (diffMinutes < 0) { 205 | diffHours -= 1; 206 | diffMinutes = 60 - Math.abs(diffMinutes); 207 | } 208 | 209 | if (loginTime.getYear() == logoutTime.getYear()) { 210 | if (loginTime.getMonth() == logoutTime.getMonth()) { 211 | if (loginTime.getDayOfMonth() != logoutTime.getDayOfMonth()) { 212 | err = true; 213 | } 214 | } else { 215 | err = true; 216 | } 217 | } else { 218 | err = true; 219 | } 220 | 221 | if (!err) { 222 | String totalTimeFromDifference = 223 | String.format("%02d:%02d:%02d", diffHours, diffMinutes, diffSeconds); 224 | LocalTime totalHoursTime = LocalTime.parse(totalTimeFromDifference); 225 | 226 | logoutActivity.logoutUserWithHours( 227 | userID, userRow, totalHoursTime, totalTimeFromDifference); 228 | } 229 | 230 | // logout the user 231 | dbUtils.setCellData(userRow, Constants.kLoggedInColumn, "FALSE", Constants.kMainSheet); 232 | 233 | if (err) { 234 | Platform.runLater( 235 | () -> { 236 | GrizzlyScene.setMessageBoxText("You forgot to log out! Please re-login!"); 237 | GrizzlyScene.clearInput(); 238 | }); 239 | } 240 | } 241 | } 242 | 243 | // checks if ID is valid long and x digit number (x based on config file) 244 | public boolean isValidID(String userID) { 245 | int idLength = LocalDbActivity.kIdLength; 246 | int mentorIdLength = LocalDbActivity.kIdLengthFallback; 247 | 248 | try { 249 | Long.parseLong(userID); 250 | 251 | if (Constants.kMentorFallback) { 252 | return userID.length() == idLength || userID.length() == mentorIdLength; 253 | 254 | } else { 255 | return userID.length() == idLength; 256 | } 257 | 258 | } catch (NumberFormatException e) { 259 | // not a valid ID 260 | return false; 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/main/java/helpers/AlertUtils.java: -------------------------------------------------------------------------------- 1 | package helpers; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Optional; 5 | import java.util.concurrent.atomic.AtomicBoolean; 6 | import java.util.concurrent.atomic.AtomicReference; 7 | import java.util.logging.Level; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | import javafx.application.Platform; 11 | import javafx.geometry.HPos; 12 | import javafx.geometry.Insets; 13 | import javafx.geometry.Pos; 14 | import javafx.scene.Node; 15 | import javafx.scene.control.*; 16 | import javafx.scene.image.Image; 17 | import javafx.scene.image.ImageView; 18 | import javafx.scene.layout.GridPane; 19 | import javafx.scene.text.Text; 20 | import javafx.stage.Stage; 21 | 22 | /** 23 | * @author Dalton Smith AlertUtils Various alert utility methods used throughout the application 24 | * https://code.makery.ch/blog/javafx-dialogs-official/ 25 | */ 26 | public class AlertUtils { 27 | 28 | public static Stage stage = null; 29 | 30 | // create alert 31 | public boolean createAlert(String title, String header, String content) { 32 | 33 | // ensure that we always show the dialog on the main UI thread 34 | if (Platform.isFxApplicationThread()) { 35 | return customDialog(title, header, content); 36 | 37 | } else { 38 | 39 | AtomicBoolean temp = new AtomicBoolean(); 40 | 41 | Platform.runLater(() -> temp.set(customDialog(title, header, content))); 42 | 43 | return temp.get(); 44 | } 45 | } 46 | 47 | // confirm new user registration 48 | public boolean confirmInput(String message) { 49 | AtomicBoolean tempBoolean = new AtomicBoolean(); 50 | AtomicBoolean isSet = new AtomicBoolean(); 51 | 52 | if (Platform.isFxApplicationThread()) { 53 | return customDialog("Confirm Login/Logout", "Confirm Login/Logout", message); 54 | 55 | } else { 56 | Platform.runLater( 57 | () -> { 58 | tempBoolean.set(customDialog("Confirm Login/Logout", "Confirm Login/Logout", message)); 59 | isSet.set(true); 60 | }); 61 | 62 | while (!isSet.get()) { 63 | // wait for the user to confirm the dialog 64 | } 65 | 66 | Boolean result = tempBoolean.get(); 67 | System.out.println(result); 68 | return tempBoolean.get(); 69 | } 70 | } 71 | 72 | private boolean customDialog(String title, String header, String message) { 73 | // Create the custom dialog. 74 | Dialog dialog = new Dialog<>(); 75 | dialog.setTitle(title); 76 | dialog.setHeaderText(header); 77 | dialog.initOwner(stage); 78 | 79 | dialog.getDialogPane().getStylesheets().add(Constants.kRootStylesheet); 80 | dialog.getDialogPane().getStyleClass().add("myDialog"); 81 | 82 | // Set Custom Icon 83 | Stage stage = (Stage) dialog.getDialogPane().getScene().getWindow(); 84 | stage.getIcons().add(new Image(Constants.kApplicationIcon)); 85 | 86 | // Set the button types. 87 | ButtonType confirmButton = new ButtonType("Ok", ButtonBar.ButtonData.OK_DONE); 88 | dialog.getDialogPane().getButtonTypes().addAll(confirmButton, ButtonType.CANCEL); 89 | 90 | Image image = new Image(Constants.kBearImage); 91 | ImageView imageView = new ImageView(image); 92 | 93 | imageView.setPreserveRatio(Constants.kBearPreserveRatio); 94 | imageView.setFitWidth(Constants.kBearImageWidth); 95 | 96 | // Set the icon (must be included in the project). 97 | dialog.setGraphic(imageView); 98 | 99 | // Create the username and password labels and fields. 100 | GridPane grid = new GridPane(); 101 | grid.setHgap(10); 102 | grid.setVgap(10); 103 | grid.setId("customDialog"); 104 | 105 | Text text = new Text(message); 106 | text.setWrappingWidth(Constants.kWordWrapWidth); 107 | 108 | grid.add(text, 0, 0); 109 | 110 | dialog.getDialogPane().setContent(grid); 111 | 112 | Optional result = dialog.showAndWait(); 113 | 114 | if (!result.isPresent()) { 115 | return false; 116 | } 117 | 118 | ButtonType buttonInfo = (ButtonType) result.get(); 119 | 120 | return buttonInfo.getButtonData() == ButtonBar.ButtonData.OK_DONE; 121 | } 122 | 123 | public ArrayList getUserInfo() { 124 | 125 | // run on FX application thread 126 | if (Platform.isFxApplicationThread()) { 127 | return showAuthDialog(); 128 | 129 | } else { 130 | AtomicReference> temp = new AtomicReference<>(); 131 | AtomicReference isSet = new AtomicReference<>(); 132 | 133 | isSet.set(false); 134 | Platform.runLater( 135 | () -> { 136 | temp.set(showAuthDialog()); 137 | isSet.set(true); 138 | }); 139 | 140 | // wait until the user has finished dialog 141 | while (!isSet.get()) {} 142 | 143 | return temp.get(); 144 | } 145 | } 146 | 147 | public String showSpreadsheetDialog() { 148 | // Create the custom dialog. 149 | Dialog dialog = new Dialog<>(); 150 | 151 | dialog.initOwner(stage); 152 | 153 | // Set Custom Icon 154 | Stage stage = (Stage) dialog.getDialogPane().getScene().getWindow(); 155 | stage.getIcons().add(new Image(Constants.kApplicationIcon)); 156 | 157 | dialog.getDialogPane().getStylesheets().add(Constants.kRootStylesheet); 158 | dialog.getDialogPane().getStyleClass().add("accountDialog"); 159 | 160 | dialog.setTitle("Spreadsheet Verification!"); 161 | dialog.setHeaderText("Please input your spreadsheet URL\n and press confirm!"); 162 | 163 | // Set the button types. 164 | ButtonType loginButtonType = new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE); 165 | dialog.getDialogPane().getButtonTypes().addAll(loginButtonType, ButtonType.CANCEL); 166 | 167 | // Create the username and password labels and fields. 168 | GridPane grid = new GridPane(); 169 | grid.setHgap(10); 170 | grid.setVgap(10); 171 | grid.setPadding(new Insets(20, 10, 10, 10)); 172 | grid.setAlignment(Pos.CENTER); 173 | grid.setId("accountGrid"); 174 | 175 | TextField sheet = new TextField(""); 176 | sheet.setMinWidth(400); 177 | 178 | grid.add(sheet, 0, 0); 179 | 180 | dialog.getDialogPane().setContent(grid); 181 | 182 | Node button = dialog.getDialogPane().lookupButton(loginButtonType); 183 | 184 | button.setDisable(true); 185 | 186 | sheet 187 | .textProperty() 188 | .addListener( 189 | (observable, oldValue, newValue) -> { 190 | if (newValue.contains("/d/")) { 191 | button.setDisable(false); 192 | } else { 193 | button.setDisable(true); 194 | } 195 | }); 196 | 197 | Platform.runLater(sheet::requestFocus); 198 | 199 | Optional result = dialog.showAndWait(); 200 | 201 | ButtonType resultButtonType = (ButtonType) result.get(); 202 | 203 | String spreadsheet = ""; 204 | if (resultButtonType.getButtonData() == ButtonBar.ButtonData.OK_DONE) { 205 | spreadsheet = sheet.getText().split("/d/")[1].split("/")[0]; 206 | LoggingUtils.log(Level.INFO, "Using ID of " + spreadsheet); 207 | } else { 208 | CommonUtils.exitApplication(); 209 | } 210 | 211 | return spreadsheet; 212 | } 213 | 214 | private ArrayList showAuthDialog() { 215 | // Create the custom dialog. 216 | Dialog dialog = new Dialog<>(); 217 | 218 | dialog.initOwner(stage); 219 | 220 | // Set Custom Icon 221 | Stage stage = (Stage) dialog.getDialogPane().getScene().getWindow(); 222 | stage.getIcons().add(new Image(Constants.kApplicationIcon)); 223 | 224 | dialog.getDialogPane().getStylesheets().add(Constants.kRootStylesheet); 225 | dialog.getDialogPane().getStyleClass().add("accountDialog"); 226 | 227 | dialog.setTitle("New User Detected"); 228 | dialog.setHeaderText("Please complete user registration!"); 229 | 230 | // Set the button types. 231 | ButtonType loginButtonType = new ButtonType("Create Account", ButtonBar.ButtonData.OK_DONE); 232 | dialog.getDialogPane().getButtonTypes().addAll(loginButtonType, ButtonType.CANCEL); 233 | 234 | // Create the username and password labels and fields. 235 | GridPane grid = new GridPane(); 236 | grid.setHgap(10); 237 | grid.setVgap(10); 238 | grid.setPadding(new Insets(20, 10, 10, 10)); 239 | grid.setAlignment(Pos.CENTER); 240 | grid.setId("accountGrid"); 241 | 242 | TextField firstName = new TextField(""); 243 | // GridPane.setHgrow(firstName, Priority.ALWAYS); 244 | TextField lastName = new TextField(""); 245 | 246 | TextField email = new TextField(""); 247 | 248 | firstName.setId("textField"); 249 | lastName.setId("textField"); 250 | 251 | firstName.setPromptText(""); 252 | lastName.setPromptText(""); 253 | 254 | email.setPromptText(""); 255 | 256 | email.setId("textField"); 257 | 258 | ToggleGroup studentRadioGroup = new ToggleGroup(); 259 | 260 | RadioButton mentorRadio = new RadioButton("Mentor"); 261 | RadioButton studentRadio = new RadioButton("Student"); 262 | 263 | ToggleGroup genderRadioGroup = new ToggleGroup(); 264 | 265 | RadioButton maleRadio = new RadioButton("Male"); 266 | RadioButton otherRadio = new RadioButton("Other"); 267 | RadioButton femaleRadio = new RadioButton("Female"); 268 | 269 | otherRadio.fire(); 270 | 271 | studentRadio.fire(); 272 | 273 | mentorRadio.setToggleGroup(studentRadioGroup); 274 | studentRadio.setToggleGroup(studentRadioGroup); 275 | 276 | GridPane genderPane = new GridPane(); 277 | 278 | maleRadio.setToggleGroup(genderRadioGroup); 279 | femaleRadio.setToggleGroup(genderRadioGroup); 280 | otherRadio.setToggleGroup(genderRadioGroup); 281 | 282 | genderPane.add(maleRadio, 0, 0); 283 | genderPane.add(femaleRadio, 1, 0); 284 | genderPane.add(otherRadio, 2, 0); 285 | 286 | grid.add(new Label("First Name:"), 0, 0); 287 | grid.add(firstName, 1, 0); 288 | grid.add(new Label("Last Name:"), 0, 1); 289 | grid.add(lastName, 1, 1); 290 | grid.add(new Label("Email:"), 0, 2); 291 | grid.add(email, 1, 2); 292 | 293 | GridPane.setColumnSpan(genderPane, 2); 294 | grid.add(genderPane, 0, 3); 295 | 296 | GridPane.setHalignment(grid, HPos.CENTER); 297 | 298 | GridPane.setHalignment(studentRadio, HPos.CENTER); 299 | GridPane.setHalignment(studentRadio, HPos.CENTER); 300 | 301 | grid.add(studentRadio, 0, 4); 302 | grid.add(mentorRadio, 1, 4); 303 | 304 | dialog.getDialogPane().setContent(grid); 305 | 306 | Node button = dialog.getDialogPane().lookupButton(loginButtonType); 307 | 308 | button.setDisable(true); 309 | 310 | email 311 | .textProperty() 312 | .addListener( 313 | (observable, oldValue, newValue) -> { 314 | Pattern regex = 315 | Pattern.compile( 316 | "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE); 317 | Matcher matcher = regex.matcher(newValue); 318 | 319 | if (matcher.matches()) { 320 | button.setDisable(false); 321 | } 322 | }); 323 | 324 | // Request focus on the firstname field by default. 325 | Platform.runLater(firstName::requestFocus); 326 | 327 | Optional result = dialog.showAndWait(); 328 | 329 | ArrayList data = new ArrayList<>(); 330 | 331 | ButtonType resultButtonType = (ButtonType) result.get(); 332 | 333 | if (resultButtonType.getButtonData() == ButtonBar.ButtonData.OK_DONE) { 334 | data.add("TRUE"); 335 | data.add(firstName.getText()); 336 | data.add(lastName.getText()); 337 | data.add(email.getText()); 338 | 339 | if (maleRadio.isSelected()) { 340 | data.add("MALE"); 341 | } else if (femaleRadio.isSelected()) { 342 | data.add("FEMALE"); 343 | } else { 344 | data.add("OTHER"); 345 | } 346 | 347 | data.add(mentorRadio.isSelected() ? "MENTOR" : "STUDENT"); 348 | 349 | } else { 350 | data.add("FALSE"); 351 | } 352 | 353 | return data; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/main/java/databases/DatabaseProcess.java: -------------------------------------------------------------------------------- 1 | package databases; 2 | 3 | import activities.LocalDbActivity; 4 | import com.google.api.client.auth.oauth2.Credential; 5 | import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; 6 | import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; 7 | import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; 8 | import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; 9 | import com.google.api.client.googleapis.json.GoogleJsonResponseException; 10 | import com.google.api.client.http.javanet.NetHttpTransport; 11 | import com.google.api.client.json.JsonFactory; 12 | import com.google.api.client.json.jackson2.JacksonFactory; 13 | import com.google.api.client.util.store.FileDataStoreFactory; 14 | import com.google.api.services.sheets.v4.Sheets; 15 | import com.google.api.services.sheets.v4.SheetsScopes; 16 | import com.google.api.services.sheets.v4.model.*; 17 | import helpers.AlertUtils; 18 | import helpers.CommonUtils; 19 | import helpers.Constants; 20 | import helpers.LoggingUtils; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.io.InputStreamReader; 24 | import java.net.*; 25 | import java.security.GeneralSecurityException; 26 | import java.util.ArrayList; 27 | import java.util.Collections; 28 | import java.util.List; 29 | import java.util.logging.Level; 30 | import javafx.application.HostServices; 31 | 32 | public class DatabaseProcess { 33 | /** 34 | * @author Dalton Smith DatabaseProcess Manages low level google sheets handling from API Uses the 35 | * power of black magic to manage the google sheet Based on Java QuickStart 36 | */ 37 | private static final String APPLICATION_NAME = "GrizzlyTime JavaFX Edition"; 38 | 39 | private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); 40 | private static final String TOKENS_DIRECTORY_PATH = "tokens"; 41 | 42 | private static final List SCOPES = Collections.singletonList(SheetsScopes.SPREADSHEETS); 43 | private static final String CREDENTIALS_FILE_PATH = "/credentials/credentials.json"; 44 | 45 | private AlertUtils util = new AlertUtils(); 46 | 47 | private static final String spreadsheet = LocalDbActivity.kSheetId; 48 | 49 | private static final String mainPage = "Current"; 50 | private static final String logPage = "Date Log"; 51 | private static final String regPage = "Student Registration"; 52 | 53 | public static boolean worksheetIsEmpty = false; 54 | 55 | // based upon the Java Google Sheets quickstart 56 | // settings google sheets logging level to SEVERE only 57 | private static Credential getCredentials( 58 | final NetHttpTransport HTTP_TRANSPORT) throws IOException { 59 | // set google logging level to severe due to permissions bug, see 60 | // https://github.com/googleapis/google-http-java-client/issues/315 61 | java.util.logging.Logger.getLogger(FileDataStoreFactory.class.getName()).setLevel(Level.SEVERE); 62 | 63 | // Load client secrets. 64 | InputStream in = DatabaseProcess.class.getResourceAsStream(CREDENTIALS_FILE_PATH); 65 | 66 | if (in == null) { 67 | LoggingUtils.log( 68 | Level.SEVERE, 69 | "Credentials file was not loaded, check that the credentials file is in the resources directory!"); 70 | CommonUtils.exitApplication(); 71 | } 72 | 73 | GoogleClientSecrets clientSecrets = 74 | GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in)); 75 | 76 | // Build flow and trigger user authorization request. 77 | GoogleAuthorizationCodeFlow flow = 78 | new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES) 79 | .setDataStoreFactory(new FileDataStoreFactory(new java.io.File(TOKENS_DIRECTORY_PATH))) 80 | .setAccessType("offline") 81 | .build(); 82 | 83 | return new AuthorizationCodeInstalledApp( 84 | flow, 85 | new com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver(), 86 | new AuthorizationCodeInstalledApp.Browser() { 87 | @Override 88 | public void browse(String url) { 89 | LoggingUtils.log( 90 | Level.INFO, "Or navigate to " + url + " to authorize the application."); 91 | // fix for java8 launching on linux because weird shit 92 | HostServices hostServices = CommonUtils.application.getHostServices(); 93 | try { 94 | hostServices.showDocument(url); 95 | } catch (NullPointerException e) { 96 | LoggingUtils.log( 97 | Level.INFO, 98 | "HostServices failed, please navigate to " 99 | + url 100 | + " to authorize the application."); 101 | } 102 | } 103 | }) 104 | .authorize("user"); 105 | } 106 | 107 | // return a list of rows and columns of a specified sheet 108 | public List> returnWorksheetData(int page) { 109 | 110 | try { 111 | String range = getPage(page); 112 | 113 | final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); 114 | Sheets service = 115 | new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT)) 116 | .setApplicationName(APPLICATION_NAME) 117 | .build(); 118 | 119 | return getDataFromSheet(service, range); 120 | 121 | } catch (NoRouteToHostException | UnknownHostException e) { 122 | LoggingUtils.log(Level.SEVERE, e); 123 | util.createAlert( 124 | "ERROR", 125 | "No Network Connection", 126 | "Please confirm your network connection and try again."); 127 | 128 | CommonUtils.exitApplication(); 129 | return null; 130 | 131 | } catch (GeneralSecurityException e2) { 132 | LoggingUtils.log(Level.SEVERE, e2); 133 | util.createAlert( 134 | "ERROR", "Invalid Credentials", "Please delete the 'tokens' directory and try again!"); 135 | 136 | CommonUtils.exitApplication(); 137 | return null; 138 | 139 | } catch (GoogleJsonResponseException e) { 140 | LoggingUtils.log(Level.SEVERE, e.getDetails().getMessage()); 141 | 142 | try { 143 | if (e.getDetails().getMessage().contains("Unable to parse range")) { 144 | LoggingUtils.log(Level.WARNING, "Attempting to create our sheets"); 145 | 146 | final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); 147 | Sheets service = 148 | new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT)) 149 | .setApplicationName(APPLICATION_NAME) 150 | .build(); 151 | 152 | ArrayList requests = new ArrayList<>(); 153 | 154 | // add our sheets to the list of sheets to be processed 155 | if (e.getDetails().getMessage().contains("Current")) { 156 | requests.add( 157 | new Request() 158 | .setAddSheet( 159 | new AddSheetRequest() 160 | .setProperties(new SheetProperties().setTitle("Current")))); 161 | LoggingUtils.log(Level.INFO, "Creating Current Sheet"); 162 | } 163 | 164 | if (e.getDetails().getMessage().contains("Date Log")) { 165 | requests.add( 166 | new Request() 167 | .setAddSheet( 168 | new AddSheetRequest() 169 | .setProperties(new SheetProperties().setTitle("Date Log")))); 170 | LoggingUtils.log(Level.INFO, "Creating Date Log Sheet"); 171 | } 172 | 173 | BatchUpdateSpreadsheetRequest body = 174 | new BatchUpdateSpreadsheetRequest().setRequests(requests); 175 | 176 | BatchUpdateSpreadsheetResponse updateResponse = 177 | service.spreadsheets().batchUpdate(spreadsheet, body).execute(); 178 | 179 | LoggingUtils.log(Level.INFO, updateResponse.getReplies().toString()); 180 | 181 | String range = getPage(page); 182 | 183 | return getDataFromSheet(service, range); 184 | } 185 | 186 | LoggingUtils.log(Level.SEVERE, "Unknown exception"); 187 | LoggingUtils.log(Level.SEVERE, e); 188 | CommonUtils.exitApplication(); 189 | return null; 190 | 191 | } catch (Exception e2) { 192 | LoggingUtils.log(Level.SEVERE, e); 193 | util.createAlert( 194 | "ERROR", "Error creating sheets", "Specified sheets do not exist, failed to create"); 195 | 196 | CommonUtils.exitApplication(); 197 | return null; 198 | } 199 | 200 | // attempt to create our sheets 201 | 202 | } catch (IOException e3) { 203 | LoggingUtils.log(Level.SEVERE, e3); 204 | 205 | util.createAlert( 206 | "ERROR", 207 | "Error connecting to database", 208 | "Please confirm that the URL is valid and that you have internet access."); 209 | 210 | CommonUtils.exitApplication(); 211 | return null; 212 | } 213 | } 214 | 215 | private List> getDataFromSheet(Sheets service, String range) throws IOException { 216 | ValueRange response = null; 217 | 218 | try { 219 | response = service.spreadsheets().values().get(spreadsheet, range).execute(); 220 | 221 | } catch (NoRouteToHostException e) { 222 | LoggingUtils.log(Level.SEVERE, "No route to host!"); 223 | util.createAlert( 224 | "ERROR", 225 | "No Route to Host", 226 | "Unable to connect to the database, check internet connection and restart the application."); 227 | CommonUtils.exitApplication(); 228 | 229 | } catch (SocketTimeoutException e) { 230 | LoggingUtils.log(Level.SEVERE, "Connection timed out!"); 231 | util.createAlert( 232 | "ERROR", 233 | "Connection timed out", 234 | "Connecting to database took too long! Please check your internet connection and retry!"); 235 | CommonUtils.exitApplication(); 236 | } 237 | 238 | if (response.getValues() == null) { 239 | worksheetIsEmpty = true; 240 | } 241 | 242 | return response.getValues(); 243 | } 244 | 245 | public void updateSpreadSheet(int row, int column, String data, int page) { 246 | 247 | // Build a new authorized API client service. 248 | String columnLetter = toAlphabetic(column - 1); 249 | 250 | String range = columnLetter + row; 251 | String sheetPage = getPage(page); 252 | range = sheetPage + "!" + range; 253 | 254 | ValueRange requestBody = new ValueRange(); 255 | requestBody.setValues(Collections.singletonList(Collections.singletonList(data))); 256 | 257 | try { 258 | final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); 259 | Sheets service = 260 | new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT)) 261 | .setApplicationName(APPLICATION_NAME) 262 | .build(); 263 | 264 | service 265 | .spreadsheets() 266 | .values() 267 | .update(spreadsheet, range, requestBody) 268 | .setValueInputOption("RAW") 269 | .execute(); 270 | 271 | } catch (GeneralSecurityException e) { 272 | LoggingUtils.log(Level.SEVERE, "INVALID CREDENTIALS"); 273 | 274 | } catch (GoogleJsonResponseException e) { 275 | if (e.getDetails().getMessage().contains("exceeds grid limits")) { 276 | LoggingUtils.log(Level.WARNING, "Appending Data"); 277 | try { 278 | final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); 279 | Sheets service = 280 | new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT)) 281 | .setApplicationName(APPLICATION_NAME) 282 | .build(); 283 | 284 | service 285 | .spreadsheets() 286 | .values() 287 | .append(spreadsheet, range, requestBody) 288 | .setValueInputOption("RAW") 289 | .execute(); 290 | 291 | } catch (IOException e1) { 292 | LoggingUtils.log(Level.SEVERE, e); 293 | 294 | } catch (GeneralSecurityException e1) { 295 | LoggingUtils.log(Level.SEVERE, e); 296 | util.createAlert( 297 | "ERROR", 298 | "Invalid Credentials", 299 | "You do not have permission to edit this spreadsheet!"); 300 | } 301 | 302 | } else { 303 | LoggingUtils.log(Level.SEVERE, e); 304 | } 305 | 306 | } catch (IOException e2) { 307 | LoggingUtils.log(Level.SEVERE, e2); 308 | // do nothing 309 | 310 | } 311 | } 312 | 313 | public void updateSpreadSheetBatch(ArrayList batchData, int page) { 314 | try { 315 | LoggingUtils.log(Level.INFO, "Batch updating sheet"); 316 | final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); 317 | Sheets service = 318 | new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT)) 319 | .setApplicationName(APPLICATION_NAME) 320 | .build(); 321 | 322 | List requestData = new ArrayList<>(); 323 | 324 | for (BatchUpdateData data : batchData) { 325 | // Build a new authorized API client service. 326 | String columnLetter = toAlphabetic(data.getColumn() - 1); 327 | 328 | String range = columnLetter + data.getRow(); 329 | 330 | String sheetPage = getPage(page); 331 | range = sheetPage + "!" + range; 332 | 333 | requestData.add( 334 | new ValueRange() 335 | .setRange(range) 336 | .setValues(Collections.singletonList(Collections.singletonList(data.getData())))); 337 | } 338 | 339 | BatchUpdateValuesRequest batchBody = 340 | new BatchUpdateValuesRequest().setValueInputOption("RAW").setData(requestData); 341 | 342 | // TODO check response 343 | service.spreadsheets().values().batchUpdate(spreadsheet, batchBody).execute(); 344 | 345 | } catch (GeneralSecurityException e) { 346 | LoggingUtils.log(Level.SEVERE, "INVALID CREDENTIALS"); 347 | 348 | } catch (IOException e2) { 349 | LoggingUtils.log(Level.SEVERE, e2); 350 | // do nothing 351 | 352 | } 353 | } 354 | 355 | // update current working page 356 | private String getPage(int page) { 357 | switch (page) { 358 | case Constants.kMainSheet: 359 | return mainPage; 360 | 361 | case Constants.kLogSheet: 362 | return logPage; 363 | 364 | case Constants.kRegistrationSheet: 365 | return regPage; 366 | 367 | default: 368 | return mainPage; 369 | } 370 | } 371 | 372 | public static String toAlphabetic(int i) { 373 | if (i < 0) { 374 | return "-" + toAlphabetic(-i - 1); 375 | } 376 | 377 | int quot = i / 26; 378 | int rem = i % 26; 379 | char letter = (char) ((int) 'A' + rem); 380 | if (quot == 0) { 381 | return "" + letter; 382 | 383 | } else { 384 | return toAlphabetic(quot - 1) + letter; 385 | } 386 | } 387 | } 388 | --------------------------------------------------------------------------------