Initial commit

This commit is contained in:
Deniz Düzgören
2020-02-16 12:58:42 +01:00
commit bbec19e79c
235 changed files with 11134 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
#Wed Feb 12 10:01:56 CET 2020
gradle.version=6.1-rc-1
Binary file not shown.
Binary file not shown.
View File
+119
View File
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>
</project>
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/modules/Qwark.iml" filepath="$PROJECT_DIR$/.idea/modules/Qwark.iml" group="Qwark" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/app.iml" group="Qwark/app" />
</modules>
</component>
</project>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="Qwark" external.linked.project.path="$MODULE_DIR$/../.." external.root.project.path="$MODULE_DIR$/../.." external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle">
<configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/../../build" />
<option name="BUILDABLE" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$/../..">
<excludeFolder url="file://$MODULE_DIR$/../../.gradle" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+210
View File
File diff suppressed because one or more lines are too long
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="Qwark" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle">
<configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
<option name="BUILDABLE" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
</content>
<orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+1
View File
@@ -0,0 +1 @@
/build
+210
View File
File diff suppressed because one or more lines are too long
+62
View File
@@ -0,0 +1,62 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
defaultConfig {
applicationId "com.denizd.qwark"
minSdkVersion 23
targetSdkVersion 29
versionCode 2
versionName "0.2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation "androidx.fragment:fragment-ktx:1.2.1"
testImplementation 'junit:junit:4.12'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.2.0-alpha04'
implementation 'androidx.preference:preference-ktx:1.1.0'
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
def room_version = "2.2.3" // "2.1.0-alpha04"
implementation "androidx.room:room-runtime:2.2.3"
kapt "androidx.room:room-compiler:$room_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'com.shawnlin:number-picker:2.4.8'
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
{
"version": 1,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.denizd.qwark",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"properties": [],
"versionCode": 1,
"versionName": "1",
"enabled": true,
"outputFile": "app-release.apk"
}
]
}
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.denizd.qwark">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme.Launcher">
<activity android:name=".activity.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,137 @@
package com.denizd.qwark.activity
import android.Manifest
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.TextView
import androidx.core.app.ActivityCompat
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import com.denizd.qwark.R
import com.denizd.qwark.fragment.*
import com.denizd.qwark.fragment.CourseFragment
import com.denizd.qwark.fragment.ParticipationFragment
import com.denizd.qwark.viewmodel.MainViewModel
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomnavigation.BottomNavigationView
internal class MainActivity : AppCompatActivity(R.layout.activity_main) {
private lateinit var toolbarTitle: TextView
private lateinit var bottomNavigationView: BottomNavigationView
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTheme(R.style.AppTheme)
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 42)
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
viewModel.applyTheme(window, this)
viewModel.checkForFirstTimeYearNotice(this)
val actionBar = findViewById<AppBarLayout>(R.id.app_bar_layout)
toolbarTitle = findViewById(R.id.toolbarTxt)
bottomNavigationView = findViewById(R.id.bottom_nav)
findViewById<AppBarLayout>(R.id.app_bar_layout).also { appBarLayout ->
appBarLayout.setOnApplyWindowInsetsListener { _, insets ->
(appBarLayout.layoutParams as ViewGroup.MarginLayoutParams)
.topMargin = insets.systemWindowInsetTop
insets
}
}
window.decorView.systemUiVisibility = if (resources.getBoolean(R.bool.isLight)) {
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
window.attributes = window.attributes.apply {
flags and WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS.inv()
}
window.statusBarColor = Color.TRANSPARENT
bottomNavigationView.setOnNavigationItemSelectedListener { item ->
actionBar.setExpanded(true)
loadFragment(viewModel.getFragmentType(item.itemId))
}
// TODO figure out why this isn't working
// if (savedInstanceState == null) {
// supportFragmentManager.beginTransaction().apply {
// val fragments = arrayOf(
// CourseFragment(),
//// GradeFragment(),
// NoteFragment(),
// ParticipationFragment(),
//// ParticipationCourseFragment(),
// FinalsFragment(),
// SettingsFragment()
// )
// for (fragment in fragments) {
// add(R.id.fragment_container, fragment, fragment.name)
// if (fragment !is CourseFragment) hide(fragment)
// }
// }.commit()
// }
// TODO reselected listener for scrolling up
}
override fun onResume() {
super.onResume()
if (supportFragmentManager.fragments.size == 0) {
bottomNavigationView.selectedItemId = R.id.courses
loadFragment(viewModel.getFragmentType(R.id.courses))
}
}
private fun loadFragment(type: String): Boolean {
val fragment = supportFragmentManager.findFragmentByTag(type) ?: viewModel.getFragment(type)
if (fragment.lifecycle.currentState != Lifecycle.State.INITIALIZED) {
return false
}
supportFragmentManager.beginTransaction().replace(R.id.fragment_container, fragment, type)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN).commit()
// with (supportFragmentManager) {
// beginTransaction()
// .hide(fragments[fragments.size - 1]) // should this be 0?
// .show(findFragmentByTag(type) ?: TODO())
// .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
// .commit()
// }
return true
}
override fun onBackPressed() {
// Log.d("TAG2", "${supportFragmentManager.}") // supportFragmentManager.fragments[0].tag} ${supportFragmentManager.fragments[0].javaClass.simpleName
when (supportFragmentManager.fragments[0].tag) {
"GradeFragment", "ParticipationCourseFragment" -> {
bottomNavigationView.visibility = View.INVISIBLE
findViewById<View>(R.id.view).visibility = View.INVISIBLE
super.onBackPressed()
}
"NoteFragment" -> {
if ((supportFragmentManager.fragments[0] as NoteFragment).onBackPressed()) {
super.onBackPressed()
}
}
else -> {
bottomNavigationView.visibility = View.VISIBLE
findViewById<View>(R.id.view).visibility = View.VISIBLE
super.onBackPressed()
}
}
}
}
@@ -0,0 +1,116 @@
package com.denizd.qwark.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import com.denizd.qwark.R
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.model.Course
import com.denizd.qwark.model.CourseExam
import com.denizd.qwark.util.getUserDefinedAverage
import com.denizd.qwark.util.roundToInt
class CourseAdapter(private var courses: List<CourseExam>,
private val onClickListener: CourseClickListener,
private val gradeType: Int?,
private val isParticipationFragment: Boolean = false) : RecyclerView.Adapter<CourseAdapter.CourseViewHolder>() {
class CourseViewHolder(view: View, private val clickListener: CourseClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener {
val backgroundView: ConstraintLayout = view.findViewById(R.id.backgroundLayout)
val titleTextView: TextView = view.findViewById(R.id.title)
val averageTextView: TextView = view.findViewById(R.id.average)
val imageView: ImageView = view.findViewById(R.id.image_view)
lateinit var currentCourse: CourseExam
init {
view.setOnClickListener(this)
view.setOnLongClickListener(this)
}
override fun onClick(v: View?) {
clickListener.onCourseClick(currentCourse)
}
override fun onLongClick(v: View?): Boolean {
clickListener.onCourseLongClick(currentCourse)
return true
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CourseViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.course_card, parent, false)
return CourseViewHolder(v, onClickListener)
}
override fun onBindViewHolder(holder: CourseViewHolder, position: Int) {
val currentItem = courses[position]
val context = holder.averageTextView.context
holder.backgroundView.setBackgroundColor(
context.getColor(if (currentItem.advanced) R.color.colorAccent else R.color.colorCardBackground)
)
val textColour = context.getColor(if (currentItem.advanced) R.color.colorTextInverted else R.color.colorText)
val titleText = if (currentItem.advanced) context.getString(R.string.advanced_abbreviated_placeholder) else "%s"
holder.titleTextView.text = String.format(titleText, currentItem.name)
val averageText = when (currentItem.gradeCount) {
0 -> context.getString(R.string.average_points_no_grade_placeholder)
else -> when (gradeType) {
QwarkUtil.GRADE_PRECISE, QwarkUtil.GRADE_ROUNDED ->
context.resources.getQuantityString(R.plurals.average_points_placeholder,
if (currentItem.average == "1.00") 1 else 2,
currentItem.average.getUserDefinedAverage(gradeType)
)
else -> context.getString(R.string.average_grade_placeholder, currentItem.average.getUserDefinedAverage(gradeType))
}
}
var timeLeft = ((currentItem.examTime.toDouble() - QwarkUtil.timeAtMidnight.toDouble()) / (1000.0 * 60.0 * 60.0 * 24.0)).roundToInt()
val examText = when (timeLeft) {
0 -> context.getString(R.string.exam_today_placeholder)
else -> {
when {
timeLeft > 7 -> {
timeLeft /= 7
context.resources.getQuantityString(R.plurals.exam_weeks_placeholder, timeLeft, timeLeft.toString())
}
timeLeft in 1..7 -> context.resources.getQuantityString(R.plurals.exam_days_placeholder, timeLeft, timeLeft.toString())
else -> ""
}
}
}
holder.averageTextView.text = if (isParticipationFragment) {
val args = currentItem.participation.split('|')
context.getString(R.string.participation_placeholder, args[0], args[1])
} else buildString {
append(averageText)
append(examText)
}
if (currentItem.icon.isNotEmpty()) {
holder.imageView.setImageDrawable(
context.getDrawable(QwarkUtil.getDrawableIntForString(currentItem.icon))
)
}
holder.titleTextView.setTextColor(textColour)
holder.averageTextView.setTextColor(textColour)
holder.imageView.setColorFilter(textColour)
holder.currentCourse = currentItem // QwarkUtil.getCourseExamAsCourse(currentItem)
}
override fun getItemCount(): Int = courses.size
fun setCourses(courses: List<CourseExam>) {
this.courses = courses
notifyDataSetChanged()
}
interface CourseClickListener {
fun onCourseClick(course: CourseExam)
fun onCourseLongClick(course: CourseExam)
}
}
@@ -0,0 +1,72 @@
package com.denizd.qwark.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.denizd.qwark.R
import com.denizd.qwark.model.FinalGrade
internal class FinalGradeAdapter(private var finalGrades: List<FinalGrade>, private val onClickListener: FinalGradeClickListener) : RecyclerView.Adapter<FinalGradeAdapter.FinalGradeViewHolder>() {
internal class FinalGradeViewHolder(view: View, private val clickListener: FinalGradeClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener {
internal val name: TextView = view.findViewById(R.id.name)
internal val grade: TextView = view.findViewById(R.id.grade)
internal val multiplier: TextView = view.findViewById(R.id.multiplier)
internal lateinit var currentFinalGrade: FinalGrade
init {
view.setOnClickListener(this)
// view.setOnLongClickListener(this)
}
override fun onClick(v: View?) {
clickListener.onFinalGradeClick(currentFinalGrade)
}
override fun onLongClick(v: View?): Boolean {
// clickListener.onFinalGradeLongClick(currentFinalGrade)
return true
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FinalGradeViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.final_grade_item, parent, false)
return FinalGradeViewHolder(v, onClickListener)
}
override fun onBindViewHolder(holder: FinalGradeViewHolder, position: Int) {
val currentItem = finalGrades[position]
if (currentItem.grade == "-1") {
holder.name.text = "---"
holder.grade.text = "––"
} else {
holder.name.text = currentItem.name
holder.grade.text = currentItem.grade // QwarkUtil.getGradeWithLeadingZero(currentItem.grade.toInt())
}
holder.multiplier.text = currentItem.note
val linkDrawable = if (currentItem.courseId != -1) {
R.drawable.link
} else {
0
}
holder.multiplier.setCompoundDrawablesWithIntrinsicBounds(linkDrawable, 0, 0, 0)
holder.currentFinalGrade = currentItem
}
override fun getItemCount(): Int = finalGrades.size
fun setFinalGrades(finalGrades: List<FinalGrade>) {
this.finalGrades = finalGrades
notifyDataSetChanged()
}
internal interface FinalGradeClickListener {
fun onFinalGradeClick(finalGrade: FinalGrade)
// fun onFinalGradeLongClick(finalGradeId: Int)
}
}
@@ -0,0 +1,87 @@
package com.denizd.qwark.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.denizd.qwark.R
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.model.Grade
import com.denizd.qwark.util.getUserDefinedAverage
import java.util.*
class GradeAdapter(private var grades: List<Grade>, private val onClickListener: GradeClickListener, private val gradeType: Int) : RecyclerView.Adapter<GradeAdapter.GradeViewHolder>() {
class GradeViewHolder(view: View, private val clickListener: GradeClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener {
val grade: TextView = view.findViewById(R.id.grade)
val notes: TextView = view.findViewById(R.id.notes)
val info: TextView = view.findViewById(R.id.info)
val progressBar: ProgressBar = view.findViewById(R.id.progress_bar)
lateinit var currentGrade: Grade
init {
view.setOnClickListener(this)
view.setOnLongClickListener(this)
}
override fun onClick(v: View?) {
clickListener.onGradeClick(currentGrade)
}
override fun onLongClick(v: View?): Boolean {
clickListener.onGradeLongClick(currentGrade.gradeId)
return true
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GradeViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.grade_card, parent, false)
return GradeViewHolder(v, onClickListener)
}
override fun onBindViewHolder(holder: GradeViewHolder, position: Int) {
val currentItem = grades[position]
holder.grade.text = if (currentItem.grade == -1) {
"??"
} else {
QwarkUtil.getGradeAsString(currentItem.grade, true).getUserDefinedAverage(gradeType, true)
}
holder.notes.text = if (currentItem.note.isEmpty()) "---" else currentItem.note
holder.info.text = buildString {
append(
holder.info.context.getString(
if (currentItem.verbal) R.string.verbal_placeholder else R.string.written_placeholder
)
)
if (currentItem.examTime != -1L) append(
holder.info.context.getString(
R.string.exam_on_placeholder,
QwarkUtil.getSimpleDateFormat().format(Date(currentItem.examTime)
).toString()
)
)
}
holder.progressBar.apply {
max = 100
progress = currentItem.weighting
}
holder.currentGrade = currentItem
}
override fun getItemCount(): Int = grades.size
fun setGrades(grades: List<Grade>) {
this.grades = grades
notifyDataSetChanged()
}
interface GradeClickListener {
fun onGradeClick(grade: Grade)
fun onGradeLongClick(gradeId: Int)
}
}
@@ -0,0 +1,39 @@
package com.denizd.qwark.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.denizd.qwark.R
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.model.HistoricalAvg
import java.util.*
internal class HistoricalAverageAdapter(private var averages: List<HistoricalAvg>?) : RecyclerView.Adapter<HistoricalAverageAdapter.AverageViewHolder>() {
internal class AverageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val grade: TextView = view.findViewById(R.id.grade)
val time: TextView = view.findViewById(R.id.time)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AverageViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.average_card, parent, false)
return AverageViewHolder(v)
}
override fun onBindViewHolder(holder: AverageViewHolder, position: Int) {
val currentItem = averages!![position]
val leadingZero = if (currentItem.average.toDouble() < 10.0) "0%s" else "%s"
holder.grade.text = String.format(leadingZero, currentItem.average) // TODO this conflicts with Points (Precise)
holder.time.text = QwarkUtil.getSimpleDateTimeFormat().format(Date(currentItem.time))
}
override fun getItemCount(): Int = averages?.size ?: 0
fun setAverages(averages: List<HistoricalAvg>) {
this.averages = averages
notifyDataSetChanged()
}
}
@@ -0,0 +1,73 @@
package com.denizd.qwark.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import com.denizd.qwark.R
import com.denizd.qwark.model.CourseIcon
import com.google.android.material.card.MaterialCardView
internal class IconAdapter(private var icons: List<CourseIcon>, private val onClickListener: IconClickListener) : RecyclerView.Adapter<IconAdapter.IconViewHolder>() {
private var selectedItem = 0
internal class IconViewHolder(view: View, private val clickListener: IconClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener {
val imageView: ImageView = view.findViewById(R.id.image_view)
val card: MaterialCardView = view.findViewById(R.id.card)
var image: Int = 0
var key: String = ""
init {
view.setOnClickListener(this)
}
override fun onClick(v: View?) {
clickListener.onIconClick(key, image, adapterPosition)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IconViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.icon_item, parent, false)
return IconViewHolder(v, onClickListener)
}
override fun onBindViewHolder(holder: IconViewHolder, position: Int) {
val currentItem = icons[position]
val context = holder.card.context
holder.imageView.setImageDrawable(context.getDrawable(currentItem.value))
holder.image = currentItem.value
holder.key = currentItem.key
holder.card.setCardBackgroundColor(context.getColor(if (position == selectedItem) {
R.color.colorAccent
} else {
R.color.colorCardBackground
}))
holder.imageView.setColorFilter(context.getColor(if (position == selectedItem) {
R.color.colorBackground
} else {
R.color.colorText
}))
}
override fun getItemCount(): Int = icons.size
fun setIcons(icons: List<CourseIcon>) {
this.icons = icons
notifyDataSetChanged()
}
fun setSelectedItem(position: Int) {
selectedItem = position
notifyDataSetChanged()
}
internal interface IconClickListener {
fun onIconClick(key: String, value: Int, position: Int)
}
}
@@ -0,0 +1,81 @@
package com.denizd.qwark.adapter
import android.text.SpannableString
import android.text.style.StrikethroughSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.denizd.qwark.R
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.model.Note
import com.google.android.material.chip.Chip
internal class NoteAdapter(private var notes: List<Note>, private val onClickListener: OnNoteClickListener) : RecyclerView.Adapter<NoteAdapter.NoteViewHolder>() {
internal class NoteViewHolder(view: View, private val clickListener: OnNoteClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener {
val content: TextView = view.findViewById(R.id.content)
val created: TextView = view.findViewById(R.id.created)
val category: Chip = view.findViewById(R.id.category_chip)
internal lateinit var currentNote: Note
init {
view.setOnClickListener(this)
view.setOnLongClickListener(this)
category.setOnClickListener {
clickListener.onCategoryChipClick(currentNote.category)
}
}
override fun onClick(v: View?) {
clickListener.onNoteClick(currentNote)
}
override fun onLongClick(v: View?): Boolean {
clickListener.onNoteLongClick(currentNote)
return true
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.note_card, parent, false)
return NoteViewHolder(v, onClickListener)
}
override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
val currentItem = notes[position]
val content = SpannableString(currentItem.content)
val contentColour = ContextCompat.getColor(holder.content.context, if (currentItem.dismissed) {
content.setSpan(StrikethroughSpan(), 0, content.length, 0)
R.color.colorHint
} else {
R.color.colorText
})
holder.content.setText(content, TextView.BufferType.SPANNABLE)
holder.content.setTextColor(contentColour)
holder.created.text = QwarkUtil.getCreatedString(holder.created.context, currentItem.time)
holder.category.text = if (currentItem.category.isEmpty()) "---" else currentItem.category
holder.category.visibility = if (currentItem.category.isEmpty()) View.GONE else View.VISIBLE
holder.currentNote = currentItem
}
override fun getItemCount(): Int = notes.size
fun setNotes(notes: List<Note>) {
this.notes = notes
notifyDataSetChanged()
}
internal interface OnNoteClickListener {
fun onNoteClick(note: Note)
fun onNoteLongClick(note: Note)
fun onCategoryChipClick(category: String)
}
}
@@ -0,0 +1,76 @@
package com.denizd.qwark.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.denizd.qwark.R
import com.denizd.qwark.model.Participation
import com.denizd.qwark.util.QwarkUtil
import java.util.*
class ParticipationAdapter(private var participations: List<Participation>, private val onClickListener: ParticipationClickListener) : RecyclerView.Adapter<ParticipationAdapter.ParticipationViewHolder>() {
class ParticipationViewHolder(view: View, private val onClickListener: ParticipationClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener {
val imageView: ImageView = view.findViewById(R.id.image_view)
val time: TextView = view.findViewById(R.id.time)
val participationText: TextView = view.findViewById(R.id.participation)
lateinit var participation: Participation
init {
view.setOnClickListener(this)
view.setOnLongClickListener(this)
}
override fun onClick(v: View?) {
onClickListener.onParticipationClick(participation)
}
override fun onLongClick(v: View?): Boolean {
onClickListener.onParticipationLongClick(participation)
return true
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParticipationViewHolder =
ParticipationViewHolder(LayoutInflater.from(parent.context).inflate(
R.layout.participation_card,
parent,
false
), onClickListener)
override fun onBindViewHolder(holder: ParticipationViewHolder, position: Int) {
val currentItem = participations[position]
holder.imageView.setImageDrawable(holder.imageView.context.getDrawable(
with (currentItem) {
when {
timesHandRaised > timesSpoken -> R.drawable.hand
timesHandRaised < timesSpoken -> R.drawable.speech
else -> R.drawable.cross
}
}
))
holder.time.text = QwarkUtil.getSimpleDateTimeFormat().format(Date(currentItem.time))
holder.participationText.text = holder.participationText.context.getString(
R.string.participation_placeholder,
currentItem.timesHandRaised.toString(),
currentItem.timesSpoken.toString()
)
holder.participation = currentItem
}
override fun getItemCount(): Int = participations.size
fun setParticipations(participations: List<Participation>) {
this.participations = participations
notifyDataSetChanged()
}
interface ParticipationClickListener {
fun onParticipationClick(participation: Participation)
fun onParticipationLongClick(participation: Participation)
}
}
@@ -0,0 +1,57 @@
package com.denizd.qwark.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.denizd.qwark.R
import com.denizd.qwark.model.SchoolDay
class SchoolDayAdapter(private var days: List<SchoolDay>, private val onClickListener: DayClickListener) : RecyclerView.Adapter<SchoolDayAdapter.DayViewHolder>() {
class DayViewHolder(view: View, private val clickListener: DayClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener {
val title: TextView = view.findViewById(R.id.title)
lateinit var currentDay: SchoolDay
init {
view.setOnClickListener(this)
}
override fun onClick(v: View?) {
clickListener.onDayClick(currentDay)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DayViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.year_card, parent, false)
return DayViewHolder(v, onClickListener)
}
override fun onBindViewHolder(holder: DayViewHolder, position: Int) {
val currentItem = days[position]
holder.title.text = holder.title.context.resources.getStringArray(R.array.days)[dayMap[currentItem.day] ?: 0]
holder.currentDay = currentItem
}
override fun getItemCount(): Int = days.size
fun setDays(days: List<SchoolDay>) {
this.days = days
notifyDataSetChanged()
}
private val dayMap: Map<String, Int> = mapOf(
"mon" to 0,
"tue" to 1,
"wed" to 2,
"thu" to 3,
"fri" to 4
)
interface DayClickListener {
fun onDayClick(day: SchoolDay)
}
}
@@ -0,0 +1,49 @@
package com.denizd.qwark.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.denizd.qwark.R
import com.denizd.qwark.model.SchoolYear
internal class YearAdapter(private var schoolYears: List<SchoolYear>, private val onClickListener: YearClickListener) : RecyclerView.Adapter<YearAdapter.YearViewHolder>() {
internal class YearViewHolder(view: View, private val clickListener: YearClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener {
internal val title: TextView = view.findViewById(R.id.title)
internal lateinit var currentSchoolYear: SchoolYear
init {
view.setOnClickListener(this)
}
override fun onClick(v: View?) {
clickListener.onYearClick(currentSchoolYear)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): YearViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.year_card, parent, false)
return YearViewHolder(v, onClickListener)
}
override fun onBindViewHolder(holder: YearViewHolder, position: Int) {
val currentItem = schoolYears[position]
holder.title.text = currentItem.year
holder.currentSchoolYear = currentItem
}
override fun getItemCount(): Int = schoolYears.size
fun setYears(schoolYears: List<SchoolYear>) {
this.schoolYears = schoolYears
notifyDataSetChanged()
}
internal interface YearClickListener {
fun onYearClick(schoolYear: SchoolYear)
}
}
@@ -0,0 +1,24 @@
package com.denizd.qwark.database
import android.app.Application
import androidx.lifecycle.LiveData
import com.denizd.qwark.model.Course
import com.denizd.qwark.model.CourseExam
import com.denizd.qwark.model.HistoricalAvg
internal class CourseRepository(app: Application) {
private val dao: QwarkDao = QwarkDatabase.getInstance(app).dao()
// val allCourses: LiveData<List<Course>> = dao.allCourses
// val allHistoricalAverages: LiveData<List<HistoricalAvg>> = dao.allHistoricalAverages
fun insert(course: Course) { dao.insert(course) }
fun update(course: Course) { dao.update(course) }
fun delete(course: Course) { dao.delete(course) }
fun insert(avg: HistoricalAvg) { dao.insert(avg) }
fun getCoursesByYearId(yearId: Int): LiveData<List<CourseExam>> = dao.getCoursesByYearId(yearId)
fun getAveragesForCourses(courseId: Int): List<HistoricalAvg> = dao.getAveragesForCourses(courseId)
}
@@ -0,0 +1,47 @@
package com.denizd.qwark.database
import android.app.Application
import androidx.lifecycle.LiveData
import com.denizd.qwark.util.QwarkPreferences
import com.denizd.qwark.model.FinalGrade
import java.math.BigDecimal
import java.math.RoundingMode
class FinalsRepository(app: Application) {
private val dao: QwarkDao = QwarkDatabase.getInstance(app).dao()
private val prefs: QwarkPreferences = QwarkPreferences.getPrefs(app)
fun getScoreProfileName(): String = prefs.getScoreProfileName()
fun getFinalGrades(finalScoreId: Int): LiveData<List<FinalGrade>> = dao.getFinalGrades(finalScoreId)
fun getFinalScoreId() = prefs.getScoreProfileId()
fun getCourseSortType() = prefs.getCourseSortType()
fun getGradeType() = prefs.getGradeType()
fun updateFinalGradeAverage(courseId: Int, finalGradeId: Int) {
val course = dao.getCourse(courseId)[0]
dao.updateFinalGradeAverage(
course.name,
BigDecimal(course.average).setScale(0, RoundingMode.HALF_UP).toInt(),
finalGradeId
)
}
fun updateLinkedGrades() {
val finalGrades = dao.getFinalGradesAsList(getFinalScoreId())
for (finalGrade in finalGrades) {
if (finalGrade.courseId != -1) {
updateFinalGradeAverage(finalGrade.courseId, finalGrade.finalGradeId)
}
}
}
fun getSchoolYearName(courseId: Int) = dao.getSchoolYearName(courseId)
fun getAllYears() = dao.getAllYears()
fun getAllCourses() = dao.getAllCourseExams()
fun getCourse(courseId: Int) = dao.getCourse(courseId)[0]
fun update(finalGrade: FinalGrade) { dao.update(finalGrade) }
}
@@ -0,0 +1,30 @@
package com.denizd.qwark.database
import android.app.Application
import androidx.lifecycle.LiveData
import com.denizd.qwark.model.Grade
import com.denizd.qwark.model.HistoricalAvg
import com.denizd.qwark.util.*
import com.denizd.qwark.util.QwarkPreferences
class GradeRepository(app: Application) {
private val dao: QwarkDao = QwarkDatabase.getInstance(app).dao()
private val prefs: QwarkPreferences = QwarkPreferences.getPrefs(app)
fun getGradeType(): Int = prefs.getInt(PreferenceKey.GRADE_TYPE)
// TODO this might crash
fun getCourse(courseId: Int) = dao.getCourse(courseId)[0]
fun insert(grade: Grade) { dao.insert(grade) }
fun update(grade: Grade) { dao.update(grade) }
fun delete(gradeId: Int) { dao.deleteGrade(gradeId) }
fun insert(avg: HistoricalAvg) { dao.insert(avg) }
fun updateAverage(average: String, gradeCount: Int, courseId: Int) {
dao.updateAverage(average, gradeCount, courseId)
}
fun getGradesByForeignKey(courseId: Int): LiveData<List<Grade>> = dao.getGradesByForeignKey(courseId)
}
@@ -0,0 +1,19 @@
package com.denizd.qwark.database
import android.app.Application
import androidx.lifecycle.LiveData
import com.denizd.qwark.model.Note
class NoteRepository(app: Application) {
private val dao: QwarkDao = QwarkDatabase.getInstance(app).dao()
val allNotes: LiveData<List<Note>> = dao.allNotes
fun getNotesForCategory(category: String): LiveData<List<Note>> = dao.notesForCategory(category)
fun getAllCategories(): List<String> = dao.getAllCategories()
fun insert(note: Note) { dao.insert(note) }
fun update(note: Note) { dao.update(note) }
fun updateDismissed(newDismissedStatus: Boolean, noteId: Int) { dao.updateDismissed(newDismissedStatus, noteId) }
fun delete(noteId: Int) { dao.deleteNote(noteId) }
}
@@ -0,0 +1,72 @@
package com.denizd.qwark.database
import android.app.Application
import android.util.Log
import androidx.lifecycle.LiveData
import com.denizd.qwark.util.QwarkPreferences
import com.denizd.qwark.model.Course
import com.denizd.qwark.model.CourseExam
import com.denizd.qwark.model.Participation
import com.denizd.qwark.model.SchoolDay
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.util.round
import com.denizd.qwark.util.roundToInt
import java.util.*
class ParticipationRepository(app: Application) {
private val days: Array<String> = arrayOf("mon", "tue", "wed", "thu", "fri")
private val dao: QwarkDao = QwarkDatabase.getInstance(app).dao()
private val prefs: QwarkPreferences = QwarkPreferences.getPrefs(app.applicationContext)
// monday: 1, increment by 1 for each day
val dayId: Int = when (prefs.getParticipationDay()) {
0 -> if (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 1 in 1..5)
Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 1 else 1
else -> prefs.getParticipationDay()
}
val allCoursesForToday: LiveData<List<Course>> = dao.getCoursesByDayType(getDayType(), prefs.getSchoolYear())
fun initParticipationTables() {
for (day in days) {
dao.insert(SchoolDay(day, 0, prefs.getSchoolYear()))
}
}
fun getParticipationCount(participations: List<Participation>) = if (prefs.getParticipationDisplay() == 0) {
arrayOf(
(participations.map { it.timesHandRaised }.sum().toDouble() / participations.size.toDouble()).round(),
(participations.map { it.timesSpoken }.sum().toDouble() / participations.size.toDouble()).round()
)
} else {
arrayOf(
(participations.map { it.timesHandRaised }.sum()),
(participations.map { it.timesSpoken }.sum())
)
}
fun getDayType(): String = if (dayId in 1..5) days[dayId - 1] else days[1]
fun getDays(): List<SchoolDay> = dao.getDays(prefs.getSchoolYear())
fun getCourses(): List<CourseExam> = QwarkUtil.getCoursesSorted(
dao.getCoursesByYearIdAsList(prefs.getSchoolYear()),
prefs.getCourseSortType()
)
fun link(courseId: Int) { dao.link(courseId, getDayType(), prefs.getSchoolYear()) }
fun removeLink(relationId: Int) { dao.removeLink(relationId) }
fun getAllParticipationsForCourse(courseId: Int): LiveData<List<Participation>> =
dao.getAllParticipationsForCourse(courseId)
fun insert(timesHandRaised: Int, timesSpoken: Int, time: Long, courseId: Int) {
dao.insert(
Participation(
timesHandRaised,
timesSpoken,
time,
0,
courseId,
dayId
)
)
}
fun delete(participation: Participation) { dao.delete(participation) }
fun updateParticipation(participation: String, courseId: Int) { dao.updateParticipation(participation, courseId) }
fun getSchoolYearName() = prefs.getSchoolYearName()
}
@@ -0,0 +1,34 @@
package com.denizd.qwark.database
import android.content.Context
import com.denizd.qwark.model.Note
import java.io.OutputStreamWriter
import java.net.Socket
class QwarkClient(applicationContext: Context) {
private val dao: QwarkDao = QwarkDatabase.getInstance(applicationContext).dao()
fun send(finish: () -> Unit) {
try {
Socket("172.20.10.7", 22222).use { socket ->
OutputStreamWriter(socket.getOutputStream(), "utf-8").use { writer ->
writer.write(dao.getNotesAsList().prepare())
writer.flush()
}
}
finish()
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun List<Note>.prepare(): String {
var result = ""
forEach { note ->
result += "$${note.content}^${note.dismissed}^${note.time}^${note.category}^${note.noteId}|"
}
result += '\\'
return result
}
}
@@ -0,0 +1,185 @@
package com.denizd.qwark.database
import androidx.lifecycle.LiveData
import androidx.room.*
import com.denizd.qwark.model.*
import com.denizd.qwark.model.Course
import com.denizd.qwark.model.Grade
import com.denizd.qwark.model.HistoricalAvg
import com.denizd.qwark.model.Note
import com.denizd.qwark.model.SchoolYear
import com.denizd.qwark.util.QwarkUtil
@Dao
interface QwarkDao {
// courses
@get:Query("SELECT *, -1 AS exam_time FROM course ORDER BY time")
val allCourses: LiveData<List<CourseExam>>
@Query("SELECT course.*, (SELECT MIN(exam_time) FROM grade WHERE grade.course_id = course.course_id AND exam_time >= :time) AS exam_time FROM course GROUP BY course.course_id ORDER BY time")
fun getAllCourseExams(time: Long = QwarkUtil.timeAtMidnight): List<CourseExam>
@Query("SELECT *, -1 AS exam_time FROM course WHERE course_id != :u ORDER BY time")
fun getAllCourses(u: Int = -1): List<CourseExam> // the u variable is an unnecessary workaround to return a list instead of LiveData
@Query("SELECT course.*, (SELECT MIN(exam_time) FROM grade WHERE grade.course_id = course.course_id AND exam_time >= :time) AS exam_time FROM course WHERE year_id = :yearId GROUP BY course.course_id ORDER BY time")
fun getCoursesByYearId(yearId: Int, time: Long = QwarkUtil.timeAtMidnight): LiveData<List<CourseExam>>
@Query("SELECT *, -1 AS exam_time FROM course WHERE year_id = :yearId ORDER BY time")
fun getCoursesByYearIdAsList(yearId: Int): List<CourseExam>
@Query("SELECT *, -1 AS exam_time FROM course WHERE course_id = :courseId")
fun getCourse(courseId: Int): List<CourseExam>
@Insert
fun insert(course: Course)
@Delete
fun delete(course: Course)
@Update
fun update(course: Course)
@Query("UPDATE course SET average = :average, grade_count = :gradeCount WHERE course_id = :courseId")
fun updateAverage(average: String, gradeCount: Int, courseId: Int)
@Query("UPDATE course SET participation = :participation WHERE course_id = :courseId")
fun updateParticipation(participation: String, courseId: Int)
// grades
@get:Query("SELECT * FROM grade ORDER BY time")
val allGrades: LiveData<List<Grade>>
@Query("SELECT * FROM grade WHERE course_id = :courseId ORDER BY time")
fun getGradesByForeignKey(courseId: Int): LiveData<List<Grade>>
@Insert
fun insert(grade: Grade)
@Update
fun update(grade: Grade)
@Query("DELETE FROM grade WHERE grade_id = :gradeId")
fun deleteGrade(gradeId: Int)
// notes
@get:Query("SELECT * FROM note ORDER BY time DESC")
val allNotes: LiveData<List<Note>>
@Query("SELECT * FROM note ORDER BY time DESC")
fun getNotesAsList(): List<Note>
@Insert
fun insert(note: Note)
@Query("UPDATE note SET dismissed = :newDismissedStatus WHERE note_id = :noteId")
fun updateDismissed(newDismissedStatus: Boolean, noteId: Int)
@Query("DELETE FROM note WHERE note_id = :noteId")
fun deleteNote(noteId: Int)
@Query("SELECT * FROM note WHERE category = :category ORDER BY time DESC")
fun notesForCategory(category: String): LiveData<List<Note>>
@Update
fun update(note: Note)
@Query("SELECT DISTINCT category FROM note")
fun getAllCategories(): List<String>
// historical averages
@get:Query("SELECT * FROM historical_avg ORDER BY course_id DESC")
val allHistoricalAverages: LiveData<List<HistoricalAvg>>
@Insert
fun insert(historicalAvg: HistoricalAvg)
@Query("SELECT * FROM historical_avg WHERE course_id = :courseId ORDER BY time DESC")
fun getAveragesForCourses(courseId: Int): List<HistoricalAvg>
// years
@get:Query("SELECT * FROM school_year ORDER BY year_id")
val allYears: LiveData<List<SchoolYear>>
@Query("SELECT * FROM school_year WHERE year_id != :u ORDER BY year_id")
fun getAllYears(u: Int = -1): List<SchoolYear>
@Insert
fun insert(schoolYear: SchoolYear)
@Update
fun update(schoolYear: SchoolYear)
@Query("DELETE FROM school_year WHERE year_id = :yearId")
fun deleteSchoolYear(yearId: Int)
// final scores
@get:Query("SELECT * FROM score_profile ORDER BY score_profile_id")
val allScoreProfiles: LiveData<List<ScoreProfile>>
@Query("SELECT * FROM score_profile WHERE score_profile_id = :scoreProfileId")
fun getScoreProfileNames(scoreProfileId: Int): List<ScoreProfile>
@Insert
fun insert(scoreProfile: ScoreProfile)
@Update
fun update(scoreProfile: ScoreProfile)
@Query("DELETE FROM score_profile WHERE score_profile_id = :scoreProfileId")
fun deleteScoreProfile(scoreProfileId: Int)
@Query("SELECT school_year.year FROM school_year, course WHERE course.year_id = school_year.year_id AND course.course_id = :courseId")
fun getSchoolYearName(courseId: Int): String
// final grades
@Query("SELECT * FROM final_grade WHERE score_profile_id = :scoreProfileId ORDER BY final_grade_id")
fun getFinalGrades(scoreProfileId: Int): LiveData<List<FinalGrade>>
@Query("SELECT * FROM final_grade WHERE score_profile_id = :scoreProfileId ORDER BY final_grade_id")
fun getFinalGradesAsList(scoreProfileId: Int): List<FinalGrade>
@Query("UPDATE final_grade SET name = :name, grade = :grade WHERE final_grade_id = :finalGradeId")
fun updateFinalGradeAverage(name: String, grade: Int, finalGradeId: Int)
@Update
fun update(finalGrade: FinalGrade)
@Insert
fun insertAll(finalGrades: Array<FinalGrade>)
// school days
@Insert
fun insert(schoolDay: SchoolDay)
// participation stuff
@Query("SELECT course.*, cs.relation_id AS oral_weighting FROM course, course_schoolday_relation AS cs, school_day WHERE course.course_id = cs.course_id AND school_day.day_id = cs.day_id AND school_day.day = :dayType AND school_day.year_id = :yearId")
fun getCoursesByDayType(dayType: String, yearId: Int): LiveData<List<Course>>
@Query("SELECT * FROM school_day WHERE year_id = :yearId")
fun getDays(yearId: Int): List<SchoolDay>
@Query("INSERT INTO course_schoolday_relation VALUES (:courseId, (SELECT day_id FROM school_day WHERE day = :dayType AND year_id = :yearId), NULL)")
fun link(courseId: Int, dayType: String, yearId: Int)
@Query("DELETE FROM course_schoolday_relation WHERE relation_id = :relationId")
fun removeLink(relationId: Int)
@Query("SELECT * FROM participation WHERE course_id = :courseId")
fun getAllParticipationsForCourse(courseId: Int): LiveData<List<Participation>>
@Insert
fun insert(participation: Participation)
@Delete
fun delete(participation: Participation)
}
@@ -0,0 +1,106 @@
package com.denizd.qwark.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.denizd.qwark.model.*
import com.denizd.qwark.model.Course
import com.denizd.qwark.model.Grade
import com.denizd.qwark.model.HistoricalAvg
import com.denizd.qwark.model.Note
import com.denizd.qwark.model.SchoolYear
@Database(entities = [Course::class, Grade::class, Note::class, HistoricalAvg::class, SchoolYear::class, FinalGrade::class, ScoreProfile::class, Participation::class, SchoolDay::class, CourseDayRelation::class], version = 9, exportSchema = false)
internal abstract class QwarkDatabase : RoomDatabase() {
abstract fun dao(): QwarkDao
companion object {
// my first successful migration! lol
private val migrationAddYearIdToDay = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE school_day")
database.execSQL("CREATE TABLE school_day ( day TEXT NOT NULL, day_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, year_id INT NOT NULL, FOREIGN KEY (year_id) REFERENCES school_year (year_id) ON DELETE CASCADE )")
}
}
private val migrationAddExamTimeToGrade = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE grades ADD COLUMN exam_time INTEGER NOT NULL")
}
}
private val migrationAddGradeCount = object : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE course ADD COLUMN grade_count INTEGER NOT NULL DEFAULT 0")
}
}
private val migrationAddPrimaryKeyToRelation = object : Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
database.apply {
execSQL("DROP TABLE course_schoolday_relation")
execSQL("""
CREATE TABLE course_schoolday_relation (
course_id INTEGER NOT NULL,
day_id INTEGER NOT NULL,
relation_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
FOREIGN KEY (course_id) REFERENCES course (course_id) ON DELETE CASCADE,
FOREIGN KEY (day_id) REFERENCES school_day (day_id) ON DELETE CASCADE
)
""".trimIndent())
}
}
}
private val migrationParticipationDateToTime = object : Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) {
database.apply {
execSQL("DROP TABLE participation")
execSQL("""
CREATE TABLE participation (
times_hand_raised INTEGER NOT NULL,
times_spoken INTEGER NOT NULL,
time INTEGER NOT NULL,
participation_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
course_id INTEGER NOT NULL,
day_id INTEGER NOT NULL,
FOREIGN KEY (course_id) REFERENCES course (course_id) ON DELETE CASCADE,
FOREIGN KEY (day_id) REFERENCES school_day (day_id) ON DELETE CASCADE
)
""".trimIndent())
}
}
}
private val migrationAddParticipation = object : Migration(8, 9) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE course ADD COLUMN participation TEXT NOT NULL DEFAULT '--|--'")
}
}
private var instance: QwarkDatabase? = null
fun getInstance(context: Context): QwarkDatabase {
if (instance == null) {
synchronized(QwarkDatabase::class) {
instance = Room.databaseBuilder(
context.applicationContext,
QwarkDatabase::class.java,
"qwark_database")
.addMigrations(
migrationAddGradeCount,
migrationAddPrimaryKeyToRelation,
migrationParticipationDateToTime,
migrationAddParticipation
).build()
}
}
return instance!!
}
}
}
@@ -0,0 +1,125 @@
package com.denizd.qwark.database
import android.app.Application
import androidx.lifecycle.LiveData
import com.denizd.qwark.util.PreferenceKey
import com.denizd.qwark.util.QwarkPreferences
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.model.Course
import com.denizd.qwark.model.FinalGrade
import com.denizd.qwark.model.ScoreProfile
import com.denizd.qwark.model.SchoolYear
class SettingsRepository(app: Application) {
private val dao: QwarkDao = QwarkDatabase.getInstance(app).dao()
private val prefs: QwarkPreferences = QwarkPreferences.getPrefs(app.applicationContext)
val allYears: LiveData<List<SchoolYear>> = dao.allYears
val allScoreProfiles: LiveData<List<ScoreProfile>> = dao.allScoreProfiles
// Preference getters
fun getAppTheme(): Int = prefs.getInt(PreferenceKey.APP_THEME, 2)
fun getShowGradeAverage(): Boolean = prefs.getBool(PreferenceKey.SHOW_GRADE_AVERAGE)
fun getGradeType(): Int = prefs.getInt(PreferenceKey.GRADE_TYPE)
fun getCurrentSchoolYear(): String = prefs.getString(PreferenceKey.SCHOOL_YEAR_NAME)
fun getCurrentSchoolYearId(): Int = prefs.getInt(PreferenceKey.SCHOOL_YEAR_ID)
private fun getScoreProfileId(): Int = prefs.getInt(PreferenceKey.SCORE_PROFILE_ID, 0)
fun getScoreProfileName(): String = prefs.getString(PreferenceKey.SCORE_PROFILE_NAME)
fun getParticipationDisplay(): Int = prefs.getParticipationDisplay()
fun getParticipationDay(): Int = prefs.getParticipationDay()
private fun getMaxFinalScoreId(increment: Boolean): Int {
if (increment) {
prefs.putInt(PreferenceKey.MAX_SCORE_PROFILE_ID, getMaxFinalScoreId(false) + 1)
.putInt(PreferenceKey.SCORE_PROFILE_ID, getMaxFinalScoreId(false) + 1)
}
return prefs.getInt(PreferenceKey.MAX_SCORE_PROFILE_ID)
}
// Preference setters
fun setAppTheme(value: Int) { prefs.putInt(PreferenceKey.APP_THEME, value) }
fun setShowGradeAverage(value: Boolean) { prefs.putBool(PreferenceKey.SHOW_GRADE_AVERAGE, value) }
fun setGradeType(value: Int) { prefs.putInt(PreferenceKey.GRADE_TYPE, value) }
fun setParticipationDisplay(value: Int) { prefs.setParticipationDisplay(value) }
fun setParticipationDay(value: Int) { prefs.setParticipationDay(value) }
fun setCurrentScoreProfile(scoreProfile: ScoreProfile) {
prefs.putInt(PreferenceKey.SCORE_PROFILE_ID, scoreProfile.scoreProfileId)
.putString(PreferenceKey.SCORE_PROFILE_NAME, scoreProfile.name)
}
fun setCurrentSchoolYear(schoolYear: SchoolYear) {
prefs.putInt(PreferenceKey.SCHOOL_YEAR_ID, schoolYear.yearId)
.putString(PreferenceKey.SCHOOL_YEAR_NAME, schoolYear.year)
}
// Database functions
fun insert(schoolYear: SchoolYear) { dao.insert(schoolYear) }
fun insert(scoreProfile: ScoreProfile) { dao.insert(scoreProfile) }
fun initNewFinalScore(name: String) {
val finalScoreId = getMaxFinalScoreId(true)
dao.insert(ScoreProfile(name, finalScoreId))
val values = ArrayList<FinalGrade>()
for (i in 0 until 24) {
values.add(FinalGrade("", "-1", "basic", "x 1", -1, 0, finalScoreId))
}
for (i in 0 until 3) {
values.add(FinalGrade("", "-1", "adv", "x 2", -1, 0, finalScoreId))
}
values.add(FinalGrade("", "-1", "adv", "x 1", -1, 0, finalScoreId))
for (i in 0 until 3) {
values.add(FinalGrade("", "-1", "adv", "x 2", -1, 0, finalScoreId))
}
values.add(FinalGrade("", "-1", "adv", "x 1", -1, 0, finalScoreId))
for (i in 1..5) {
values.add(FinalGrade("", "-1", "exam", "P$i x ${if (i == 5) "2" else "5"}", -1, 0, finalScoreId))
}
dao.insertAll(values.toTypedArray())
}
fun updateSchoolYear(name: String) {
dao.update(SchoolYear(name, getCurrentSchoolYearId()))
setCurrentSchoolYear(SchoolYear(name, getCurrentSchoolYearId()))
}
fun updateScoreProfile(name: String) {
dao.update(ScoreProfile(name, getScoreProfileId()))
setCurrentScoreProfile(ScoreProfile(name, getScoreProfileId()))
}
fun deleteSchoolYear() {
dao.deleteSchoolYear(getCurrentSchoolYearId())
setCurrentSchoolYear(SchoolYear("", 0))
}
fun deleteScoreProfile() {
dao.deleteScoreProfile(getScoreProfileId())
setCurrentScoreProfile(ScoreProfile("", 0))
}
fun copyCourses(yearId: Int) {
val currentSchoolYearId = getCurrentSchoolYearId()
val courses = dao.getCoursesByYearIdAsList(yearId)
for (course in courses) {
dao.insert(
Course(
name = course.name,
advanced = course.advanced,
icon = course.icon,
colour = course.colour,
average = if (getGradeType() == QwarkUtil.GRADE_PRECISE) "0.00" else "0",
oralWeighting = course.oralWeighting,
gradeCount = course.gradeCount,
time = System.currentTimeMillis(),
courseId = 0,
yearId = currentSchoolYearId
)
)
}
}
}
@@ -0,0 +1,188 @@
package com.denizd.qwark.fragment
import android.content.res.Configuration
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.denizd.qwark.R
import com.denizd.qwark.adapter.CourseAdapter
import com.denizd.qwark.sheet.ConfirmDeletionBottomSheet
import com.denizd.qwark.sheet.CourseCreateBottomSheet
import com.denizd.qwark.sheet.CourseOptionsBottomSheet
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.databinding.*
import com.denizd.qwark.sheet.HistoricalAverageBottomSheet
import com.denizd.qwark.model.Course
import com.denizd.qwark.model.CourseExam
import com.denizd.qwark.util.calculateTotalAverage
import com.denizd.qwark.viewmodel.CourseViewModel
import com.google.android.material.snackbar.Snackbar
import java.lang.NumberFormatException
internal class CourseFragment : QwarkFragment(), CourseAdapter.CourseClickListener {
private var _binding: PaddedRecyclerViewBinding? = null
private val binding: PaddedRecyclerViewBinding get() = _binding!!
private lateinit var viewModel: CourseViewModel
private lateinit var recyclerViewAdapter: CourseAdapter
private var scrollPosition: Parcelable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[CourseViewModel::class.java]
recyclerViewAdapter = CourseAdapter(ArrayList(), this, viewModel.getGradeType())
viewModel.allCourses.observe(this, Observer { courses ->
val c = QwarkUtil.getCoursesSorted(courses, viewModel.getCourseSortType())
recyclerViewAdapter.setCourses(c)
if (scrollPosition == null) {
binding.recyclerView.scrollToPosition(0)
binding.recyclerView.scheduleLayoutAnimation()
} else {
binding.recyclerView.layoutManager?.onRestoreInstanceState(scrollPosition)
}
superTitle = try {
courses.calculateTotalAverage(
context,
viewModel.getGradeType(),
viewModel.getSchoolYearName(),
viewModel.getShowGradeAverage()
)
} catch (e: NumberFormatException) {
getString(R.string.grade_format_error)
}
})
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
binding.recyclerView.layoutManager = StaggeredGridLayoutManager(
getGridColumnCount(newConfig),
StaggeredGridLayoutManager.VERTICAL
)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = PaddedRecyclerViewBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
appBarTitle = getString(R.string.your_courses)
binding.recyclerView.apply {
adapter = recyclerViewAdapter
layoutManager = StaggeredGridLayoutManager(
getGridColumnCount(resources.configuration),
StaggeredGridLayoutManager.VERTICAL
)
addFabScrollListener()
}
fab.apply {
show()
text = getString(R.string.add_course)
setOnClickListener {
if (viewModel.getSchoolYearName().isBlank()) {
presentErrorSnackBar(getString(R.string.no_school_year_selected))
} else {
createCourseDialog()
}
}
}
}
override fun onResume() {
super.onResume()
binding.recyclerView.apply {
applyPadding(verticalPadding = 4)
/**
* On first app launch, setting the padding will not scroll the recycler view items
* down. This is a workaround to scroll the recycler view down programmatically.
*/
// TODO this actually doesn't do anything
// if (!QwarkUtil.hasAppLaunched) {
// scrollToPosition(0)
// QwarkUtil.hasAppLaunched = true
// }
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onCourseClick(course: CourseExam) {
appBar.setExpanded(true)
val f = GradeFragment()
f.arguments = Bundle().apply {
putString("schoolYear", viewModel.getSchoolYearName())
putInt("courseId", course.courseId)
}
(context as FragmentActivity).supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, f)
.addToBackStack("GradeFragment")
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
}
override fun onCourseLongClick(course: CourseExam) {
createOptionsDialog(course)
}
private fun createOptionsDialog(course: CourseExam) {
openBottomSheet(CourseOptionsBottomSheet().also { sheet ->
QwarkUtil.putCourseInBundle(sheet, course)
})
}
private fun presentErrorSnackBar(message: String) {
Snackbar
.make(snackBarContainer, message, Snackbar.LENGTH_LONG)
.setBackgroundTint(context.getColor(R.color.colorWarning))
.show()
}
// functions for use by bottom sheets
fun insert(course: Course) { viewModel.insert(course) }
fun update(course: Course) { viewModel.update(course) }
fun getSchoolYear() = viewModel.getSchoolYear()
fun getGradeType() = viewModel.getGradeType()
fun getAveragesForCourse(courseId: Int) = viewModel.getAveragesForCourses(courseId)
fun createHistoryDialog(courseId: Int, course: String) {
openBottomSheet(HistoricalAverageBottomSheet().also { sheet ->
sheet.arguments = Bundle(2).apply {
putString("courseName", course)
putInt("courseId", courseId)
}
})
}
fun createCourseDialog(course: CourseExam? = null) {
openBottomSheet(CourseCreateBottomSheet().also { sheet ->
if (course != null) QwarkUtil.putCourseInBundle(sheet, course)
})
}
fun deleteCourse(course: Course) {
openBottomSheet(
ConfirmDeletionBottomSheet(
getString(R.string.delete_course_and_data_confirmation, course.name)
) {
viewModel.delete(course)
}
)
}
}
@@ -0,0 +1,289 @@
package com.denizd.qwark.fragment
import android.content.res.Configuration
import android.os.Bundle
import android.os.Parcelable
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.denizd.qwark.R
import com.denizd.qwark.adapter.FinalGradeAdapter
import com.denizd.qwark.sheet.ConfirmDeletionBottomSheet
import com.denizd.qwark.sheet.FinalsCreateBottomSheet
import com.denizd.qwark.sheet.FinalsOptionsBottomSheet
import com.denizd.qwark.databinding.FinalsFragmentBinding
import com.denizd.qwark.sheet.FinalsLinkBottomSheet
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.model.FinalGrade
import com.denizd.qwark.viewmodel.FinalsViewModel
import com.google.android.material.snackbar.Snackbar
internal class FinalsFragment : QwarkFragment(), FinalGradeAdapter.FinalGradeClickListener {
private lateinit var viewModel: FinalsViewModel
private var _binding: FinalsFragmentBinding? = null
private val binding: FinalsFragmentBinding get() = _binding!!
private var pointsText = ""
private var gradeText = ""
private val basicCoursesAdapter = FinalGradeAdapter(ArrayList(), this)
private val advancedCoursesAdapter = FinalGradeAdapter(ArrayList(), this)
private val examGradesAdapter = FinalGradeAdapter(ArrayList(), this)
private var basicScrollPosition: Parcelable? = null
private var advancedScrollPosition: Parcelable? = null
private var examScrollPosition: Parcelable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[FinalsViewModel::class.java]
viewModel.updateLinkedGrades {
viewModel.allFinalGrades?.observe(this, Observer { finalGrades ->
// createOverview(finalGrades, view.rootView) // TODO export to bitmap
if (basicScrollPosition == null) {
binding.basicCoursesRecyclerview.scheduleLayoutAnimation()
binding.advancedCoursesRecyclerview.scheduleLayoutAnimation()
binding.finalExamsRecyclerview.scheduleLayoutAnimation()
}
saveScrollPositions()
val basicGrades = finalGrades.filter { f -> f.type == "basic" }
val advancedGrades = finalGrades.filter { f -> f.type == "adv" }
val examGrades = finalGrades.filter { f -> f.type == "exam" }
basicCoursesAdapter.setFinalGrades(basicGrades)
advancedCoursesAdapter.setFinalGrades(advancedGrades)
examGradesAdapter.setFinalGrades(examGrades)
val finalScore = viewModel.getFinalScore(basicGrades, advancedGrades, examGrades)
if (finalScore == -1) {
binding.pointsTextview.text = "---"
binding.gradeTextview.text = "---"
} else {
pointsText = "$finalScore"
gradeText = getString(R.string.approximate_placeholder, viewModel.getFinalGrade(finalScore).toString())
setTextForGradeTextViews()
}
restoreScrollPositions()
})
}
}
private fun setTextForGradeTextViews() {
binding.pointsTextview.text = pointsText
binding.gradeTextview.text = gradeText
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FinalsFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
appBarTitle = getString(R.string.your_final_score)
superTitle = if (viewModel.getScoreProfileName() == "") {
getString(R.string.no_score_profile_selected)
} else {
getString(R.string.score_profile_template, viewModel.getScoreProfileName())
}
fab.hide()
val layoutManagerBasic = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
val layoutManagerAdvanced = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
val layoutManagerExams = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
binding.basicCoursesRecyclerview.layoutManager = layoutManagerBasic
binding.advancedCoursesRecyclerview.layoutManager = layoutManagerAdvanced
binding.finalExamsRecyclerview.layoutManager = layoutManagerExams
binding.basicCoursesRecyclerview.adapter = basicCoursesAdapter
binding.advancedCoursesRecyclerview.adapter = advancedCoursesAdapter
binding.finalExamsRecyclerview.adapter = examGradesAdapter
}
override fun onResume() {
super.onResume()
binding.paddingView.apply {
applyPadding()
}
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
binding.textLayout?.applyPadding()
}
setTextForGradeTextViews()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onFinalGradeClick(finalGrade: FinalGrade) {
openBottomSheet(FinalsOptionsBottomSheet().also { sheet ->
QwarkUtil.putFinalGradeInBundle(sheet, finalGrade)
})
}
internal fun createEditGradeSheet(finalGrade: FinalGrade) {
openBottomSheet(FinalsCreateBottomSheet().also { sheet ->
QwarkUtil.putFinalGradeInBundle(sheet, finalGrade)
})
}
internal fun createLinkGradeSheet(finalGrade: FinalGrade) {
openBottomSheet(FinalsLinkBottomSheet().also { sheet ->
QwarkUtil.putFinalGradeInBundle(sheet, finalGrade)
})
}
internal fun getAllYears() = viewModel.getAllYears()
internal fun getAllCourses() = viewModel.getAllCourses()
internal fun viewLinkedCourse(courseId: Int) {
val f = GradeFragment()
QwarkUtil.putCourseInBundle(f, viewModel.getCourse(courseId))
val d = viewModel.getSchoolYearName(courseId)
Log.d("TAG", d)
f.arguments?.putString("schoolYear", d)
activity?.supportFragmentManager?.beginTransaction()
?.replace(R.id.fragment_container, f)
?.addToBackStack("GradeFragment")
?.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
?.commit()
}
internal fun clearFinalGrade(finalGrade: FinalGrade) {
val deletionSheet = ConfirmDeletionBottomSheet(getString(R.string.clear_final_grade_desc, finalGrade.name)) {
val newFinalGrade = FinalGrade(
name = "",
grade = "-1",
type = finalGrade.type,
note = finalGrade.note,
courseId = -1,
finalGradeId = finalGrade.finalGradeId,
scoreProfileId = finalGrade.scoreProfileId
)
viewModel.update(newFinalGrade)
}
openBottomSheet(deletionSheet)
}
internal fun clearFinalGradeError() {
Snackbar
.make(snackBarContainer, getString(R.string.clear_final_grade_error), Snackbar.LENGTH_LONG)
.setBackgroundTint(context.getColor(R.color.colorWarning))
.show()
}
internal fun update(finalGrade: FinalGrade) { viewModel.update(finalGrade) }
// TODO perhaps replace with a bottom sheet
// private fun createOverview(finalGrades: List<FinalGrade>, view: View) {
// val dialogBuilder = MaterialAlertDialogBuilder(context)
// val v = View.inflate(context, R.layout.finals_overview, null)
// val l = v.findViewById<LinearLayout>(R.id.linear_layout)
//
// for (finalGrade in finalGrades) {
// val fv = View.inflate(context, R.layout.finals_overview_item, null)
// fv.findViewById<TextView>(R.id.course).text = finalGrade.name
// fv.findViewById<TextView>(R.id.grade).text = finalGrade.grade
// l.addView(fv)
// }
// l.addView(View.inflate(context, R.layout.powered_by_qwark, null))
// val dialog = dialogBuilder.setView(v).create()
// dialog.show()
// save(drawToBitmap(l, v.width, v.height))
// }
//
// private fun drawToBitmap(layout: View, width: Int, height: Int): Bitmap {
// val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
// val canvas = Canvas(bmp)
// layout.measure(
// View.MeasureSpec.makeMeasureSpec(canvas.width, View.MeasureSpec.EXACTLY),
// View.MeasureSpec.makeMeasureSpec(canvas.height, View.MeasureSpec.EXACTLY))
// layout.layout(0,0,layout.measuredWidth,layout.measuredHeight)
// layout.draw(canvas)
// return bmp
// }
//
// private fun save(bitmap: Bitmap) {
// val name = "image"
// val relativeLocation = Environment.DIRECTORY_PICTURES + File.pathSeparator + "Qwark"
//
// val contentValues = ContentValues().apply {
// put(MediaStore.Images.ImageColumns.DISPLAY_NAME, name)
// put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
//
// // without this part causes "Failed to create new MediaStore record" exception to be invoked (uri is null below)
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// put(MediaStore.Images.ImageColumns.RELATIVE_PATH, relativeLocation)
// }
// }
//
// val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
// var stream: OutputStream? = null
// var uri: Uri? = null
// val contentResolver = context.contentResolver
//
// try {
// uri = contentResolver.insert(contentUri, contentValues)
// if (uri == null) {
// throw IOException("Failed to create new MediaStore record.")
// }
//
// stream = contentResolver.openOutputStream(uri)
//
// if (stream == null) {
// throw IOException("Failed to get output stream.")
// }
// if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
// throw IOException("Failed to save bitmap.")
// }
//
//// Snackbar.make(mCoordinator, R.string.image_saved_success, Snackbar.LENGTH_INDEFINITE).setAction("Open") {
//// val intent = Intent()
//// intent.type = "image/*"
//// intent.action = Intent.ACTION_VIEW
//// intent.data = contentUri
//// startActivity(Intent.createChooser(intent, "Select Gallery App"))
//// }.show()
//
// } catch (e: IOException) {
// if (uri != null) {
// contentResolver.delete(uri, null, null)
// }
// throw IOException(e)
//
// } finally {
// stream?.close()
// }
// }
private fun saveScrollPositions(reset: Boolean = false) {
if (reset) {
basicScrollPosition = null
advancedScrollPosition = null
examScrollPosition = null
} else {
basicScrollPosition = binding.basicCoursesRecyclerview.layoutManager?.onSaveInstanceState()
advancedScrollPosition = binding.advancedCoursesRecyclerview.layoutManager?.onSaveInstanceState()
examScrollPosition = binding.finalExamsRecyclerview.layoutManager?.onSaveInstanceState()
}
}
private fun restoreScrollPositions() {
binding.basicCoursesRecyclerview.layoutManager?.onRestoreInstanceState(basicScrollPosition)
binding.advancedCoursesRecyclerview.layoutManager?.onRestoreInstanceState(advancedScrollPosition)
binding.finalExamsRecyclerview.layoutManager?.onRestoreInstanceState(examScrollPosition)
}
internal fun getCourseSortType() = viewModel.getCourseSortType()
fun getGradeType() = viewModel.getGradeType()
}
@@ -0,0 +1,179 @@
package com.denizd.qwark.fragment
import android.os.Bundle
import android.os.Parcelable
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import com.denizd.qwark.R
import com.denizd.qwark.adapter.GradeAdapter
import com.denizd.qwark.sheet.ConfirmDeletionBottomSheet
import com.denizd.qwark.sheet.GradeCreateBottomSheet
import com.denizd.qwark.databinding.GradeFragmentBinding
import com.denizd.qwark.model.CourseExam
import com.denizd.qwark.model.Grade
import com.denizd.qwark.model.HistoricalAvg
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.util.calculateAverage
import com.denizd.qwark.util.getUserDefinedAverage
import com.denizd.qwark.viewmodel.GradeViewModel
import com.google.android.material.bottomnavigation.BottomNavigationView
class GradeFragment : QwarkFragment(), GradeAdapter.GradeClickListener {
private lateinit var currentCourse: CourseExam
private var average = ""
private var schoolYear = ""
private var openedFirstTime = false
private lateinit var weightingString: String
private lateinit var adapter: GradeAdapter
private lateinit var viewModel: GradeViewModel
private lateinit var inflater: LayoutInflater
private var _binding: GradeFragmentBinding? = null
private val binding: GradeFragmentBinding get() = _binding!!
private var scrollPosition: Parcelable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[GradeViewModel::class.java]
// currentCourse = QwarkUtil.getCourseFromBundle(arguments)
schoolYear = arguments?.getString("schoolYear") ?: ""
currentCourse = viewModel.getCourse(arguments?.getInt("courseId") ?: -1)
average = currentCourse.average
weightingString = "${currentCourse.oralWeighting} / ${100 - currentCourse.oralWeighting}"
viewModel.getGradesByForeignKey(currentCourse.courseId).observe(this, Observer { grades ->
adapter.setGrades(grades)
if (scrollPosition == null) {
binding.recyclerView.scheduleLayoutAnimation()
// } else {
// binding.recyclerView.layoutManager?.onRestoreInstanceState(scrollPosition)
}
// average = viewModel.calculateAverage(grades.filter { it.grade != -1 }, currentCourse.oralWeighting)
grades.filter { it.grade != -1 }.also { filteredGrades ->
average = filteredGrades.calculateAverage(
currentCourse.oralWeighting
)
binding.average.text = if (filteredGrades.isEmpty()) "X" else average.getUserDefinedAverage(getGradeType())
viewModel.updateAverage(average, filteredGrades.size, currentCourse.courseId)
}
if (openedFirstTime) {
insertNewHistoricalAverage()
} else {
openedFirstTime = true
}
})
adapter = GradeAdapter(ArrayList(), this, getGradeType())
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = GradeFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
superTitle = schoolYear
appBarTitle = String.format(
(if (currentCourse.advanced) getString(R.string.advanced_abbreviated_placeholder) else "%s"),
currentCourse.name
)
fab.hide()
with(view.rootView) {
findViewById<BottomNavigationView>(R.id.bottom_nav).visibility = View.GONE
findViewById<View>(R.id.view).visibility = View.GONE
}
binding.addGradeButton.setOnClickListener {
presentGradeDialog()
}
binding.average.text = currentCourse.average
binding.weighting.text = weightingString
binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = GridLayoutManager(context, 1)
binding.openParticipationFragmentButton.setOnClickListener {
openParticipationCourseFragment()
}
}
override fun onResume() {
super.onResume()
binding.paddingView.applyPadding()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
private fun insertNewHistoricalAverage() {
viewModel.insert(
HistoricalAvg(
courseId = currentCourse.courseId,
average = average,
time = System.currentTimeMillis()
)
)
}
private fun presentGradeDialog(grade: Grade? = null) {
openBottomSheet(GradeCreateBottomSheet().also { sheet ->
if (grade == null) {
sheet.arguments = Bundle(1).apply { putInt("courseId", currentCourse.courseId) }
} else {
QwarkUtil.putGradeInBundle(sheet, grade)
}
})
}
override fun onGradeClick(grade: Grade) {
presentGradeDialog(grade)
}
override fun onGradeLongClick(gradeId: Int) {
val deletionSheet = ConfirmDeletionBottomSheet(getString(R.string.confirm_grade_deletion)) {
viewModel.delete(gradeId)
}
openBottomSheet(deletionSheet)
}
private fun openParticipationCourseFragment() {
appBar.setExpanded(true)
val f = ParticipationCourseFragment()
f.arguments = Bundle().apply {
putString("course", currentCourse.name)
putInt("courseId", currentCourse.courseId)
putBoolean("advanced", currentCourse.advanced)
}
// for targetFragment to work, ParticipationFragment needs to be active
// TODO fix the dependency on targetFragment (maybe introduce a ParticipationCourseViewModel)
// f.setTargetFragment((context as FragmentActivity).supportFragmentManager.findFragmentByTag("ParticipationFragment"), 42)
(context as FragmentActivity).supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, f)
.addToBackStack(f.name)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
}
fun insert(grade: Grade) { viewModel.insert(grade) }
fun update(grade: Grade) { viewModel.update(grade) }
fun getGradeType(): Int = viewModel.getGradeType()
}
@@ -0,0 +1,168 @@
package com.denizd.qwark.fragment
import android.content.res.Configuration
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.denizd.qwark.R
import com.denizd.qwark.adapter.NoteAdapter
import com.denizd.qwark.sheet.ConfirmDeletionBottomSheet
import com.denizd.qwark.sheet.NoteCreateBottomSheet
import com.denizd.qwark.sheet.NoteDisplayBottomSheet
import com.denizd.qwark.sheet.NoteOptionsBottomSheet
import com.denizd.qwark.util.OnBackPressed
import com.denizd.qwark.databinding.*
import com.denizd.qwark.model.Note
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.viewmodel.NoteViewModel
import com.google.android.material.appbar.AppBarLayout
class NoteFragment : QwarkFragment(), NoteAdapter.OnNoteClickListener, OnBackPressed {
private lateinit var viewModel: NoteViewModel
private val adapter = NoteAdapter(ArrayList(), this)
private var scrollPosition: Parcelable? = null
private lateinit var appBarLayout: AppBarLayout
private var _binding: PaddedRecyclerViewBinding? = null
private val binding: PaddedRecyclerViewBinding get() = _binding!!
private var isInCategoryView = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[NoteViewModel::class.java]
viewModel.allNotes?.observe(this@NoteFragment, Observer<List<Note>> { notes ->
adapter.setNotes(notes)
if (scrollPosition == null) {
binding.recyclerView.scheduleLayoutAnimation()
} else {
restoreScrollPosition()
}
})
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
binding.recyclerView.layoutManager =
StaggeredGridLayoutManager(getGridColumnCount(newConfig), StaggeredGridLayoutManager.VERTICAL)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = PaddedRecyclerViewBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
appBarLayout = view.rootView.findViewById(R.id.app_bar_layout)
superTitle = ""
appBarTitle = getString(R.string.your_notes)
binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager =
StaggeredGridLayoutManager(getGridColumnCount(resources.configuration), StaggeredGridLayoutManager.VERTICAL)
fab.apply {
show()
text = getString(R.string.add_note)
setOnClickListener {
createNoteDialog()
}
}
binding.recyclerView.addFabScrollListener()
}
override fun onResume() {
super.onResume()
binding.recyclerView.applyPadding(verticalPadding = 4)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onBackPressed(): Boolean {
return if (isInCategoryView) {
sortByCategory("")
false
} else {
true
}
}
fun sortByCategory(category: String) {
appBarLayout.setExpanded(true)
if (category.isEmpty()) {
isInCategoryView = false
superTitle = ""
} else {
isInCategoryView = true
superTitle = getString(R.string.quotation_marks, category)
}
viewModel.refresh(category)
scrollPosition = null
}
override fun onNoteClick(note: Note) {
openBottomSheet(NoteDisplayBottomSheet().also { sheet ->
QwarkUtil.putNoteInBundle(sheet, note)
})
}
fun updateDismissed(note: Note) {
saveScrollPosition()
viewModel.updateDismissedStatus(!note.dismissed, note.noteId)
}
fun deleteNote(noteId: Int) {
val deletionSheet = ConfirmDeletionBottomSheet(getString(R.string.confirm_note_deletion)) {
scrollPosition = binding.recyclerView.layoutManager?.onSaveInstanceState()
viewModel.delete(noteId)
binding.recyclerView.scheduleLayoutAnimation()
}
openBottomSheet(deletionSheet)
}
override fun onNoteLongClick(note: Note) {
createOptionsSheet(note)
}
override fun onCategoryChipClick(category: String) {
sortByCategory(category)
}
private fun createOptionsSheet(note: Note) {
openBottomSheet(NoteOptionsBottomSheet().also { sheet ->
QwarkUtil.putNoteInBundle(sheet, note)
})
}
fun createNoteDialog(note: Note? = null) {
val createSheet = NoteCreateBottomSheet().also { sheet ->
if (note != null) QwarkUtil.putNoteInBundle(sheet, note)
}
openBottomSheet(createSheet)
}
fun saveScrollPosition() {
scrollPosition = binding.recyclerView.layoutManager?.onSaveInstanceState()
}
private fun restoreScrollPosition() {
binding.recyclerView.layoutManager?.onRestoreInstanceState(scrollPosition)
}
override fun getGridColumnCount(config: Configuration): Int = super.getGridColumnCount(config) + 1
fun insert(note: Note) { viewModel.insert(note) }
fun update(note: Note) { viewModel.update(note) }
fun getAllCategories(): List<String> = viewModel.getAllCategories()
}
@@ -0,0 +1,140 @@
package com.denizd.qwark.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.denizd.qwark.R
import com.denizd.qwark.adapter.ParticipationAdapter
import com.denizd.qwark.databinding.GradeFragmentBinding
import com.denizd.qwark.model.Participation
import com.denizd.qwark.sheet.ClassSessionSheet
import com.denizd.qwark.sheet.ConfirmDeletionBottomSheet
import com.denizd.qwark.viewmodel.ParticipationCourseViewModel
import com.google.android.material.bottomnavigation.BottomNavigationView
class ParticipationCourseFragment : QwarkFragment(), ParticipationAdapter.ParticipationClickListener {
private var _binding: GradeFragmentBinding? = null
private val binding: GradeFragmentBinding get() = _binding!!
private var course: String = ""
private var courseId: Int = 0
private var advanced: Boolean = false
private val recyclerViewAdapter = ParticipationAdapter(ArrayList(), this)
// private lateinit var participationFragment: ParticipationFragment
private var averageText = ""
private lateinit var viewModel: ParticipationCourseViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
course = arguments?.getString("course") ?: ""
courseId = arguments?.getInt("courseId") ?: 0
advanced = arguments?.getBoolean("advanced") ?: false
viewModel = ViewModelProvider(this)[ParticipationCourseViewModel::class.java]
// participationFragment = targetFragment as ParticipationFragment
// participationFragment.getAllParticipationsForCourse(courseId).observe(this, Observer { participations ->
viewModel.getAllParticipationsForCourse(courseId).observe(this, Observer { participations ->
recyclerViewAdapter.setParticipations(participations)
val participationArray = if (participations.isEmpty()) arrayOf("--", "--")
else viewModel.getParticipationCount(participations)
averageText = buildString {
append(participationArray[0])
append(" : ")
append(participationArray[1])
}
binding.average.text = averageText
binding.recyclerView.scheduleLayoutAnimation()
viewModel.updateParticipation("${participationArray[0]}|${participationArray[1]}", courseId)
})
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = GradeFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fab.hide()
with (view.rootView) {
findViewById<BottomNavigationView>(R.id.bottom_nav).visibility = View.GONE
findViewById<View>(R.id.view).visibility = View.GONE
}
superTitle = viewModel.getSchoolYearName()
appBarTitle = String.format(
(if (advanced) getString(R.string.advanced_abbreviated_placeholder) else "%s"),
course
)
binding.average.textSize = 64f
binding.weighting.text = getString(R.string.participation_ratio_desc)
binding.addGradeButton.apply {
text = getString(R.string.start_class_session)
setOnClickListener {
openBottomSheet(ClassSessionSheet().also { sheet ->
sheet.isCancelable = false
})
}
}
binding.recyclerView.apply {
adapter = recyclerViewAdapter
layoutManager = LinearLayoutManager(context)
}
binding.openParticipationFragmentButton.apply {
text = getString(R.string.open_grade_fragment)
setOnClickListener {
openGradeFragment()
}
}
binding.average.text = averageText
}
override fun onResume() {
super.onResume()
binding.paddingView.applyPadding()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onParticipationClick(participation: Participation) {
// do nothing
}
override fun onParticipationLongClick(participation: Participation) {
openBottomSheet(ConfirmDeletionBottomSheet(
getString(R.string.delete_participation_confirmation)
) {
viewModel.delete(participation)
})
}
private fun openGradeFragment() {
appBar.setExpanded(true)
val f = GradeFragment()
f.arguments = Bundle().apply {
putString("schoolYear", viewModel.getSchoolYearName())
putInt("courseId", courseId)
}
(context as FragmentActivity).supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, f)
.addToBackStack("GradeFragment")
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
}
fun createParticipation(timesHandRaised: Int, timesSpoken: Int, time: Long) {
viewModel.createParticipation(timesHandRaised, timesSpoken, time, courseId)
}
}
@@ -0,0 +1,139 @@
package com.denizd.qwark.fragment
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.denizd.qwark.R
import com.denizd.qwark.adapter.CourseAdapter
import com.denizd.qwark.databinding.PaddedRecyclerViewBinding
import com.denizd.qwark.model.CourseExam
import com.denizd.qwark.model.Participation
import com.denizd.qwark.sheet.ConfirmDeletionBottomSheet
import com.denizd.qwark.sheet.ParticipationLinkBottomSheet
import com.denizd.qwark.viewmodel.ParticipationViewModel
internal class ParticipationFragment : QwarkFragment(), CourseAdapter.CourseClickListener {
private var _binding: PaddedRecyclerViewBinding? = null
private val binding: PaddedRecyclerViewBinding get() = _binding!!
private lateinit var viewModel: ParticipationViewModel
private val recyclerViewAdapter = CourseAdapter(ArrayList(), this, null, true)
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
binding.recyclerView.layoutManager =
StaggeredGridLayoutManager(getGridColumnCount(newConfig), StaggeredGridLayoutManager.VERTICAL)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[ParticipationViewModel::class.java]
viewModel.allCoursesForToday.observe(this, Observer { courses ->
recyclerViewAdapter.setCourses(
// TODO
courses.map { c ->
CourseExam(
c.name,
c.advanced,
c.icon,
c.colour,
c.average,
c.oralWeighting,
c.gradeCount,
c.participation,
c.time,
c.courseId,
c.yearId,
0
)
}
)
binding.recyclerView.scheduleLayoutAnimation()
})
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = PaddedRecyclerViewBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
superTitle = getDay()
appBarTitle = getString(R.string.your_participation)
fab.apply {
show()
text = getString(R.string.link_course)
setOnClickListener {
openLinkSheet()
}
}
binding.recyclerView.apply {
adapter = recyclerViewAdapter
layoutManager = StaggeredGridLayoutManager(
getGridColumnCount(resources.configuration), StaggeredGridLayoutManager.VERTICAL
)
}
}
override fun onResume() {
super.onResume()
binding.recyclerView.applyPadding(verticalPadding = 4)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onCourseClick(course: CourseExam) {
appBar.setExpanded(true)
(context as FragmentActivity).supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, ParticipationCourseFragment().also { fragment ->
fragment.arguments = Bundle().apply {
putString("course", course.name)
putInt("courseId", course.courseId)
putBoolean("advanced", course.advanced)
}
fragment.setTargetFragment(this, 42)
})
.addToBackStack("ParticipationCourseFragment")
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
}
override fun onCourseLongClick(course: CourseExam) {
// in this context, Course#oralWeighting is used as CourseDayRelation#relationId
openRemoveLinkSheet(course.name, course.oralWeighting)
}
private fun openLinkSheet() {
openBottomSheet(ParticipationLinkBottomSheet())
}
private fun openRemoveLinkSheet(courseName: String, relationId: Int) {
openBottomSheet(ConfirmDeletionBottomSheet(
getString(R.string.confirm_remove_participation_link, courseName, getDay())
) {
viewModel.removeLink(relationId)
})
}
private fun getDay(): String = resources.getStringArray(R.array.days)[viewModel.dayId - 1]
// target fragment functions
fun getCourses(): List<CourseExam> = viewModel.getCourses()
fun link(courseId: Int) { viewModel.link(courseId) }
fun delete(participation: Participation) { viewModel.delete(participation) }
fun getSchoolYearName() = viewModel.getSchoolYearName()
}
@@ -0,0 +1,115 @@
package com.denizd.qwark.fragment
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.widget.TextView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import com.denizd.qwark.R
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
open class QwarkFragment : Fragment() {
private lateinit var _context: Context
private lateinit var superTitleView: TextView
private lateinit var appBarTitleView: TextView
open var superTitle: String = ""
set(value) {
superTitleView.text = value
field = value
}
open var appBarTitle = ""
set(value) {
appBarTitleView.text = value
field = value
}
private lateinit var statusBarView: View
lateinit var appBar: AppBarLayout
lateinit var snackBarContainer: CoordinatorLayout
lateinit var fab: ExtendedFloatingActionButton
val name: String = javaClass.simpleName
override fun onAttach(context: Context) {
super.onAttach(context)
_context = context
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/**
* Initialise view resources
*/
statusBarView = view.rootView.findViewById(R.id.status_bar_view)
appBarTitleView = view.rootView.findViewById(R.id.toolbarTxt)
superTitleView = view.rootView.findViewById(R.id.text_above)
appBar = view.rootView.findViewById(R.id.app_bar_layout)
snackBarContainer = view.rootView.findViewById(R.id.root_view)
fab = view.rootView.findViewById(R.id.fab)
}
/**
* Overwritten to return non-null context after onAttach has been invoked. If called before
* onAttach has been executed, an IllegalStateException will be thrown
*
* @return the context
*/
override fun getContext(): Context = _context
protected open fun getGridColumnCount(config: Configuration): Int {
return when (config.orientation) {
Configuration.ORIENTATION_PORTRAIT -> 1
else -> 2
} + if (resources.getBoolean(R.bool.isTablet)) 1 else 0
}
protected fun openBottomSheet(sheet: BottomSheetDialogFragment) {
sheet.setTargetFragment(this, 42)
sheet.show((context as FragmentActivity).supportFragmentManager, sheet.javaClass.simpleName)
}
protected fun RecyclerView.addFabScrollListener() {
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
when {
dy > 0 -> fab.hide()
dy < 0 -> fab.show()
}
}
})
}
/**
* This function must not be invoked in onViewCreated, as the view will not have been attached
* to its lifecycle owner and therefore cannot calculate the top padding properly. Invoke in
* onResume instead.
*/
protected fun <T : View> T.applyPadding(verticalPadding: Int = 0) {
setPadding(
verticalPadding.dpToPx(),
(statusBarView.height / 1) + appBar.height,
verticalPadding.dpToPx(),
0
)
}
/**
* Converts integer DP values to PX for use by programmatic means of changing dimensions
*
* @return the dimension converted to PX
*/
private fun Int.dpToPx(): Int = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
resources.displayMetrics
).toInt()
}
@@ -0,0 +1,290 @@
package com.denizd.qwark.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.denizd.qwark.R
import com.denizd.qwark.sheet.ConfirmDeletionBottomSheet
import com.denizd.qwark.sheet.CopyCoursesBottomSheet
import com.denizd.qwark.sheet.ScoreProfileCreateBottomSheet
import com.denizd.qwark.sheet.YearCreateBottomSheet
import com.denizd.qwark.databinding.SettingsFragmentBinding
import com.denizd.qwark.model.ScoreProfile
import com.denizd.qwark.model.SchoolYear
import com.denizd.qwark.viewmodel.SettingsViewModel
import com.google.android.material.snackbar.Snackbar
class SettingsFragment : QwarkFragment() {
private var _binding: SettingsFragmentBinding? = null
private val binding: SettingsFragmentBinding get() = _binding!!
private lateinit var viewModel: SettingsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[SettingsViewModel::class.java]
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = SettingsFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
superTitle = ""
appBarTitle = getString(R.string.settings)
fab.hide()
binding.darkModeDropdown.apply {
setAdapter(
ArrayAdapter.createFromResource(
context,
R.array.dark_mode_options,
R.layout.dropdown_item
)
)
binding.darkModeDropdown.setText(
binding.darkModeDropdown.adapter.getItem(viewModel.getAppTheme()).toString(),
false
)
binding.darkModeDropdown.setOnItemClickListener { _, _, position, _ ->
viewModel.setAppTheme(position)
}
}
binding.showGradeAverageSwitch.apply {
isChecked = viewModel.getShowGradeAverage()
setOnCheckedChangeListener { _, isChecked ->
viewModel.setShowGradeAverage(isChecked)
}
}
binding.courseSortTypeDropdown.apply {
setAdapter(ArrayAdapter.createFromResource(context, R.array.course_sort_type_options, R.layout.dropdown_item))
setText(binding.courseSortTypeDropdown.adapter.getItem(viewModel.getCourseSortType()).toString(), false)
setOnItemClickListener { _, _, position, _ ->
viewModel.setCourseSortType(position)
}
}
binding.gradeTypeDropdown.apply {
setAdapter(
ArrayAdapter.createFromResource(
context,
R.array.grade_type_array,
R.layout.dropdown_item
)
)
setText(
adapter.getItem(viewModel.getGradeType()).toString(),
false
)
setOnItemClickListener { _, _, position, _ ->
viewModel.setGradeType(position)
}
}
viewModel.allYears.observe(viewLifecycleOwner, Observer { years ->
binding.schoolYearDropdown.apply {
setAdapter(
ArrayAdapter<String>(
context,
R.layout.dropdown_item,
years.map { year -> year.year }
)
)
try {
setText(viewModel.getCurrentSchoolYear(), false)
} catch (e: IndexOutOfBoundsException) {
// do nothing if no school years exist or none has been selected
}
setOnItemClickListener { _, _, position, _ ->
viewModel.updateSchoolYear(years[position])
}
}
})
binding.addYearButton.setOnClickListener {
createSchoolYearSheet()
}
binding.editYearButton.setOnClickListener {
createSchoolYearSheet(edit = true) // TODO prohibit opening when text field is empty
}
binding.deleteYearButton.setOnClickListener {
if (binding.schoolYearDropdown.text.isNullOrBlank()) {
presentErrorSnackBar(getString(R.string.no_school_year_selected))
} else {
val deleteYearSheet = ConfirmDeletionBottomSheet(getString(R.string.delete_school_year_desc, binding.schoolYearDropdown.text.toString())) {
viewModel.deleteSchoolYear()
binding.schoolYearDropdown.setText("")
}
openBottomSheet(deleteYearSheet)
}
}
binding.copyCoursesButton.setOnClickListener {
openBottomSheet(CopyCoursesBottomSheet())
}
// binding.copyCoursesButton.setOnLongClickListener {
// // TODO this is currently an experiment
// fun makeSnackbar(text: String) {
// Snackbar.make(snackBarContainer, text, Snackbar.LENGTH_LONG).show()
// }
// makeSnackbar("Sending")
// viewModel.send {
// makeSnackbar("Sent all notes successfully!")
// }
// true
// }
binding.participationCountDropdown.apply {
setAdapter(
ArrayAdapter.createFromResource(
context,
R.array.participation_display_array,
R.layout.dropdown_item
)
)
setText(
adapter.getItem(viewModel.getParticipationDisplay()).toString(),
false
)
setOnItemClickListener { _, _, position, _ ->
viewModel.setParticipationDisplay(position)
}
}
binding.participationDayDropdown.apply {
setAdapter(
ArrayAdapter.createFromResource(
context,
R.array.participation_day_array,
R.layout.dropdown_item
)
)
setText(
adapter.getItem(viewModel.getParticipationDay()).toString(),
false
)
setOnItemClickListener { _, _, position, _ ->
viewModel.setParticipationDay(position)
}
}
viewModel.allScoreProfiles.observe(viewLifecycleOwner, Observer { scoreProfiles ->
binding.scoreProfileDropdown.apply {
setAdapter(
ArrayAdapter<String>(
context,
R.layout.dropdown_item,
scoreProfiles.map { s -> s.name }
)
)
try {
setText(viewModel.getScoreProfileName(), false)
} catch (e: IndexOutOfBoundsException) {
// do nothing if no score profiles exist or none has been selected
}
setOnItemClickListener { _, _, position, _ ->
viewModel.setCurrentScoreProfile(scoreProfiles[position])
}
}
})
binding.addScoreProfileButton.setOnClickListener {
createScoreProfileSheet()
}
binding.editScoreProfileButton.setOnClickListener {
createScoreProfileSheet(edit = true)
}
binding.deleteScoreProfileButton.setOnClickListener {
if (binding.scoreProfileDropdown.text.isNullOrBlank()) {
presentErrorSnackBar(getString(R.string.no_score_profile_selected))
} else {
val deleteScoreProfileSheet = ConfirmDeletionBottomSheet(
getString(R.string.delete_score_profile_desc,
binding.scoreProfileDropdown.text.toString())
) {
viewModel.deleteScoreProfile()
binding.scoreProfileDropdown.setText("")
}
openBottomSheet(deleteScoreProfileSheet)
}
}
}
override fun onResume() {
super.onResume()
binding.settingsScrollView.applyPadding()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
fun copyCourses(yearId: Int) {
viewModel.copyCourses(yearId)
Snackbar
.make(snackBarContainer, getString(R.string.copy_courses_success), Snackbar.LENGTH_LONG)
.show()
}
fun getSchoolYears() = viewModel.allYears.value // TODO does this work on LiveData?
fun getCurrentSchoolYearId() = viewModel.getCurrentSchoolYearId()
private fun createSchoolYearSheet(edit: Boolean = false) {
val currentYear = binding.schoolYearDropdown.text.toString()
if (edit && currentYear == "") {
presentErrorSnackBar(getString(R.string.no_school_year_selected))
} else {
val yearCreateSheet = YearCreateBottomSheet().also { sheet ->
sheet.arguments = Bundle(1).apply {
putString("currentYear", if (edit) currentYear else "")
}
}
openBottomSheet(yearCreateSheet)
}
}
private fun presentErrorSnackBar(message: String) {
Snackbar
.make(snackBarContainer, message, Snackbar.LENGTH_LONG)
.setBackgroundTint(context.getColor(R.color.colorWarning))
.show()
}
private fun createScoreProfileSheet(edit: Boolean = false) {
val currentScoreProfile = binding.scoreProfileDropdown.text.toString()
if (edit && currentScoreProfile == "") {
presentErrorSnackBar(getString(R.string.no_score_profile_selected))
} else {
openBottomSheet(ScoreProfileCreateBottomSheet().also { sheet ->
sheet.arguments = Bundle(1).apply {
putString("currentScoreProfile", if (edit) currentScoreProfile else "")
}
})
}
}
fun setSchoolYear(name: String) {
viewModel.updateSchoolYear(name)
binding.schoolYearDropdown.setText(name)
}
fun insert(schoolYear: SchoolYear) { viewModel.insert(schoolYear) }
fun updateScoreProfile(name: String) {
viewModel.updateScoreProfile(name)
binding.scoreProfileDropdown.setText(name)
}
fun initNewFinalScore(name: String) {
viewModel.initNewFinalScore(name)
}
}
@@ -0,0 +1,30 @@
package com.denizd.qwark.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
/**
* Data class intended to be used only as an entity for room. For access, use CourseExam class
*/
@Entity(tableName = "course")
@ForeignKey(
entity = SchoolYear::class,
parentColumns = ["key"], // this should be "year_id", but I'm too afraid to change it, since it may break the app
childColumns = ["year_id"],
onDelete = ForeignKey.CASCADE
)
data class Course(
val name: String,
val advanced: Boolean,
val icon: String,
val colour: String,
val average: String,
@ColumnInfo(name = "oral_weighting") val oralWeighting: Int,
@ColumnInfo(name = "grade_count") val gradeCount: Int,
val participation: String = "--|--",
val time: Long,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "course_id") val courseId: Int,
@ColumnInfo(name = "year_id") val yearId: Int
)
@@ -0,0 +1,29 @@
package com.denizd.qwark.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "course_schoolday_relation",
foreignKeys = [
ForeignKey(
entity = Course::class,
parentColumns = ["course_id"],
childColumns = ["course_id"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = SchoolDay::class,
parentColumns = ["day_id"],
childColumns = ["day_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class CourseDayRelation(
@ColumnInfo(name = "course_id") val courseId: Int,
@ColumnInfo(name = "day_id") val dayId: Int,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "relation_id") val relationId: Int
)
@@ -0,0 +1,19 @@
package com.denizd.qwark.model
import androidx.room.ColumnInfo
data class CourseExam(
val name: String,
val advanced: Boolean,
val icon: String,
val colour: String,
val average: String,
@ColumnInfo(name = "oral_weighting") val oralWeighting: Int,
@ColumnInfo(name = "grade_count") val gradeCount: Int,
val participation: String = "--|--",
val time: Long,
@ColumnInfo(name = "course_id") val courseId: Int,
@ColumnInfo(name = "year_id") val yearId: Int,
@ColumnInfo(name = "exam_time") val examTime: Long = -1L
)
@@ -0,0 +1,3 @@
package com.denizd.qwark.model
data class CourseIcon(val key: String, val value: Int)
@@ -0,0 +1,24 @@
package com.denizd.qwark.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.ForeignKey.CASCADE
import androidx.room.PrimaryKey
@Entity(tableName = "final_grade")
@ForeignKey(
entity = ScoreProfile::class,
parentColumns = ["score_profile_id"],
childColumns = ["score_profile_id"],
onDelete = CASCADE
)
data class FinalGrade(
val name: String,
val grade: String,
val type: String,
val note: String,
@ColumnInfo(name = "course_id") val courseId: Int,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "final_grade_id") val finalGradeId: Int,
@ColumnInfo(name = "score_profile_id") val scoreProfileId: Int
)
@@ -0,0 +1,24 @@
package com.denizd.qwark.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(tableName = "grade")
@ForeignKey(
entity = Course::class,
parentColumns = ["course_id"],
childColumns = ["course_id"],
onDelete = ForeignKey.CASCADE
)
data class Grade(
val grade: Int,
val note: String,
val verbal: Boolean,
val time: Long,
val weighting: Int,
@ColumnInfo(name = "exam_time") val examTime: Long = -1L,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "grade_id") val gradeId: Int,
@ColumnInfo(name = "course_id") val courseId: Int
)
@@ -0,0 +1,21 @@
package com.denizd.qwark.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.ForeignKey.CASCADE
import androidx.room.PrimaryKey
@Entity(tableName = "historical_avg")
@ForeignKey(
entity = Course::class,
parentColumns = ["course_id"],
childColumns = ["course_id"],
onDelete = CASCADE)
data class HistoricalAvg(
val average: String,
val time: Long,
@ColumnInfo(name = "course_id") val courseId: Int
) {
@PrimaryKey(autoGenerate = true) var id: Int = 0
}
@@ -0,0 +1,14 @@
package com.denizd.qwark.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "note")
data class Note(
val content: String,
val dismissed: Boolean,
val time: Long,
val category: String,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "note_id") val noteId: Int
)
@@ -0,0 +1,32 @@
package com.denizd.qwark.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "participation",
foreignKeys = [
ForeignKey(
entity = Course::class,
parentColumns = ["course_id"],
childColumns = ["course_id"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = SchoolDay::class,
parentColumns = ["day_id"],
childColumns = ["day_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Participation(
@ColumnInfo(name = "times_hand_raised") val timesHandRaised: Int,
@ColumnInfo(name = "times_spoken") val timesSpoken: Int,
val time: Long,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "participation_id") val participationId: Int,
@ColumnInfo(name = "course_id") val courseId: Int,
@ColumnInfo(name = "day_id") val dayId: Int
)
@@ -0,0 +1,23 @@
package com.denizd.qwark.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "school_day",
foreignKeys = [
ForeignKey(
entity = SchoolYear::class,
parentColumns = ["year_id"],
childColumns = ["year_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class SchoolDay(
val day: String,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "day_id") val dayId: Int,
@ColumnInfo(name = "year_id") val yearId: Int
)
@@ -0,0 +1,11 @@
package com.denizd.qwark.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "school_year")
data class SchoolYear(
val year: String,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "year_id") val yearId: Int
)
@@ -0,0 +1,11 @@
package com.denizd.qwark.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "score_profile")
data class ScoreProfile(
val name: String,
@PrimaryKey(autoGenerate = false) @ColumnInfo(name = "score_profile_id") val scoreProfileId: Int
)
@@ -0,0 +1,123 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.content.DialogInterface
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.denizd.qwark.R
import com.denizd.qwark.databinding.ClassSessionSheetBinding
import com.denizd.qwark.fragment.ParticipationCourseFragment
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlin.random.Random
class ClassSessionSheet : BottomSheetDialogFragment() {
private var _binding: ClassSessionSheetBinding? = null
private val binding: ClassSessionSheetBinding get() = _binding!!
private lateinit var mContext: Context
private lateinit var participationFragment: ParticipationCourseFragment
private var timesHandRaised: Int = 0
set(value) {
field = value
binding.handRaisedButton.text = "$value"
}
private var timesSpoken: Int = 0
set(value) {
field = value
binding.spokenButton.text = "$value"
}
private val time: Long = System.currentTimeMillis()
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = ClassSessionSheetBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
participationFragment = targetFragment as ParticipationCourseFragment
binding.handRaisedButton.apply {
setCompoundDrawables(context.getDrawable(R.drawable.hand).also { drawable ->
drawable?.bounds = Rect(0, 0, 256, 256)
drawable?.setTint(mContext.getColor(R.color.colorAccent))
}, null, null, null)
text = "$timesHandRaised"
setOnClickListener {
timesHandRaised += 1
}
setOnLongClickListener {
if (timesHandRaised > 0)
timesHandRaised -= 1
else
if (Random.nextInt(25) == 20) makeToast(R.string.self_esteem)
true
}
}
binding.spokenButton.apply {
setCompoundDrawables(context.getDrawable(R.drawable.speech).also { drawable ->
drawable?.bounds = Rect(0, 0, 256, 256)
drawable?.setTint(mContext.getColor(R.color.colorAccent))
}, null, null, null)
text = "$timesSpoken"
setOnClickListener {
timesSpoken += 1
}
setOnLongClickListener {
if (timesSpoken > 0)
timesSpoken -= 1
else
if (Random.nextInt(25) == 20) makeToast(R.string.self_esteem)
true
}
}
binding.cancelButton.apply {
setOnClickListener {
makeToast(R.string.long_press_to_cancel)
}
setOnLongClickListener {
dismiss()
true
}
}
binding.finishButton.apply {
setOnClickListener {
makeToast(R.string.long_press_to_finish)
}
setOnLongClickListener {
finish()
dismiss()
true
}
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
private fun makeToast(stringResource: Int) {
Toast
.makeText(mContext, getString(stringResource), Toast.LENGTH_LONG)
.show()
}
private fun finish() {
participationFragment.createParticipation(
timesHandRaised, timesSpoken, time
)
}
}
@@ -0,0 +1,62 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider
import com.denizd.qwark.databinding.ConfirmationDialogBinding
import com.denizd.qwark.viewmodel.ConfirmDeletionViewModel
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
class ConfirmDeletionBottomSheet() : BottomSheetDialogFragment() {
constructor(text: String, confirmedAction: () -> Unit) : this() {
this.text = text
this.confirmedAction = confirmedAction
}
private var _binding: ConfirmationDialogBinding? = null
private val binding: ConfirmationDialogBinding get() = _binding!!
private lateinit var mContext: Context
private lateinit var text: String
private lateinit var confirmedAction: () -> Unit
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = ConfirmationDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModel = ViewModelProvider(this)[ConfirmDeletionViewModel::class.java]
if (::text.isInitialized) {
viewModel.textBody = text
viewModel.confirmedFunction = confirmedAction
}
binding.content.text = viewModel.textBody
binding.cancelButton.setOnClickListener {
dismiss()
}
binding.confirmButton.setOnClickListener {
viewModel.confirmedFunction()
dismiss()
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
@@ -0,0 +1,53 @@
package com.denizd.qwark.sheet
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.denizd.qwark.R
import com.denizd.qwark.adapter.YearAdapter
import com.denizd.qwark.databinding.RecyclerDialogBinding
import com.denizd.qwark.fragment.SettingsFragment
import com.denizd.qwark.model.SchoolYear
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
internal class CopyCoursesBottomSheet : BottomSheetDialogFragment(), YearAdapter.YearClickListener {
private var _binding: RecyclerDialogBinding? = null
private val binding: RecyclerDialogBinding get() = _binding!!
private lateinit var settingsTargetFragment: SettingsFragment
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = RecyclerDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsTargetFragment = targetFragment as SettingsFragment
fillWithYears()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
private fun fillWithYears() {
binding.title.text = getString(R.string.pick_a_year)
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = YearAdapter((settingsTargetFragment.getSchoolYears() ?: ArrayList()).filter { y ->
y.yearId != settingsTargetFragment.getCurrentSchoolYearId()
}, this@CopyCoursesBottomSheet)
scheduleLayoutAnimation()
}
}
override fun onYearClick(schoolYear: SchoolYear) {
settingsTargetFragment.copyCourses(schoolYear.yearId)
dismiss()
}
}
@@ -0,0 +1,138 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.denizd.qwark.R
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.databinding.CourseCreateDialogBinding
import com.denizd.qwark.fragment.CourseFragment
import com.denizd.qwark.model.Course
import com.denizd.qwark.model.CourseExam
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
internal class CourseCreateBottomSheet : BottomSheetDialogFragment() {
private var _binding: CourseCreateDialogBinding? = null
private val binding: CourseCreateDialogBinding get() = _binding!!
private lateinit var mContext: Context
private var icon = ""
private lateinit var course: CourseExam
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments?.size() ?: 0 != 0) course = QwarkUtil.getCourseFromBundle(arguments)
}
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = CourseCreateDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val targetFragment = targetFragment as CourseFragment
binding.oralWeightingEditText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
binding.writtenWeightingEditText.setText(if (binding.oralWeightingEditText.text.isNullOrBlank()) {
"100%"
} else {
"${100 - binding.oralWeightingEditText.text.toString().toInt()}%"
})
}
})
if (::course.isInitialized) {
binding.contentEditText.setText(course.name)
binding.advancedCheckBox.isChecked = course.advanced
binding.oralWeightingEditText.setText("${course.oralWeighting}")
icon = if (course.icon == "") "circle" else course.icon
binding.title.text = getString(R.string.edit_course)
binding.createButton.text = getString(R.string.confirm)
} else {
icon = "circle"
binding.oralWeightingEditText.setText(60.toString())
}
binding.imageView.setImageResource(QwarkUtil.getDrawableIntForString(icon))
binding.imageButton.setOnClickListener {
openIconBottomSheet()
}
binding.cancelButton.setOnClickListener {
dismiss()
}
binding.createButton.setOnClickListener {
val verbal = binding.oralWeightingEditText.text
when {
binding.contentEditText.text.toString().isEmpty() -> binding.contentEditText.error = getString(R.string.field_empty)
!verbal.isNullOrBlank() && verbal.toString().toInt() > 100 -> binding.oralWeightingEditText.error = getString(R.string.weighting_above_hundred)
else -> {
if (::course.isInitialized) {
targetFragment.update(
Course(
name = binding.contentEditText.text.toString(),
advanced = binding.advancedCheckBox.isChecked,
icon = icon,
colour = "",
average = course.average,
oralWeighting = if (verbal.isNullOrBlank()) 0 else verbal.toString().toInt(),
gradeCount = course.gradeCount,
time = course.time,
courseId = course.courseId,
yearId = targetFragment.getSchoolYear()
)
)
} else {
targetFragment.insert(
Course(
name = binding.contentEditText.text.toString(),
advanced = binding.advancedCheckBox.isChecked,
icon = icon,
colour = "",
average = if (targetFragment.getGradeType() == QwarkUtil.GRADE_PRECISE) "0.00" else "0",
oralWeighting = if (verbal.isNullOrBlank()) 0 else verbal.toString().toInt(),
gradeCount = 0,
time = System.currentTimeMillis(),
courseId = 0,
yearId = targetFragment.getSchoolYear()
)
)
}
dismiss()
}
}
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
private fun openIconBottomSheet() = fragmentManager?.let { fm ->
val sheet = IconPickerBottomSheet().apply {
arguments = Bundle(1).apply {
putString("currentIcon", icon)
}
}
sheet.setTargetFragment(this, 42)
sheet.show(fm, sheet.tag)
}
internal fun setIcon(icon: String) {
binding.imageView.setImageResource(QwarkUtil.getDrawableIntForString(icon))
this.icon = icon
}
}
@@ -0,0 +1,63 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.denizd.qwark.R
import com.denizd.qwark.databinding.CourseOptionsDialogBinding
import com.denizd.qwark.fragment.CourseFragment
import com.denizd.qwark.model.Course
import com.denizd.qwark.model.CourseExam
import com.denizd.qwark.util.QwarkUtil
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
internal class CourseOptionsBottomSheet : BottomSheetDialogFragment() {
private var _binding: CourseOptionsDialogBinding? = null
private val binding: CourseOptionsDialogBinding get() = _binding!!
private lateinit var mContext: Context
private lateinit var course: CourseExam
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
course = QwarkUtil.getCourseFromBundle(arguments)
}
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = CourseOptionsDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val targetFragment = targetFragment as CourseFragment
binding.title.text = getString(R.string.options_for_course, course.name)
binding.viewHistoryButton.setOnClickListener {
targetFragment.createHistoryDialog(course.courseId, course.name)
dismiss()
}
binding.editCourseButton.setOnClickListener {
targetFragment.createCourseDialog(course)
dismiss()
}
binding.deleteCourseButton.setOnClickListener {
targetFragment.deleteCourse(QwarkUtil.getCourseExamAsCourse(course))
dismiss()
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
@@ -0,0 +1,85 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import com.denizd.qwark.R
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.databinding.FinalGradeEditDialogBinding
import com.denizd.qwark.fragment.FinalsFragment
import com.denizd.qwark.model.FinalGrade
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
internal class FinalsCreateBottomSheet : BottomSheetDialogFragment() {
private var _binding: FinalGradeEditDialogBinding? = null
private val binding: FinalGradeEditDialogBinding get() = _binding!!
private lateinit var mContext: Context
private lateinit var finalGrade: FinalGrade
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finalGrade = QwarkUtil.getFinalGradeFromBundle(arguments)
}
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FinalGradeEditDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val targetFragment = targetFragment as FinalsFragment
binding.courseEdittext.setText(finalGrade.name)
// val gradeArray = ArrayAdapter.createFromResource(mContext, R.array.grades_points, R.layout.dropdown_item)
// binding.gradePicker.setAdapter(gradeArray)
// binding.gradePicker.setText(
// if (finalGrade.grade == "-1") {
// binding.gradePicker.adapter.getItem(0).toString()
// } else {
// QwarkUtil.getGradeWithLeadingZero(finalGrade.grade.toInt())
// },
// false
// )
binding.gradePicker.value = if (finalGrade.grade == "-1") 0 else finalGrade.grade.toInt()
binding.cancelButton.setOnClickListener {
dismiss()
}
binding.confirmButton.setOnClickListener {
when {
binding.courseEdittext.text.isNullOrBlank() -> binding.courseEdittext.error = getString(
R.string.field_empty)
else -> {
val newFinalGrade = FinalGrade(
name = binding.courseEdittext.text.toString(),
grade = binding.gradePicker.value.toString(), // binding.gradePicker.text.toString().toInt().toString(), // ugly workaround to remove the leading zero
type = finalGrade.type,
note = finalGrade.note,
courseId = -1,
finalGradeId = finalGrade.finalGradeId,
scoreProfileId = finalGrade.scoreProfileId
)
targetFragment.update(newFinalGrade)
dismiss()
}
}
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
@@ -0,0 +1,92 @@
package com.denizd.qwark.sheet
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.denizd.qwark.R
import com.denizd.qwark.adapter.CourseAdapter
import com.denizd.qwark.adapter.YearAdapter
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.databinding.RecyclerDialogBinding
import com.denizd.qwark.fragment.FinalsFragment
import com.denizd.qwark.model.Course
import com.denizd.qwark.model.CourseExam
import com.denizd.qwark.model.FinalGrade
import com.denizd.qwark.model.SchoolYear
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import java.math.BigDecimal
import java.math.RoundingMode
internal class FinalsLinkBottomSheet : BottomSheetDialogFragment(), YearAdapter.YearClickListener, CourseAdapter.CourseClickListener {
private lateinit var finalGrade: FinalGrade
private lateinit var binding: RecyclerDialogBinding
private var advanced = false
private var isSelectingCourse = false
private lateinit var finalsTargetFragment: FinalsFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finalGrade = QwarkUtil.getFinalGradeFromBundle(arguments)
advanced = finalGrade.type == "adv"
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = RecyclerDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
finalsTargetFragment = targetFragment as FinalsFragment
fillWithYears()
}
override fun onYearClick(schoolYear: SchoolYear) {
fillWithCourses(schoolYear)
}
override fun onCourseClick(course: CourseExam) {
val newFinalGrade = FinalGrade(
name = course.name,
grade = BigDecimal(course.average.toDouble()).setScale(0, RoundingMode.HALF_UP).toString(),
type = finalGrade.type,
note = finalGrade.note,
courseId = course.courseId,
finalGradeId = finalGrade.finalGradeId,
scoreProfileId = finalGrade.scoreProfileId
)
finalsTargetFragment.update(newFinalGrade)
dismiss()
}
override fun onCourseLongClick(course: CourseExam) {}
private fun fillWithYears() {
binding.title.text = getString(R.string.pick_a_year)
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(context)
adapter = YearAdapter(finalsTargetFragment.getAllYears(), this@FinalsLinkBottomSheet)
scheduleLayoutAnimation()
}
isSelectingCourse = false
}
private fun fillWithCourses(schoolYear: SchoolYear) {
binding.title.text = getString(R.string.pick_a_course)
binding.recyclerView.adapter =
CourseAdapter(
QwarkUtil.getCoursesSorted(
finalsTargetFragment.getAllCourses().filter { c -> c.yearId == schoolYear.yearId && c.advanced == advanced },
finalsTargetFragment.getCourseSortType()
),
this,
finalsTargetFragment.getGradeType()
)
binding.recyclerView.scheduleLayoutAnimation()
isSelectingCourse = true
}
}
@@ -0,0 +1,72 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.denizd.qwark.R
import com.denizd.qwark.databinding.FinalGradeOptionsDialogBinding
import com.denizd.qwark.fragment.FinalsFragment
import com.denizd.qwark.model.FinalGrade
import com.denizd.qwark.util.QwarkUtil
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
internal class FinalsOptionsBottomSheet : BottomSheetDialogFragment() {
private var _binding: FinalGradeOptionsDialogBinding? = null
private val binding: FinalGradeOptionsDialogBinding get() = _binding!!
private lateinit var mContext: Context
private lateinit var finalGrade: FinalGrade
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finalGrade = QwarkUtil.getFinalGradeFromBundle(arguments)
}
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FinalGradeOptionsDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val targetFragment = targetFragment as FinalsFragment
binding.title.text = getString(R.string.options_for_final_grade, if (finalGrade.name.isEmpty()) getString(R.string.final_grade) else finalGrade.name)
if (finalGrade.type == "exam") binding.linkGradeButton.visibility = View.GONE
if (finalGrade.courseId == -1) binding.viewLinkedCourseButton.visibility = View.GONE
binding.editGradeButton.setOnClickListener {
targetFragment.createEditGradeSheet(finalGrade)
dismiss()
}
binding.linkGradeButton.setOnClickListener {
targetFragment.createLinkGradeSheet(finalGrade)
dismiss()
}
binding.viewLinkedCourseButton.setOnClickListener {
targetFragment.viewLinkedCourse(finalGrade.courseId)
dismiss()
}
binding.clearGradeButton.setOnClickListener {
if (finalGrade.grade == "-1") {
targetFragment.clearFinalGradeError()
} else {
targetFragment.clearFinalGrade(finalGrade)
}
dismiss()
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
@@ -0,0 +1,175 @@
package com.denizd.qwark.sheet
import android.animation.LayoutTransition
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import com.denizd.qwark.R
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.databinding.GradeCreateDialogBinding
import com.denizd.qwark.fragment.GradeFragment
import com.denizd.qwark.model.Grade
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import java.text.ParseException
import java.util.*
class GradeCreateBottomSheet : BottomSheetDialogFragment() {
private var _binding: GradeCreateDialogBinding? = null
private val binding: GradeCreateDialogBinding get() = _binding!!
private lateinit var mContext: Context
private lateinit var grade: Grade
private var courseId: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments?.size() ?: 0 > 1) grade = QwarkUtil.getGradeFromBundle(arguments)
courseId = arguments?.getInt("courseId") ?: 0
}
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = GradeCreateDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
LayoutTransition().apply {
setAnimateParentHierarchy(false)
(binding.root as LinearLayout).layoutTransition = this
}
val targetFragment = targetFragment as GradeFragment
// initGradeDropdown()
binding.verbalSwitch.setOnCheckedChangeListener { switch, isChecked ->
binding.verbalSwitchText.text = when (isChecked) {
true -> getString(R.string.written)
false -> getString(R.string.verbal)
}
if (!isChecked) {
binding.examSwitch.isChecked = false
switch.isChecked = false
}
}
if (::grade.isInitialized) {
with (binding) {
title.text = getString(R.string.edit_grade)
createButton.text = getString(R.string.confirm)
gradePicker.value = if (grade.grade == -1) 0 else grade.grade
weightingEditText.setText(grade.weighting.toString())
noteEditText.setText(grade.note)
verbalSwitch.isChecked = !grade.verbal
examSwitch.isChecked = grade.examTime != -1L
examEditText.setText(QwarkUtil.getSimpleDateFormat().format(Date(grade.examTime)))
if (examSwitch.isChecked) examTextLayout.visibility = View.VISIBLE // also hide grade picker when applicable
}
}
binding.gradePicker.displayedValues = QwarkUtil.getGradeArray(targetFragment.getGradeType())
binding.examSwitch.setOnCheckedChangeListener { _, isChecked ->
setGradePickerVisibility(isChecked)
binding.verbalSwitch.isChecked = true
binding.examTextLayout.visibility = if (isChecked) View.VISIBLE else View.GONE
// if (!isChecked) initGradeDropdown()
}
binding.examEditText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
setGradePickerVisibility(binding.examSwitch.isChecked)
}
})
binding.cancelButton.setOnClickListener {
dismiss()
}
binding.createButton.setOnClickListener {
when {
binding.weightingEditText.text.isNullOrBlank() -> binding.weightingEditText.error =
getString(R.string.field_empty)
binding.weightingEditText.text.toString().toInt() > 100 -> binding.weightingEditText.error =
getString(R.string.weighting_above_hundred)
else -> {
try {
val examDate = if (binding.examSwitch.isChecked) parseExamDate() else 1L
val newGrade = Grade(
grade = if (binding.examSwitch.isChecked && examDate >= QwarkUtil.timeAtMidnight) -1 else binding.gradePicker.value,
verbal = !binding.verbalSwitch.isChecked,
weighting = binding.weightingEditText.text.toString().toInt(),
note = binding.noteEditText.text.toString(),
time = if (::grade.isInitialized) grade.time else System.currentTimeMillis(),
gradeId = if (::grade.isInitialized) grade.gradeId else 0,
courseId = courseId,
examTime = if (binding.examSwitch.isChecked) examDate else -1L
)
if (::grade.isInitialized) {
targetFragment.update(newGrade)
} else {
targetFragment.insert(newGrade)
}
dismiss()
} catch (e: ParseException) {
binding.examEditText.error = getString(R.string.date_format_issue)
}
}
}
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
// private fun initGradeDropdown() {
// binding.gradePicker.apply {
// setAdapter(
// ArrayAdapter.createFromResource(
// mContext,
// R.array.grades_points,
// R.layout.dropdown_item
// ).also {
// it.setDropDownViewResource(R.layout.dropdown_item)
// }
// ) // TODO replace with NumberPicker or custom solution
// setText(binding.gradePicker.adapter.getItem(0).toString(), false)
// }
// }
@Throws(ParseException::class)
private fun parseExamDate(): Long = QwarkUtil.getSimpleDateFormat().parse(binding.examEditText.text.toString())?.time ?: 0L
// @Throws(NumberFormatException::class)
// private fun getGrade(): Int = binding.gradePicker.text.toString().toInt()
private fun setGradePickerVisibility(isExamGrade: Boolean) {
if (binding.examEditText.text.toString().length == 10) {
binding.gradePicker.visibility = try {
if (isExamGrade && parseExamDate() >= QwarkUtil.timeAtMidnight) {
View.GONE
} else {
View.VISIBLE
}
} catch (e: ParseException) {
View.VISIBLE
}
}
binding.examEditText.requestFocus()
}
}
@@ -0,0 +1,48 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import com.denizd.qwark.R
import com.denizd.qwark.adapter.HistoricalAverageAdapter
import com.denizd.qwark.databinding.RecyclerDialogBinding
import com.denizd.qwark.fragment.CourseFragment
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
internal class HistoricalAverageBottomSheet : BottomSheetDialogFragment() {
private var _binding: RecyclerDialogBinding? = null
private val binding: RecyclerDialogBinding get() = _binding!!
private lateinit var mContext: Context
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = RecyclerDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val targetFragment = targetFragment as CourseFragment
val data = targetFragment.getAveragesForCourse(arguments?.getInt("courseId") ?: -1)
binding.title.text = getString(R.string.averages_for_course, arguments?.getString("courseName"))
binding.recyclerView.apply {
layoutManager = GridLayoutManager(context, 1)
adapter = HistoricalAverageAdapter(data)
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
@@ -0,0 +1,73 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import com.denizd.qwark.adapter.IconAdapter
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.databinding.IconDialogBinding
import com.denizd.qwark.model.CourseIcon
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
class IconPickerBottomSheet : BottomSheetDialogFragment(),
IconAdapter.IconClickListener {
private var _binding: IconDialogBinding? = null
private val binding: IconDialogBinding get() = _binding!!
private lateinit var mContext: Context
private var icon: String = ""
private lateinit var iconAdapter: IconAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
icon = arguments?.getString("currentIcon") ?: ""
}
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = IconDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val targetFragment = targetFragment as CourseCreateBottomSheet
val icons = QwarkUtil.drawablesList
iconAdapter = IconAdapter(icons, this)
if (icon != "") {
iconAdapter.setSelectedItem(icons.indexOf(CourseIcon(icon, QwarkUtil.getDrawableIntForString(icon))))
}
binding.recyclerView.apply {
layoutManager = GridLayoutManager(mContext, 4)
adapter = iconAdapter
}
binding.cancelButton.setOnClickListener {
dismiss()
}
binding.confirmButton.setOnClickListener {
targetFragment.setIcon(icon)
dismiss()
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onIconClick(key: String, value: Int, position: Int) {
icon = key
iconAdapter.setSelectedItem(position)
}
}
@@ -0,0 +1,93 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import com.denizd.qwark.R
import com.denizd.qwark.databinding.NoteCreateDialogBinding
import com.denizd.qwark.fragment.NoteFragment
import com.denizd.qwark.model.Note
import com.denizd.qwark.util.QwarkUtil
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
internal class NoteCreateBottomSheet : BottomSheetDialogFragment() {
private var _binding: NoteCreateDialogBinding? = null
private val binding: NoteCreateDialogBinding get() = _binding!!
private lateinit var mContext: Context
private lateinit var note: Note
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments?.size() ?: 0 != 0) note = QwarkUtil.getNoteFromBundle(arguments)
}
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = NoteCreateDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val targetFragment = targetFragment as NoteFragment
binding.categoryDropDown.setAdapter(
ArrayAdapter<String>(mContext, R.layout.dropdown_item, targetFragment.getAllCategories())
)
if (::note.isInitialized) {
binding.title.text = getString(R.string.edit_note)
binding.contentEditText.setText(note.content)
binding.categoryDropDown.setText(note.category, false)
binding.createButton.text = getString(R.string.confirm)
}
binding.createButton.setOnClickListener {
if (binding.contentEditText.text.isNullOrBlank()) {
binding.contentEditText.error = getString(R.string.field_empty)
} else {
if (::note.isInitialized) {
targetFragment.saveScrollPosition()
targetFragment.update(
Note(
content = binding.contentEditText.text.toString(),
dismissed = note.dismissed,
time = note.time,
category = binding.categoryDropDown.text.toString().trim(),
noteId = note.noteId
)
)
} else {
targetFragment.insert(
Note(
content = binding.contentEditText.text.toString(),
dismissed = false,
time = System.currentTimeMillis(),
category = binding.categoryDropDown.text.toString().trim(),
noteId = 0
)
)
}
dismiss()
}
}
binding.cancelButton.setOnClickListener {
dismiss()
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
@@ -0,0 +1,60 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.denizd.qwark.R
import com.denizd.qwark.util.QwarkUtil
import com.denizd.qwark.databinding.NoteBottomSheetBinding
import com.denizd.qwark.model.Note
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
internal class NoteDisplayBottomSheet : BottomSheetDialogFragment() {
private var _binding: NoteBottomSheetBinding? = null
private val binding: NoteBottomSheetBinding get() = _binding!!
private lateinit var mContext: Context
private lateinit var note: Note
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
note = QwarkUtil.getNoteFromBundle(arguments)
}
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = NoteBottomSheetBinding.inflate(inflater, container, false)
return binding.root
}
// override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// return super.onCreateDialog(savedInstanceState).also { dialog ->
// dialog.setOnShowListener {
// BottomSheetBehavior.from(dialog.findViewById<FrameLayout>(
// com.google.android.material.R.id.design_bottom_sheet
// )).apply {
// state = BottomSheetBehavior.STATE_EXPANDED
// }
// }
// }
// }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.title.text = QwarkUtil.getCreatedString(mContext, note.time)
val noteText = if (note.dismissed) getString(R.string.note_dismissed) else "%s"
binding.note.text = String.format(noteText, note.content)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
@@ -0,0 +1,73 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.denizd.qwark.R
import com.denizd.qwark.databinding.NoteOptionsDialogBinding
import com.denizd.qwark.fragment.NoteFragment
import com.denizd.qwark.model.Note
import com.denizd.qwark.util.QwarkUtil
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
internal class NoteOptionsBottomSheet : BottomSheetDialogFragment() {
private var _binding: NoteOptionsDialogBinding? = null
private val binding: NoteOptionsDialogBinding get() = _binding!!
private lateinit var mContext: Context
private lateinit var note: Note
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
note = QwarkUtil.getNoteFromBundle(arguments)
}
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = NoteOptionsDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (note.dismissed) {
binding.dismissedStatusText.text = getString(R.string.mark_note_undone)
binding.dismissedImageView.setImageDrawable(mContext.getDrawable(R.drawable.label))
}
if (note.category == "") {
binding.viewCategoryButton.visibility = View.GONE
}
binding.viewCategoryText.text = getString(R.string.view_notes_for_category, note.category)
val noteFragment = targetFragment as NoteFragment
binding.updateDismissedButton.setOnClickListener {
dismiss()
noteFragment.updateDismissed(note)
}
binding.viewCategoryButton.setOnClickListener {
dismiss()
noteFragment.sortByCategory(note.category)
}
binding.editNoteButton.setOnClickListener {
dismiss()
noteFragment.createNoteDialog(note)
}
binding.deleteNoteButton.setOnClickListener {
dismiss()
noteFragment.deleteNote(note.noteId)
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
@@ -0,0 +1,68 @@
package com.denizd.qwark.sheet
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.denizd.qwark.R
import com.denizd.qwark.adapter.CourseAdapter
import com.denizd.qwark.databinding.RecyclerDialogBinding
import com.denizd.qwark.fragment.ParticipationFragment
import com.denizd.qwark.model.*
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
internal class ParticipationLinkBottomSheet : BottomSheetDialogFragment(), CourseAdapter.CourseClickListener {
private lateinit var finalGrade: FinalGrade
private lateinit var binding: RecyclerDialogBinding
private lateinit var participationFragment: ParticipationFragment
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = RecyclerDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
participationFragment = targetFragment as ParticipationFragment
fillWithCourses()
}
override fun onCourseClick(course: CourseExam) {
participationFragment.link(course.courseId)
dismiss()
}
override fun onCourseLongClick(course: CourseExam) {}
private fun fillWithCourses() {
binding.title.text = getString(R.string.pick_a_course)
binding.recyclerView.apply {
adapter =
CourseAdapter(
participationFragment.getCourses().map { c ->
CourseExam(
c.name,
c.advanced,
c.icon,
c.colour,
c.average,
c.oralWeighting,
c.gradeCount,
c.participation,
c.time,
c.courseId,
c.yearId,
0
)
},
this@ParticipationLinkBottomSheet,
null
)
layoutManager = LinearLayoutManager(context)
scheduleLayoutAnimation()
}
}
}
@@ -0,0 +1,46 @@
package com.denizd.qwark.sheet
import android.os.Bundle
import android.view.View
import com.denizd.qwark.R
import com.denizd.qwark.fragment.SettingsFragment
internal class ScoreProfileCreateBottomSheet : TextInputBottomSheet() {
private lateinit var settingsFragment: SettingsFragment
private var currentScoreProfile: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
currentScoreProfile = arguments?.getString("currentScoreProfile") ?: ""
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsFragment = targetFragment as SettingsFragment
binding.textInputLayout.hint = getString(R.string.score_profile)
if (currentScoreProfile != "") {
binding.title.text = getString(R.string.edit_score_profile)
binding.createButton.text = getString(R.string.confirm)
binding.editText.setText(currentScoreProfile)
} else {
binding.title.text = getString(R.string.add_score_profile)
}
}
override fun positiveButtonClicked() {
val name = binding.editText.text
if (name.isNullOrBlank()) {
binding.editText.error = getString(R.string.field_empty)
} else {
if (currentScoreProfile == "") {
settingsFragment.initNewFinalScore(name.toString())
} else {
settingsFragment.updateScoreProfile(name.toString())
}
}
super.positiveButtonClicked()
}
}
@@ -0,0 +1,41 @@
package com.denizd.qwark.sheet
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.denizd.qwark.databinding.TextInputSheetBinding
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
internal open class TextInputBottomSheet : BottomSheetDialogFragment() {
protected var _binding: TextInputSheetBinding? = null
protected val binding: TextInputSheetBinding get() = _binding!!
private lateinit var mContext: Context
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = TextInputSheetBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.createButton.setOnClickListener { positiveButtonClicked() }
binding.cancelButton.setOnClickListener { negativeButtonClicked() }
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
protected open fun positiveButtonClicked() { dismiss() }
protected open fun negativeButtonClicked() { dismiss() }
}
@@ -0,0 +1,47 @@
package com.denizd.qwark.sheet
import android.os.Bundle
import android.view.View
import com.denizd.qwark.R
import com.denizd.qwark.fragment.SettingsFragment
import com.denizd.qwark.model.SchoolYear
internal class YearCreateBottomSheet : TextInputBottomSheet() {
private lateinit var settingsFragment: SettingsFragment
private var currentYear: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
currentYear = arguments?.getString("currentYear") ?: ""
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsFragment = targetFragment as SettingsFragment
binding.textInputLayout.hint = getString(R.string.school_year_cap)
if (currentYear != "") {
binding.title.text = getString(R.string.edit_school_year)
binding.createButton.text = getString(R.string.confirm)
binding.editText.setText(currentYear)
} else {
binding.title.text = getString(R.string.add_school_year)
}
}
override fun positiveButtonClicked() {
val name = binding.editText.text
if (binding.editText.text.isNullOrBlank()) {
binding.editText.error = getString(R.string.field_empty)
} else {
if (currentYear == "") {
settingsFragment.insert(SchoolYear(name.toString(), 0))
} else {
settingsFragment.setSchoolYear(name.toString())
}
}
super.positiveButtonClicked()
}
}
@@ -0,0 +1,105 @@
package com.denizd.qwark.util
import android.content.Context
import com.denizd.qwark.R
import com.denizd.qwark.model.CourseExam
import com.denizd.qwark.model.Grade
import java.lang.NumberFormatException
import java.math.BigDecimal
import java.math.RoundingMode
fun Double.round(scale: Int = 2): Double = BigDecimal(this).setScale(scale, RoundingMode.HALF_UP).toDouble()
fun Double.roundToInt(): Int = round(0).toInt()
fun BigDecimal.roundToInt(): Int = setScale(0, RoundingMode.HALF_UP).toInt()
fun String.roundToInt(): Int = BigDecimal(this.toDouble()).setScale(0, RoundingMode.HALF_UP).toInt()
fun String.roundToDouble(): Double = BigDecimal(this.toDouble()).setScale(2, RoundingMode.HALF_UP).toDouble()
fun Double.roundToGrade(): String {
val result = ((17 - this) / 3).round().toString()
return if (result.toDouble() < 1.0) "1.0" else result
}
fun List<Grade>.calculateAverage(oralWeighting: Int): String = if (this.isEmpty()) {
BigDecimal(0.0).setScale(2, RoundingMode.HALF_UP)
} else {
val oralGrades = this.filter { it.verbal }
val writtenGrades = this.filter { !it.verbal }
var oralGrade = 0.0
var oralPercentage = 0.0
var writtenGrade = 0.0
var writtenPercentage = 0.0
for (g in oralGrades) {
oralGrade += (g.grade.toDouble() * (g.weighting.toDouble() / 100.0))
oralPercentage += g.weighting.toDouble()
}
for (g in writtenGrades) {
writtenGrade += (g.grade.toDouble() * (g.weighting.toDouble() / 100.0))
writtenPercentage += g.weighting.toDouble()
}
if (oralPercentage != 100.0) {
oralGrade = (oralGrade / (oralPercentage / 100.0))
}
if (writtenPercentage != 100.0) {
writtenGrade = (writtenGrade / (writtenPercentage / 100.0))
}
// return value for totalGrade
BigDecimal(
try {
when {
writtenPercentage == 0.0 -> oralGrade
oralPercentage == 0.0 -> writtenGrade
else -> (oralGrade * (oralWeighting.toDouble() / 100.0)) + (writtenGrade * ((100.0 - oralWeighting.toDouble()) / 100.0))
}
} catch (n: NumberFormatException) {
0.0
}
).setScale(2, RoundingMode.HALF_UP)
}.toString()
fun List<CourseExam>.calculateTotalAverage(context: Context, gradeType: Int, schoolYearName: String, showGradeAverage: Boolean): String {
var totalAvg = 0.0
var totalCourses = 0.0
var totalGradeCount = 0
for (course in this) {
if (course.gradeCount > 0) {
val courseAverage = if (gradeType == QwarkUtil.GRADE_PRECISE) course.average.toDouble() else course.average.roundToInt().toDouble()
totalAvg += if (course.advanced) courseAverage * 2.0 else courseAverage
totalCourses += if (course.advanced) 2.0 else 1.0
totalGradeCount += 1
}
}
val finalAverage = if (totalCourses > 0.0) {
totalAvg /= totalCourses
totalAvg.round()
} else {
0.0
}
return when {
schoolYearName.isBlank() -> context.getString(R.string.no_school_year_selected)
totalGradeCount == 0 -> context.getString(R.string.no_grades_entered_placeholder, schoolYearName)
gradeType in arrayOf(QwarkUtil.GRADE_PRECISE, QwarkUtil.GRADE_ROUNDED) -> context.getString(
if (showGradeAverage) R.string.average_and_grade_for_year else R.string.average_for_year,
schoolYearName,
finalAverage,
finalAverage.roundToGrade()
)
else -> context.getString(
R.string.average_grade_for_year,
schoolYearName,
finalAverage.toString().getUserDefinedAverage(gradeType)
)
}
}
fun String.getUserDefinedAverage(gradeType: Int?, isGradeAdapter: Boolean = false): String = when (gradeType) {
QwarkUtil.GRADE_PRECISE, null -> this
QwarkUtil.GRADE_ROUNDED -> {
if (isGradeAdapter)
QwarkUtil.getGradeWithLeadingZero(this.roundToInt())
else
this.roundToInt().toString()
}
else -> QwarkUtil.getNewGradeType(this.roundToInt(), gradeType)
}
@@ -0,0 +1,5 @@
package com.denizd.qwark.util
internal interface OnBackPressed {
fun onBackPressed(): Boolean
}
@@ -0,0 +1,21 @@
package com.denizd.qwark.util
enum class PreferenceKey(val value: String) {
// Strings
SCHOOL_YEAR_NAME("school_year_name"),
SCORE_PROFILE_NAME("score_profile_name"),
// Ints
APP_THEME("dark_mode"),
GRADE_TYPE("grade_type"),
SCHOOL_YEAR_ID("school_year"),
SCORE_PROFILE_ID("final_score_id"),
MAX_SCORE_PROFILE_ID("max_final_score_id"),
COURSE_SORT_TYPE("course_sort_type"),
PARTICIPATION_DISPLAY("participation_display"),
PARTICIPATION_DAY("participation_day"),
// Bools
SHOW_GRADE_AVERAGE("show_grade_average")
}
@@ -0,0 +1,54 @@
package com.denizd.qwark.util
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
internal class QwarkPreferences private constructor(context: Context) {
private val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val edit: SharedPreferences.Editor = prefs.edit()
companion object {
private var qwarkPrefs: QwarkPreferences? = null
internal fun getPrefs(context: Context): QwarkPreferences {
if (qwarkPrefs == null) {
synchronized (QwarkPreferences::class) {
qwarkPrefs = QwarkPreferences(context)
}
}
return qwarkPrefs!!
}
}
internal fun getString(key: PreferenceKey, default: String = ""): String = prefs.getString(key.value, default) ?: default
internal fun getInt(key: PreferenceKey, default: Int = 0): Int = prefs.getInt(key.value, default)
internal fun getBool(key: PreferenceKey, default: Boolean = false): Boolean = prefs.getBoolean(key.value, default)
internal fun putString(key: PreferenceKey, value: String): QwarkPreferences {
edit.putString(key.value, value).apply()
return this
}
internal fun putInt(key: PreferenceKey, value: Int): QwarkPreferences {
edit.putInt(key.value, value).apply()
return this
}
internal fun putBool(key: PreferenceKey, value: Boolean): QwarkPreferences {
edit.putBoolean(key.value, value).apply()
return this
}
internal fun getShowGradeAverage(): Boolean = getBool(PreferenceKey.SHOW_GRADE_AVERAGE)
internal fun getGradeType(): Int = getInt(PreferenceKey.GRADE_TYPE)
internal fun getCourseSortType(): Int = getInt(PreferenceKey.COURSE_SORT_TYPE, 1)
internal fun setCourseSortType(value: Int) { putInt(PreferenceKey.COURSE_SORT_TYPE, value) }
internal fun getSchoolYear(): Int = getInt(PreferenceKey.SCHOOL_YEAR_ID)
internal fun getSchoolYearName(): String = getString(PreferenceKey.SCHOOL_YEAR_NAME)
internal fun getScoreProfileName(): String = getString(PreferenceKey.SCORE_PROFILE_NAME)
internal fun getScoreProfileId(): Int = getInt(PreferenceKey.SCORE_PROFILE_ID)
fun getParticipationDisplay(): Int = getInt(PreferenceKey.PARTICIPATION_DISPLAY)
fun getParticipationDay(): Int = getInt(PreferenceKey.PARTICIPATION_DAY)
fun setParticipationDisplay(value: Int) { putInt(PreferenceKey.PARTICIPATION_DISPLAY, value) }
fun setParticipationDay(value: Int) { putInt(PreferenceKey.PARTICIPATION_DAY, value) }
}
@@ -0,0 +1,341 @@
package com.denizd.qwark.util
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.denizd.qwark.R
import com.denizd.qwark.databinding.ConfirmationDialogBinding
import com.denizd.qwark.model.*
import com.google.android.material.button.MaterialButton
import java.text.SimpleDateFormat
import java.util.*
object QwarkUtil {
val drawablesList: ArrayList<CourseIcon> by lazy {
getDrawablesAsList()
}
private val drawables: Map<String, Int> = mapOf(
"circle" to R.drawable.ic_circle,
"arts" to R.drawable.ic_arts,
"biology" to R.drawable.ic_biology,
"chemistry" to R.drawable.ic_chemistry,
"chinese" to R.drawable.ic_chinese,
"cogwheel" to R.drawable.ic_cogwheel,
"compsci" to R.drawable.ic_compsci,
"drama" to R.drawable.ic_drama,
"english" to R.drawable.ic_english,
"french" to R.drawable.ic_french,
"geography" to R.drawable.ic_geography,
"german" to R.drawable.ic_german,
"gll" to R.drawable.ic_gll,
"help" to R.drawable.ic_help,
"history" to R.drawable.ic_history,
"idea" to R.drawable.ic_idea,
"information" to R.drawable.ic_information,
"latin" to R.drawable.ic_latin,
"maths" to R.drawable.ic_maths,
"music" to R.drawable.ic_music,
"pe" to R.drawable.ic_pe,
"pencil" to R.drawable.ic_pencil,
"philosophy" to R.drawable.ic_philosophy,
"physics" to R.drawable.ic_physics,
"politics" to R.drawable.ic_politics,
"religion" to R.drawable.ic_religion,
"spanish" to R.drawable.ic_spanish,
"tomato" to R.drawable.ic_tomato,
"turkish" to R.drawable.ic_turkish,
"wat" to R.drawable.ic_wat
)
private val numberedGrades: Map<Int, String> = mapOf(
15 to "1+",
14 to "1",
13 to "1-",
12 to "2+",
11 to "2",
10 to "2-",
9 to "3+",
8 to "3",
7 to "3-",
6 to "4+",
5 to "4",
4 to "4-",
3 to "5+",
2 to "5",
1 to "5-",
0 to "6"
)
private val letteredGrades: Map<Int, String> = mapOf(
15 to "A+",
14 to "A",
13 to "A-",
12 to "B+",
11 to "B",
10 to "B-",
9 to "C+",
8 to "C",
7 to "C-",
6 to "D+",
5 to "D",
4 to "D-",
3 to "E+",
2 to "E",
1 to "E-",
0 to "F"
)
private val lettersToPoints: Map<String, Int> = mapOf(
"A+" to 15,
"A" to 14,
"A-" to 13,
"B+" to 12,
"B" to 11,
"B-" to 10,
"C+" to 9,
"C" to 8,
"C-" to 7,
"D+" to 6,
"D" to 5,
"D-" to 4,
"E+" to 3,
"E" to 2,
"E-" to 1,
"F" to 0
)
private val gradesToPoints: Map<String, Int> = mapOf(
"1+" to 15,
"1" to 14,
"1-" to 13,
"2+" to 12,
"2" to 11,
"2-" to 10,
"3+" to 9,
"3" to 8,
"3-" to 7,
"4+" to 6,
"4" to 5,
"4-" to 4,
"5+" to 3,
"5" to 2,
"5-" to 1,
"6" to 0
)
val pointGradeArray: Array<String> = arrayOf(
"0", "1", "2", "3", "4", "5", "6",
"7", "8", "9", "10", "11", "12", "13", "14", "15"
)
private val numberedGradeArray: Array<String> = arrayOf(
"6", "5-", "5", "5+", "4-", "4", "4+",
"3-", "3", "3+", "2-", "2", "2+", "1-", "1", "1+"
)
private val letteredGradeArray: Array<String> = arrayOf(
"F", "E-", "E", "E+", "D-", "D", "D+",
"C-", "C", "C+", "B-", "B", "B+", "A-", "A", "A+"
)
fun getGradeArray(gradeType: Int): Array<String> = when (gradeType) {
GRADE_NUMBERED -> numberedGradeArray
GRADE_LETTERED -> letteredGradeArray
else -> pointGradeArray
}
private fun Double.getGradeType(map: Map<Int, String>): String = map[this.roundToInt()] ?: ""
fun getNewGradeType(grade: Int, type: Int): String = when (type) {
GRADE_NUMBERED -> numberedGrades[grade] ?: "error"
GRADE_LETTERED -> letteredGrades[grade] ?: "error"
else -> "error"
}
fun getNumberForLetter(grade: String): Int = lettersToPoints[grade] ?: -1
val timeAtMidnight: Long = GregorianCalendar().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
const val GRADE_PRECISE = 0
const val GRADE_ROUNDED = 1
const val GRADE_NUMBERED = 2
const val GRADE_LETTERED = 3
private const val SORT_COURSES_TIME_ASC = 0
private const val SORT_COURSES_TIME_DESC = 1
private const val SORT_COURSES_NAME_ASC = 2
private const val SORT_COURSES_NAME_DESC = 3
private const val SORT_COURSES_ADV = 4
private const val SORT_COURSES_BASIC = 5
private const val SORT_COURSES_AVG_ASC = 6
private const val SORT_COURSES_AVG_DESC = 7
private const val SORT_COURSES_NEXT_EXAM = 8
fun getCoursesSorted(courses: List<CourseExam>, sortType: Int): List<CourseExam> = when (sortType) { // TODO check what ascending and descending really mean lol
SORT_COURSES_TIME_ASC -> courses.asReversed()
SORT_COURSES_TIME_DESC -> courses
SORT_COURSES_NAME_ASC -> courses.sortedBy { c -> c.name }
SORT_COURSES_NAME_DESC -> courses.sortedByDescending { c -> c.name }
SORT_COURSES_ADV -> courses.sortedByDescending { c -> c.advanced }
SORT_COURSES_BASIC -> courses.sortedBy { c -> c.advanced }
SORT_COURSES_AVG_ASC -> courses.sortedBy { c -> c.average.toDouble() }
SORT_COURSES_AVG_DESC -> courses.sortedByDescending { c -> c.average.toDouble() }
else -> courses.sortedBy { c -> if (c.examTime > 1L) c.examTime else Long.MAX_VALUE } // NEXT_EXAM
}
fun getGradeAsString(grade: Int, leadingZero: Boolean): String = (if (leadingZero && grade < 10) "0" else "") + grade
fun getGradeWithLeadingZero(grade: Int) = "${if (grade < 10) "0" else ""}$grade"
fun getSimpleDateTimeFormat(): SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd, HH:mm", Locale.ROOT)
fun getSimpleDateFormat(): SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
fun getDrawableIntForString(drawable: String): Int {
return drawables[drawable] ?: 0
}
private fun getMapAsCourseIcon(entry: Map.Entry<String, Int>): CourseIcon {
return CourseIcon(entry.key, entry.value)
}
private fun getDrawablesAsList(): ArrayList<CourseIcon> = ArrayList<CourseIcon>().also { list ->
for (drawable in drawables) {
list.add(getMapAsCourseIcon(drawable))
}
}
fun getCreatedString(context: Context, time: Long): String {
return context.getString(R.string.created_at, getSimpleDateTimeFormat().format(Date(time)))
}
fun createInfoDialog(context: Context, title: String, body: String) {
val dialogBuilder = AlertDialog.Builder(context)
val dialogView = View.inflate(context, R.layout.info_display_dialog, null)
dialogView.findViewById<TextView>(R.id.info_title_textview).text = title
dialogView.findViewById<TextView>(R.id.info_body_textview).text = body
val dialog = dialogBuilder.setView(dialogView).create()
dialogView.findViewById<MaterialButton>(R.id.confirm_button).setOnClickListener {
dialog.dismiss()
}
dialog.show()
}
fun putCourseInBundle(fragment: Fragment, course: CourseExam) {
val bundle = Bundle(11).apply {
putString("name", course.name)
putBoolean("advanced", course.advanced)
putString("icon", course.icon)
putString("colour", course.colour)
putString("average", course.average)
putInt("oralWeighting", course.oralWeighting)
putInt("gradeCount", course.gradeCount)
putLong("time", course.time)
putInt("courseId", course.courseId)
putInt("yearId", course.yearId)
putLong("examTime", course.examTime)
}
fragment.arguments = bundle
}
fun getCourseFromBundle(bundle: Bundle?) = CourseExam(
name = bundle?.getString("name") ?: "",
advanced = bundle?.getBoolean("advanced") ?: false,
icon = bundle?.getString("icon") ?: "",
colour = bundle?.getString("colour") ?: "",
average = bundle?.getString("average") ?: "",
oralWeighting = bundle?.getInt("oralWeighting") ?: 0,
gradeCount = bundle?.getInt("gradeCount") ?: 0,
time = bundle?.getLong("time") ?: 0L,
courseId = bundle?.getInt("courseId") ?: 0,
yearId = bundle?.getInt("yearId") ?: 0,
examTime = bundle?.getLong("examTime") ?: 0L
)
fun putFinalGradeInBundle(fragment: Fragment, finalGrade: FinalGrade) {
val bundle = Bundle(7).apply {
putString("name", finalGrade.name)
putInt("courseId", finalGrade.courseId)
putString("grade", finalGrade.grade)
putString("type", finalGrade.type)
putString("note", finalGrade.note)
putInt("finalGradeId", finalGrade.finalGradeId)
putInt("scoreProfileId", finalGrade.scoreProfileId)
}
fragment.arguments = bundle
}
fun getFinalGradeFromBundle(bundle: Bundle?) = FinalGrade(
name = bundle?.getString("name") ?: "",
courseId = bundle?.getInt("courseId") ?: 0,
grade = bundle?.getString("grade") ?: "",
type = bundle?.getString("type") ?: "",
note = bundle?.getString("note") ?: "",
finalGradeId = bundle?.getInt("finalGradeId") ?: 0,
scoreProfileId = bundle?.getInt("scoreProfileId") ?: 0
)
fun putGradeInBundle(fragment: Fragment, grade: Grade) {
val bundle = Bundle(8).apply {
putInt("grade", grade.grade)
putString("note", grade.note)
putBoolean("verbal", grade.verbal)
putLong("time", grade.time)
putInt("weighting", grade.weighting)
putLong("examTime", grade.examTime)
putInt("gradeId", grade.gradeId)
putInt("courseId", grade.courseId)
}
fragment.arguments = bundle
}
fun getGradeFromBundle(bundle: Bundle?) = Grade(
grade = bundle?.getInt("grade") ?: 0,
note = bundle?.getString("note") ?: "",
verbal = bundle?.getBoolean("verbal") ?: false,
time = bundle?.getLong("time") ?: 0L,
weighting = bundle?.getInt("weighting") ?: 0,
examTime = bundle?.getLong("examTime") ?: 0L,
gradeId = bundle?.getInt("gradeId") ?: 0,
courseId = bundle?.getInt("courseId") ?: 0
)
fun putNoteInBundle(fragment: Fragment, note: Note) {
val bundle = Bundle(5).apply {
putString("content", note.content)
putBoolean("dismissed", note.dismissed)
putLong("time", note.time)
putString("category", note.category)
putInt("noteId", note.noteId)
}
fragment.arguments = bundle
}
fun getNoteFromBundle(bundle: Bundle?) = Note(
content = bundle?.getString("content") ?: "",
dismissed = bundle?.getBoolean("dismissed") ?: false,
time = bundle?.getLong("time") ?: 0L,
category = bundle?.getString("category") ?: "",
noteId = bundle?.getInt("noteId") ?: 0
)
fun getCourseExamAsCourse(c: CourseExam): Course = Course(
name = c.name,
advanced = c.advanced,
icon = c.icon,
colour = c.colour,
average = c.average,
oralWeighting = c.oralWeighting,
gradeCount = c.gradeCount,
time = c.time,
courseId = c.courseId,
yearId = c.yearId
)
}
@@ -0,0 +1,10 @@
package com.denizd.qwark.viewmodel
import androidx.lifecycle.ViewModel
// workaround to preserve Fragment with constructor on rotation change
class ConfirmDeletionViewModel : ViewModel() {
var confirmedFunction: () -> Unit = {}
var textBody = ""
}
@@ -0,0 +1,32 @@
package com.denizd.qwark.viewmodel
import android.app.Application
import androidx.lifecycle.LiveData
import com.denizd.qwark.database.CourseRepository
import com.denizd.qwark.model.Course
import com.denizd.qwark.model.CourseExam
import com.denizd.qwark.model.HistoricalAvg
import com.denizd.qwark.util.QwarkPreferences
class CourseViewModel(application: Application) : QwarkViewModel(application) {
private val repo = CourseRepository(application)
private val prefs: QwarkPreferences = QwarkPreferences.getPrefs(application)
val allCourses: LiveData<List<CourseExam>> = repo.getCoursesByYearId(getSchoolYear())
fun getShowGradeAverage(): Boolean = prefs.getShowGradeAverage()
fun getGradeType(): Int = prefs.getGradeType()
fun getCourseSortType(): Int = prefs.getCourseSortType()
fun getSchoolYear(): Int = prefs.getSchoolYear()
fun getSchoolYearName(): String = prefs.getSchoolYearName()
fun insert(avg: HistoricalAvg) = doAsync { repo.insert(avg) }
fun getAveragesForCourses(courseId: Int): List<HistoricalAvg> = returnBlocking {
repo.getAveragesForCourses(courseId)
}
fun insert(course: Course) = doAsync { repo.insert(course) }
fun update(course: Course) = doAsync { repo.update(course) }
fun delete(course: Course) = doAsync { repo.delete(course) }
}
@@ -0,0 +1,117 @@
package com.denizd.qwark.viewmodel
import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.denizd.qwark.database.FinalsRepository
import com.denizd.qwark.model.CourseExam
import com.denizd.qwark.model.FinalGrade
import com.denizd.qwark.model.SchoolYear
import kotlinx.coroutines.*
import java.lang.Exception
import java.lang.NumberFormatException
class FinalsViewModel(app: Application) : QwarkViewModel(app) {
private val repo = FinalsRepository(app)
val allFinalGrades: LiveData<List<FinalGrade>>? = repo.getFinalGrades(repo.getFinalScoreId())
fun update(finalGrade: FinalGrade) = viewModelScope.launch {
withContext(Dispatchers.IO) {
repo.update(finalGrade)
}
}
fun getSchoolYearName(courseId: Int) = returnBlocking { repo.getSchoolYearName(courseId) }
fun getScoreProfileName(): String = repo.getScoreProfileName()
fun getGradeType(): Int = repo.getGradeType()
fun getFinalScore(basicGrades: List<FinalGrade>, advancedGrades: List<FinalGrade>, examGrades: List<FinalGrade>): Int = try {
var finalScore = 0
for (grade in basicGrades) {
finalScore += checkGrade(grade)
}
for (i in 0..2) {
finalScore += (checkGrade(advancedGrades[i]) * 2)
}
for (i in 4..6) {
finalScore += (checkGrade(advancedGrades[i]) * 2)
}
finalScore += checkGrade(advancedGrades[3]) + checkGrade(advancedGrades[7])
for (grade in examGrades) {
finalScore += if (grade.note == "P5 x 2") {
(checkGrade(grade) * 2)
} else {
(checkGrade(grade) * 5)
}
}
finalScore
} catch (e: Exception) {
-1
}
private fun checkGrade(finalGrade: FinalGrade): Int = if (finalGrade.grade.toInt() > -1) finalGrade.grade.toInt() else throw NumberFormatException()
fun getFinalGrade(points: Int): Double = when (points) {
in 823..900 -> 1.0
in 805..822 -> 1.1
in 787..804 -> 1.2
in 769..786 -> 1.3
in 751..768 -> 1.4
in 733..750 -> 1.5
in 715..732 -> 1.6
in 697..714 -> 1.7
in 679..696 -> 1.8
in 661..678 -> 1.9
in 643..660 -> 2.0
in 625..642 -> 2.1
in 607..624 -> 2.2
in 589..606 -> 2.3
in 571..588 -> 2.4
in 553..570 -> 2.5
in 535..552 -> 2.6
in 517..534 -> 2.7
in 499..516 -> 2.8
in 481..498 -> 2.9
in 463..480 -> 3.0
in 445..462 -> 3.1
in 427..444 -> 3.2
in 409..426 -> 3.3
in 391..408 -> 3.4
in 373..390 -> 3.5
in 355..372 -> 3.6
in 337..354 -> 3.7
in 319..336 -> 3.8
in 301..318 -> 3.9
else -> 4.0
}
// Database
fun getAllYears(): List<SchoolYear> = returnBlocking { repo.getAllYears() }
fun getAllCourses(): List<CourseExam> = returnBlocking { repo.getAllCourses() }
fun updateFinalGradeAverage(courseId: Int, finalGradeId: Int) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
repo.updateFinalGradeAverage(courseId, finalGradeId)
}
}
}
fun updateLinkedGrades(attachObserver: () -> Unit) {
viewModelScope.launch {
withContext(Dispatchers.IO) { repo.updateLinkedGrades() }
withContext(Dispatchers.Main) { attachObserver() }
}
}
fun getCourse(courseId: Int) = runBlocking {
withContext(Dispatchers.IO) {
repo.getCourse(courseId)
}
}
fun getCourseSortType() = repo.getCourseSortType()
}
@@ -0,0 +1,28 @@
package com.denizd.qwark.viewmodel
import android.app.Application
import androidx.lifecycle.LiveData
import com.denizd.qwark.database.GradeRepository
import com.denizd.qwark.model.Grade
import com.denizd.qwark.model.HistoricalAvg
class GradeViewModel(application: Application) : QwarkViewModel(application) {
private val repo = GradeRepository(application)
fun getGradeType(): Int = repo.getGradeType()
fun getCourse(courseId: Int) = returnBlocking { repo.getCourse(courseId) }
fun getGradesByForeignKey(courseId: Int): LiveData<List<Grade>> = returnBlocking {
repo.getGradesByForeignKey(courseId)
}
fun insert(grade: Grade) = doAsync { repo.insert(grade) }
fun update(grade: Grade) = doAsync { repo.update(grade) }
fun updateAverage(average: String, gradeCount: Int, courseId: Int) = doAsync {
repo.updateAverage(average, gradeCount, courseId)
}
fun delete(gradeId: Int) = doAsync { repo.delete(gradeId) }
fun insert(avg: HistoricalAvg) = doAsync { repo.insert(avg) }
}

Some files were not shown because too many files have changed in this diff Show More