commit bbec19e79c5f13b3588947840cb6edd8d0823ace Author: Deniz Düzgören Date: Sun Feb 16 12:58:42 2020 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72f4d98 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.gradle/6.1-rc-1/executionHistory/executionHistory.bin b/.gradle/6.1-rc-1/executionHistory/executionHistory.bin new file mode 100644 index 0000000..8efa01c Binary files /dev/null and b/.gradle/6.1-rc-1/executionHistory/executionHistory.bin differ diff --git a/.gradle/6.1-rc-1/executionHistory/executionHistory.lock b/.gradle/6.1-rc-1/executionHistory/executionHistory.lock new file mode 100644 index 0000000..fe2d54b Binary files /dev/null and b/.gradle/6.1-rc-1/executionHistory/executionHistory.lock differ diff --git a/.gradle/6.1-rc-1/fileChanges/last-build.bin b/.gradle/6.1-rc-1/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/.gradle/6.1-rc-1/fileChanges/last-build.bin differ diff --git a/.gradle/6.1-rc-1/fileContent/fileContent.lock b/.gradle/6.1-rc-1/fileContent/fileContent.lock new file mode 100644 index 0000000..2784308 Binary files /dev/null and b/.gradle/6.1-rc-1/fileContent/fileContent.lock differ diff --git a/.gradle/6.1-rc-1/fileHashes/fileHashes.bin b/.gradle/6.1-rc-1/fileHashes/fileHashes.bin new file mode 100644 index 0000000..82e0c64 Binary files /dev/null and b/.gradle/6.1-rc-1/fileHashes/fileHashes.bin differ diff --git a/.gradle/6.1-rc-1/fileHashes/fileHashes.lock b/.gradle/6.1-rc-1/fileHashes/fileHashes.lock new file mode 100644 index 0000000..8096c0d Binary files /dev/null and b/.gradle/6.1-rc-1/fileHashes/fileHashes.lock differ diff --git a/.gradle/6.1-rc-1/fileHashes/resourceHashesCache.bin b/.gradle/6.1-rc-1/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000..8e3b9eb Binary files /dev/null and b/.gradle/6.1-rc-1/fileHashes/resourceHashesCache.bin differ diff --git a/.gradle/6.1-rc-1/gc.properties b/.gradle/6.1-rc-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle/6.1-rc-1/javaCompile/classAnalysis.bin b/.gradle/6.1-rc-1/javaCompile/classAnalysis.bin new file mode 100644 index 0000000..9c51123 Binary files /dev/null and b/.gradle/6.1-rc-1/javaCompile/classAnalysis.bin differ diff --git a/.gradle/6.1-rc-1/javaCompile/jarAnalysis.bin b/.gradle/6.1-rc-1/javaCompile/jarAnalysis.bin new file mode 100644 index 0000000..69183ff Binary files /dev/null and b/.gradle/6.1-rc-1/javaCompile/jarAnalysis.bin differ diff --git a/.gradle/6.1-rc-1/javaCompile/javaCompile.lock b/.gradle/6.1-rc-1/javaCompile/javaCompile.lock new file mode 100644 index 0000000..6e40d03 Binary files /dev/null and b/.gradle/6.1-rc-1/javaCompile/javaCompile.lock differ diff --git a/.gradle/6.1-rc-1/javaCompile/taskHistory.bin b/.gradle/6.1-rc-1/javaCompile/taskHistory.bin new file mode 100644 index 0000000..d0975b0 Binary files /dev/null and b/.gradle/6.1-rc-1/javaCompile/taskHistory.bin differ diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..ae3034d Binary files /dev/null and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..548c62e --- /dev/null +++ b/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Wed Feb 12 10:01:56 CET 2020 +gradle.version=6.1-rc-1 diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 0000000..983f722 Binary files /dev/null and b/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/.gradle/checksums/checksums.lock b/.gradle/checksums/checksums.lock new file mode 100644 index 0000000..f3ca017 Binary files /dev/null and b/.gradle/checksums/checksums.lock differ diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.idea/codeStyles b/.idea/codeStyles new file mode 100644 index 0000000..e2e415d --- /dev/null +++ b/.idea/codeStyles @@ -0,0 +1,119 @@ + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7bfef59 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5f05c51 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/Qwark.iml b/.idea/modules/Qwark.iml new file mode 100644 index 0000000..cf4067c --- /dev/null +++ b/.idea/modules/Qwark.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/app/app.iml b/.idea/modules/app/app.iml new file mode 100644 index 0000000..491902c --- /dev/null +++ b/.idea/modules/app/app.iml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/Qwark.iml b/Qwark.iml new file mode 100644 index 0000000..02be2da --- /dev/null +++ b/Qwark.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 0000000..5a2412b --- /dev/null +++ b/app/app.iml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..a80f982 --- /dev/null +++ b/app/build.gradle @@ -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' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/release/app-release.apk b/app/release/app-release.apk new file mode 100644 index 0000000..afb97e0 Binary files /dev/null and b/app/release/app-release.apk differ diff --git a/app/release/output.json b/app/release/output.json new file mode 100644 index 0000000..c0b17df --- /dev/null +++ b/app/release/output.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0d2ba2b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..f25ef56 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/denizd/qwark/activity/MainActivity.kt b/app/src/main/java/com/denizd/qwark/activity/MainActivity.kt new file mode 100644 index 0000000..826511f --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/activity/MainActivity.kt @@ -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(R.id.app_bar_layout) + toolbarTitle = findViewById(R.id.toolbarTxt) + bottomNavigationView = findViewById(R.id.bottom_nav) + + findViewById(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(R.id.view).visibility = View.INVISIBLE + super.onBackPressed() + } + "NoteFragment" -> { + if ((supportFragmentManager.fragments[0] as NoteFragment).onBackPressed()) { + super.onBackPressed() + } + } + else -> { + bottomNavigationView.visibility = View.VISIBLE + findViewById(R.id.view).visibility = View.VISIBLE + super.onBackPressed() + } + } + } +} diff --git a/app/src/main/java/com/denizd/qwark/adapter/CourseAdapter.kt b/app/src/main/java/com/denizd/qwark/adapter/CourseAdapter.kt new file mode 100644 index 0000000..7a04595 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/adapter/CourseAdapter.kt @@ -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, + private val onClickListener: CourseClickListener, + private val gradeType: Int?, + private val isParticipationFragment: Boolean = false) : RecyclerView.Adapter() { + + 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) { + this.courses = courses + notifyDataSetChanged() + } + + interface CourseClickListener { + fun onCourseClick(course: CourseExam) + fun onCourseLongClick(course: CourseExam) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/adapter/FinalGradeAdapter.kt b/app/src/main/java/com/denizd/qwark/adapter/FinalGradeAdapter.kt new file mode 100644 index 0000000..0c2d11a --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/adapter/FinalGradeAdapter.kt @@ -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, private val onClickListener: FinalGradeClickListener) : RecyclerView.Adapter() { + + 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) { + this.finalGrades = finalGrades + notifyDataSetChanged() + } + + internal interface FinalGradeClickListener { + fun onFinalGradeClick(finalGrade: FinalGrade) +// fun onFinalGradeLongClick(finalGradeId: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/adapter/GradeAdapter.kt b/app/src/main/java/com/denizd/qwark/adapter/GradeAdapter.kt new file mode 100644 index 0000000..4ce9c04 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/adapter/GradeAdapter.kt @@ -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, private val onClickListener: GradeClickListener, private val gradeType: Int) : RecyclerView.Adapter() { + + 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) { + this.grades = grades + notifyDataSetChanged() + } + + interface GradeClickListener { + fun onGradeClick(grade: Grade) + fun onGradeLongClick(gradeId: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/adapter/HistoricalAverageAdapter.kt b/app/src/main/java/com/denizd/qwark/adapter/HistoricalAverageAdapter.kt new file mode 100644 index 0000000..46f544d --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/adapter/HistoricalAverageAdapter.kt @@ -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?) : RecyclerView.Adapter() { + + 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) { + this.averages = averages + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/adapter/IconAdapter.kt b/app/src/main/java/com/denizd/qwark/adapter/IconAdapter.kt new file mode 100644 index 0000000..9616113 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/adapter/IconAdapter.kt @@ -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, private val onClickListener: IconClickListener) : RecyclerView.Adapter() { + + 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) { + this.icons = icons + notifyDataSetChanged() + } + + fun setSelectedItem(position: Int) { + selectedItem = position + notifyDataSetChanged() + } + + internal interface IconClickListener { + fun onIconClick(key: String, value: Int, position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/adapter/NoteAdapter.kt b/app/src/main/java/com/denizd/qwark/adapter/NoteAdapter.kt new file mode 100644 index 0000000..f4205d0 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/adapter/NoteAdapter.kt @@ -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, private val onClickListener: OnNoteClickListener) : RecyclerView.Adapter() { + + 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) { + this.notes = notes + notifyDataSetChanged() + } + + internal interface OnNoteClickListener { + fun onNoteClick(note: Note) + fun onNoteLongClick(note: Note) + fun onCategoryChipClick(category: String) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/adapter/ParticipationAdapter.kt b/app/src/main/java/com/denizd/qwark/adapter/ParticipationAdapter.kt new file mode 100644 index 0000000..e082df9 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/adapter/ParticipationAdapter.kt @@ -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, private val onClickListener: ParticipationClickListener) : RecyclerView.Adapter() { + + 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) { + this.participations = participations + notifyDataSetChanged() + } + + interface ParticipationClickListener { + fun onParticipationClick(participation: Participation) + fun onParticipationLongClick(participation: Participation) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/adapter/SchoolDayAdapter.kt b/app/src/main/java/com/denizd/qwark/adapter/SchoolDayAdapter.kt new file mode 100644 index 0000000..74ee7a6 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/adapter/SchoolDayAdapter.kt @@ -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, private val onClickListener: DayClickListener) : RecyclerView.Adapter() { + + 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) { + this.days = days + notifyDataSetChanged() + } + + private val dayMap: Map = mapOf( + "mon" to 0, + "tue" to 1, + "wed" to 2, + "thu" to 3, + "fri" to 4 + ) + + interface DayClickListener { + fun onDayClick(day: SchoolDay) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/adapter/YearAdapter.kt b/app/src/main/java/com/denizd/qwark/adapter/YearAdapter.kt new file mode 100644 index 0000000..df02a55 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/adapter/YearAdapter.kt @@ -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, private val onClickListener: YearClickListener) : RecyclerView.Adapter() { + + 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) { + this.schoolYears = schoolYears + notifyDataSetChanged() + } + + internal interface YearClickListener { + fun onYearClick(schoolYear: SchoolYear) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/database/CourseRepository.kt b/app/src/main/java/com/denizd/qwark/database/CourseRepository.kt new file mode 100644 index 0000000..b4fb4c7 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/database/CourseRepository.kt @@ -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> = dao.allCourses +// val allHistoricalAverages: LiveData> = 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> = dao.getCoursesByYearId(yearId) + fun getAveragesForCourses(courseId: Int): List = dao.getAveragesForCourses(courseId) +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/database/FinalsRepository.kt b/app/src/main/java/com/denizd/qwark/database/FinalsRepository.kt new file mode 100644 index 0000000..ec8b152 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/database/FinalsRepository.kt @@ -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> = 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) } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/database/GradeRepository.kt b/app/src/main/java/com/denizd/qwark/database/GradeRepository.kt new file mode 100644 index 0000000..a8cb9c0 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/database/GradeRepository.kt @@ -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> = dao.getGradesByForeignKey(courseId) +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/database/NoteRepository.kt b/app/src/main/java/com/denizd/qwark/database/NoteRepository.kt new file mode 100644 index 0000000..f9f4db9 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/database/NoteRepository.kt @@ -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> = dao.allNotes + + fun getNotesForCategory(category: String): LiveData> = dao.notesForCategory(category) + fun getAllCategories(): List = 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) } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/database/ParticipationRepository.kt b/app/src/main/java/com/denizd/qwark/database/ParticipationRepository.kt new file mode 100644 index 0000000..ce6610b --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/database/ParticipationRepository.kt @@ -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 = 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> = dao.getCoursesByDayType(getDayType(), prefs.getSchoolYear()) + + fun initParticipationTables() { + for (day in days) { + dao.insert(SchoolDay(day, 0, prefs.getSchoolYear())) + } + } + + fun getParticipationCount(participations: List) = 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 = dao.getDays(prefs.getSchoolYear()) + fun getCourses(): List = 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> = + 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() +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/database/QwarkClient.kt b/app/src/main/java/com/denizd/qwark/database/QwarkClient.kt new file mode 100644 index 0000000..25b191f --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/database/QwarkClient.kt @@ -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.prepare(): String { + var result = "" + forEach { note -> + result += "$${note.content}^${note.dismissed}^${note.time}^${note.category}^${note.noteId}|" + } + result += '\\' + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/database/QwarkDao.kt b/app/src/main/java/com/denizd/qwark/database/QwarkDao.kt new file mode 100644 index 0000000..341a0ad --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/database/QwarkDao.kt @@ -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> + + @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 + + @Query("SELECT *, -1 AS exam_time FROM course WHERE course_id != :u ORDER BY time") + fun getAllCourses(u: Int = -1): List // 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> + + @Query("SELECT *, -1 AS exam_time FROM course WHERE year_id = :yearId ORDER BY time") + fun getCoursesByYearIdAsList(yearId: Int): List + + @Query("SELECT *, -1 AS exam_time FROM course WHERE course_id = :courseId") + fun getCourse(courseId: Int): List + + @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> + + @Query("SELECT * FROM grade WHERE course_id = :courseId ORDER BY time") + fun getGradesByForeignKey(courseId: Int): LiveData> + + @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> + + @Query("SELECT * FROM note ORDER BY time DESC") + fun getNotesAsList(): List + + @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> + + @Update + fun update(note: Note) + + @Query("SELECT DISTINCT category FROM note") + fun getAllCategories(): List + + + // historical averages + @get:Query("SELECT * FROM historical_avg ORDER BY course_id DESC") + val allHistoricalAverages: LiveData> + + @Insert + fun insert(historicalAvg: HistoricalAvg) + + @Query("SELECT * FROM historical_avg WHERE course_id = :courseId ORDER BY time DESC") + fun getAveragesForCourses(courseId: Int): List + + + // years + @get:Query("SELECT * FROM school_year ORDER BY year_id") + val allYears: LiveData> + + @Query("SELECT * FROM school_year WHERE year_id != :u ORDER BY year_id") + fun getAllYears(u: Int = -1): List + + @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> + + @Query("SELECT * FROM score_profile WHERE score_profile_id = :scoreProfileId") + fun getScoreProfileNames(scoreProfileId: Int): List + + @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> + + @Query("SELECT * FROM final_grade WHERE score_profile_id = :scoreProfileId ORDER BY final_grade_id") + fun getFinalGradesAsList(scoreProfileId: Int): List + + @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) + + + // 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> + + @Query("SELECT * FROM school_day WHERE year_id = :yearId") + fun getDays(yearId: Int): List + + @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> + + @Insert + fun insert(participation: Participation) + + @Delete + fun delete(participation: Participation) +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/database/QwarkDatabase.kt b/app/src/main/java/com/denizd/qwark/database/QwarkDatabase.kt new file mode 100644 index 0000000..f7e1bef --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/database/QwarkDatabase.kt @@ -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!! + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/database/SettingsRepository.kt b/app/src/main/java/com/denizd/qwark/database/SettingsRepository.kt new file mode 100644 index 0000000..1958961 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/database/SettingsRepository.kt @@ -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> = dao.allYears + val allScoreProfiles: LiveData> = 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() + 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 + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/fragment/CourseFragment.kt b/app/src/main/java/com/denizd/qwark/fragment/CourseFragment.kt new file mode 100644 index 0000000..470596c --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/fragment/CourseFragment.kt @@ -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) + } + ) + } +} diff --git a/app/src/main/java/com/denizd/qwark/fragment/FinalsFragment.kt b/app/src/main/java/com/denizd/qwark/fragment/FinalsFragment.kt new file mode 100644 index 0000000..ce62084 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/fragment/FinalsFragment.kt @@ -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, view: View) { +// val dialogBuilder = MaterialAlertDialogBuilder(context) +// val v = View.inflate(context, R.layout.finals_overview, null) +// val l = v.findViewById(R.id.linear_layout) +// +// for (finalGrade in finalGrades) { +// val fv = View.inflate(context, R.layout.finals_overview_item, null) +// fv.findViewById(R.id.course).text = finalGrade.name +// fv.findViewById(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() +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/fragment/GradeFragment.kt b/app/src/main/java/com/denizd/qwark/fragment/GradeFragment.kt new file mode 100644 index 0000000..e769a3f --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/fragment/GradeFragment.kt @@ -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(R.id.bottom_nav).visibility = View.GONE + findViewById(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() +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/fragment/NoteFragment.kt b/app/src/main/java/com/denizd/qwark/fragment/NoteFragment.kt new file mode 100644 index 0000000..4948db5 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/fragment/NoteFragment.kt @@ -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> { 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 = viewModel.getAllCategories() +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/fragment/ParticipationCourseFragment.kt b/app/src/main/java/com/denizd/qwark/fragment/ParticipationCourseFragment.kt new file mode 100644 index 0000000..8109c3c --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/fragment/ParticipationCourseFragment.kt @@ -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(R.id.bottom_nav).visibility = View.GONE + findViewById(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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/fragment/ParticipationFragment.kt b/app/src/main/java/com/denizd/qwark/fragment/ParticipationFragment.kt new file mode 100644 index 0000000..93b7096 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/fragment/ParticipationFragment.kt @@ -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 = viewModel.getCourses() + fun link(courseId: Int) { viewModel.link(courseId) } + fun delete(participation: Participation) { viewModel.delete(participation) } + fun getSchoolYearName() = viewModel.getSchoolYearName() +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/fragment/QwarkFragment.kt b/app/src/main/java/com/denizd/qwark/fragment/QwarkFragment.kt new file mode 100644 index 0000000..803f12c --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/fragment/QwarkFragment.kt @@ -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.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() +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/fragment/SettingsFragment.kt b/app/src/main/java/com/denizd/qwark/fragment/SettingsFragment.kt new file mode 100644 index 0000000..e2adf9c --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/fragment/SettingsFragment.kt @@ -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( + 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( + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/model/Course.kt b/app/src/main/java/com/denizd/qwark/model/Course.kt new file mode 100644 index 0000000..36f4d31 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/Course.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/model/CourseDayRelation.kt b/app/src/main/java/com/denizd/qwark/model/CourseDayRelation.kt new file mode 100644 index 0000000..7d50a43 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/CourseDayRelation.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/model/CourseExam.kt b/app/src/main/java/com/denizd/qwark/model/CourseExam.kt new file mode 100644 index 0000000..69fd2b5 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/CourseExam.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/model/CourseIcon.kt b/app/src/main/java/com/denizd/qwark/model/CourseIcon.kt new file mode 100644 index 0000000..dd1c7df --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/CourseIcon.kt @@ -0,0 +1,3 @@ +package com.denizd.qwark.model + +data class CourseIcon(val key: String, val value: Int) \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/model/FinalGrade.kt b/app/src/main/java/com/denizd/qwark/model/FinalGrade.kt new file mode 100644 index 0000000..1a2cee6 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/FinalGrade.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/model/Grade.kt b/app/src/main/java/com/denizd/qwark/model/Grade.kt new file mode 100644 index 0000000..592e966 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/Grade.kt @@ -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 +) diff --git a/app/src/main/java/com/denizd/qwark/model/HistoricalAvg.kt b/app/src/main/java/com/denizd/qwark/model/HistoricalAvg.kt new file mode 100644 index 0000000..d3d5f9e --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/HistoricalAvg.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/model/Note.kt b/app/src/main/java/com/denizd/qwark/model/Note.kt new file mode 100644 index 0000000..c0c2409 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/Note.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/model/Participation.kt b/app/src/main/java/com/denizd/qwark/model/Participation.kt new file mode 100644 index 0000000..b59c69a --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/Participation.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/model/SchoolDay.kt b/app/src/main/java/com/denizd/qwark/model/SchoolDay.kt new file mode 100644 index 0000000..61cdfab --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/SchoolDay.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/model/SchoolYear.kt b/app/src/main/java/com/denizd/qwark/model/SchoolYear.kt new file mode 100644 index 0000000..a07407d --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/SchoolYear.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/model/ScoreProfile.kt b/app/src/main/java/com/denizd/qwark/model/ScoreProfile.kt new file mode 100644 index 0000000..e237c16 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/model/ScoreProfile.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/ClassSessionSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/ClassSessionSheet.kt new file mode 100644 index 0000000..b64b2df --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/ClassSessionSheet.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/ConfirmDeletionBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/ConfirmDeletionBottomSheet.kt new file mode 100644 index 0000000..cfc5538 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/ConfirmDeletionBottomSheet.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/CopyCoursesBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/CopyCoursesBottomSheet.kt new file mode 100644 index 0000000..680b4c6 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/CopyCoursesBottomSheet.kt @@ -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() + } +} diff --git a/app/src/main/java/com/denizd/qwark/sheet/CourseCreateBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/CourseCreateBottomSheet.kt new file mode 100644 index 0000000..d1d7537 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/CourseCreateBottomSheet.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/CourseOptionsBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/CourseOptionsBottomSheet.kt new file mode 100644 index 0000000..2e184c8 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/CourseOptionsBottomSheet.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/FinalsCreateBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/FinalsCreateBottomSheet.kt new file mode 100644 index 0000000..5fe687b --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/FinalsCreateBottomSheet.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/FinalsLinkBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/FinalsLinkBottomSheet.kt new file mode 100644 index 0000000..870f978 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/FinalsLinkBottomSheet.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/FinalsOptionsBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/FinalsOptionsBottomSheet.kt new file mode 100644 index 0000000..24b8484 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/FinalsOptionsBottomSheet.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/GradeCreateBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/GradeCreateBottomSheet.kt new file mode 100644 index 0000000..1be56df --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/GradeCreateBottomSheet.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/HistoricalAverageBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/HistoricalAverageBottomSheet.kt new file mode 100644 index 0000000..077cae6 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/HistoricalAverageBottomSheet.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/IconPickerBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/IconPickerBottomSheet.kt new file mode 100644 index 0000000..0e10b30 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/IconPickerBottomSheet.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/NoteCreateBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/NoteCreateBottomSheet.kt new file mode 100644 index 0000000..00114b2 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/NoteCreateBottomSheet.kt @@ -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(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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/NoteDisplayBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/NoteDisplayBottomSheet.kt new file mode 100644 index 0000000..6a059af --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/NoteDisplayBottomSheet.kt @@ -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( +// 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/NoteOptionsBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/NoteOptionsBottomSheet.kt new file mode 100644 index 0000000..2b4e122 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/NoteOptionsBottomSheet.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/ParticipationLinkBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/ParticipationLinkBottomSheet.kt new file mode 100644 index 0000000..091b6a4 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/ParticipationLinkBottomSheet.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/ScoreProfileCreateBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/ScoreProfileCreateBottomSheet.kt new file mode 100644 index 0000000..cb81ca1 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/ScoreProfileCreateBottomSheet.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/TextInputBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/TextInputBottomSheet.kt new file mode 100644 index 0000000..cab722c --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/TextInputBottomSheet.kt @@ -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() } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/sheet/YearCreateBottomSheet.kt b/app/src/main/java/com/denizd/qwark/sheet/YearCreateBottomSheet.kt new file mode 100644 index 0000000..bb7bf34 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/sheet/YearCreateBottomSheet.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/util/GradeCalculations.kt b/app/src/main/java/com/denizd/qwark/util/GradeCalculations.kt new file mode 100644 index 0000000..72b49ac --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/util/GradeCalculations.kt @@ -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.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.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) +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/util/OnBackPressed.kt b/app/src/main/java/com/denizd/qwark/util/OnBackPressed.kt new file mode 100644 index 0000000..47d5841 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/util/OnBackPressed.kt @@ -0,0 +1,5 @@ +package com.denizd.qwark.util + +internal interface OnBackPressed { + fun onBackPressed(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/util/PreferenceKey.kt b/app/src/main/java/com/denizd/qwark/util/PreferenceKey.kt new file mode 100644 index 0000000..bf84ac0 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/util/PreferenceKey.kt @@ -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") +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/util/QwarkPreferences.kt b/app/src/main/java/com/denizd/qwark/util/QwarkPreferences.kt new file mode 100644 index 0000000..abcfa45 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/util/QwarkPreferences.kt @@ -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) } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/util/QwarkUtil.kt b/app/src/main/java/com/denizd/qwark/util/QwarkUtil.kt new file mode 100644 index 0000000..87b3741 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/util/QwarkUtil.kt @@ -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 by lazy { + getDrawablesAsList() + } + + private val drawables: Map = 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 = 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 = 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 = 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 = 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 = arrayOf( + "0", "1", "2", "3", "4", "5", "6", + "7", "8", "9", "10", "11", "12", "13", "14", "15" + ) + private val numberedGradeArray: Array = arrayOf( + "6", "5-", "5", "5+", "4-", "4", "4+", + "3-", "3", "3+", "2-", "2", "2+", "1-", "1", "1+" + ) + private val letteredGradeArray: Array = arrayOf( + "F", "E-", "E", "E+", "D-", "D", "D+", + "C-", "C", "C+", "B-", "B", "B+", "A-", "A", "A+" + ) + + fun getGradeArray(gradeType: Int): Array = when (gradeType) { + GRADE_NUMBERED -> numberedGradeArray + GRADE_LETTERED -> letteredGradeArray + else -> pointGradeArray + } + + private fun Double.getGradeType(map: Map): 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, sortType: Int): List = 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): CourseIcon { + return CourseIcon(entry.key, entry.value) + } + + private fun getDrawablesAsList(): ArrayList = ArrayList().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(R.id.info_title_textview).text = title + dialogView.findViewById(R.id.info_body_textview).text = body + val dialog = dialogBuilder.setView(dialogView).create() + dialogView.findViewById(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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/viewmodel/ConfirmDeletionViewModel.kt b/app/src/main/java/com/denizd/qwark/viewmodel/ConfirmDeletionViewModel.kt new file mode 100644 index 0000000..609dded --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/viewmodel/ConfirmDeletionViewModel.kt @@ -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 = "" +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/viewmodel/CourseViewModel.kt b/app/src/main/java/com/denizd/qwark/viewmodel/CourseViewModel.kt new file mode 100644 index 0000000..3f0df70 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/viewmodel/CourseViewModel.kt @@ -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> = 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 = 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) } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/viewmodel/FinalsViewModel.kt b/app/src/main/java/com/denizd/qwark/viewmodel/FinalsViewModel.kt new file mode 100644 index 0000000..6dcad27 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/viewmodel/FinalsViewModel.kt @@ -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>? = 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, advancedGrades: List, examGrades: List): 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 = returnBlocking { repo.getAllYears() } + + fun getAllCourses(): List = 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() +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/viewmodel/GradeViewModel.kt b/app/src/main/java/com/denizd/qwark/viewmodel/GradeViewModel.kt new file mode 100644 index 0000000..f6fc4ec --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/viewmodel/GradeViewModel.kt @@ -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> = 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) } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/viewmodel/MainViewModel.kt b/app/src/main/java/com/denizd/qwark/viewmodel/MainViewModel.kt new file mode 100644 index 0000000..c3a237d --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/viewmodel/MainViewModel.kt @@ -0,0 +1,60 @@ +package com.denizd.qwark.viewmodel + +import android.app.Application +import android.content.Context +import android.view.Window +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.ContextCompat +import androidx.lifecycle.AndroidViewModel +import androidx.preference.PreferenceManager +import com.denizd.qwark.R +import com.denizd.qwark.fragment.* +import com.denizd.qwark.fragment.CourseFragment +import com.denizd.qwark.fragment.FinalsFragment +import com.denizd.qwark.fragment.NoteFragment +import com.denizd.qwark.fragment.ParticipationFragment +import com.denizd.qwark.fragment.SettingsFragment +import com.denizd.qwark.util.QwarkUtil + +class MainViewModel(val app: Application) : AndroidViewModel(app) { + + private val prefs = PreferenceManager.getDefaultSharedPreferences(app.applicationContext) + + fun getFragmentType(type: Int): String = when (type) { // TODO replace with Fragment#tag + R.id.courses -> "CourseFragment" + R.id.notes -> "NoteFragment" + R.id.participation -> "ParticipationFragment" + R.id.finals -> "FinalsFragment" + else -> "SettingsFragment" + } + + fun getFragment(type: String): QwarkFragment = when (type) { + "CourseFragment" -> CourseFragment() + "NoteFragment" -> NoteFragment() + "ParticipationFragment" -> ParticipationFragment() + "FinalsFragment" -> FinalsFragment() + else -> SettingsFragment() + } + + fun applyTheme(window: Window, context: Context) { + when (prefs.getInt("dark_mode", 2)) { + 0 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + 1 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + else -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + } + val barColour = ContextCompat.getColor(context, R.color.colorBackground) + window.navigationBarColor = barColour + window.statusBarColor = barColour + } + + fun checkForFirstTimeYearNotice(context: Context) { + if (prefs.getBoolean("first_launch", true)) { + QwarkUtil.createInfoDialog( + context, + app.getString(R.string.alert_create_school_year_title), + app.getString(R.string.alert_create_school_year_desc) + ) + prefs.edit().putBoolean("first_launch", false).apply() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/viewmodel/NoteViewModel.kt b/app/src/main/java/com/denizd/qwark/viewmodel/NoteViewModel.kt new file mode 100644 index 0000000..7a9b450 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/viewmodel/NoteViewModel.kt @@ -0,0 +1,78 @@ +package com.denizd.qwark.viewmodel + +import android.app.Application +import androidx.lifecycle.* +import com.denizd.qwark.database.NoteRepository +import com.denizd.qwark.model.Note +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +class NoteViewModel(application: Application) : AndroidViewModel(application) { + + private val repo = NoteRepository(application) + private val reloadTrigger = MutableLiveData() + val allNotes: LiveData>? = Transformations.switchMap(reloadTrigger) { category -> + if (category == "") { + repo.allNotes + } else { + repo.getNotesForCategory(category) + } + } + + init { + refresh("") + } + + fun refresh(category: String) { + reloadTrigger.value = category + } + + fun insert(note: Note) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + repo.insert(note) + } + } + } + + fun updateDismissedStatus(newDismissedStatus: Boolean, noteId: Int) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + repo.updateDismissed(newDismissedStatus, noteId) + } + } + } + + fun delete(noteId: Int) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + repo.delete(noteId) + } + } + } + +// // TODO get this as LiveData variable to get changes +// fun getNotesForCategory(category: String): LiveData>? { +// return runBlocking { +// withContext(Dispatchers.IO) { +// repo.notesForCategory(category) +// } +// } +// } + + fun update(note: Note) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + repo.update(note) + } + } + } + + fun getAllCategories(): List = runBlocking { + withContext(Dispatchers.IO) { + repo.getAllCategories() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/viewmodel/ParticipationCourseViewModel.kt b/app/src/main/java/com/denizd/qwark/viewmodel/ParticipationCourseViewModel.kt new file mode 100644 index 0000000..80716e3 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/viewmodel/ParticipationCourseViewModel.kt @@ -0,0 +1,22 @@ +package com.denizd.qwark.viewmodel + +import android.app.Application +import androidx.lifecycle.LiveData +import com.denizd.qwark.database.ParticipationRepository +import com.denizd.qwark.model.Participation + +class ParticipationCourseViewModel(app: Application) : QwarkViewModel(app) { + + private val repo = ParticipationRepository(app) + + fun getAllParticipationsForCourse(courseId: Int): LiveData> = returnBlocking { + repo.getAllParticipationsForCourse(courseId) + } + fun getParticipationCount(participations: List) = repo.getParticipationCount(participations) + fun updateParticipation(participation: String, courseId: Int) = doAsync { repo.updateParticipation(participation, courseId) } + fun createParticipation(timesHandRaised: Int, timesSpoken: Int, time: Long, courseId: Int) = doAsync { + repo.insert(timesHandRaised, timesSpoken, time, courseId) + } + fun getSchoolYearName() = repo.getSchoolYearName() + fun delete(participation: Participation) = doAsync { repo.delete(participation) } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/viewmodel/ParticipationViewModel.kt b/app/src/main/java/com/denizd/qwark/viewmodel/ParticipationViewModel.kt new file mode 100644 index 0000000..4f238fb --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/viewmodel/ParticipationViewModel.kt @@ -0,0 +1,22 @@ +package com.denizd.qwark.viewmodel + +import android.app.Application +import androidx.lifecycle.LiveData +import com.denizd.qwark.database.ParticipationRepository +import com.denizd.qwark.model.Course +import com.denizd.qwark.model.CourseExam +import com.denizd.qwark.model.Participation +import com.denizd.qwark.model.SchoolDay + +class ParticipationViewModel(app: Application) : QwarkViewModel(app) { + + private val repo = ParticipationRepository(app) + val allCoursesForToday: LiveData> = repo.allCoursesForToday + val dayId: Int = repo.dayId + + fun getCourses(): List = returnBlocking { repo.getCourses() } + fun link(courseId: Int) = doAsync { repo.link(courseId) } + fun removeLink(relationId: Int) = doAsync { repo.removeLink(relationId) } + fun delete(participation: Participation) = doAsync { repo.delete(participation) } + fun getSchoolYearName() = repo.getSchoolYearName() +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/viewmodel/QwarkViewModel.kt b/app/src/main/java/com/denizd/qwark/viewmodel/QwarkViewModel.kt new file mode 100644 index 0000000..a27222a --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/viewmodel/QwarkViewModel.kt @@ -0,0 +1,26 @@ +package com.denizd.qwark.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +open class QwarkViewModel(app: Application) : AndroidViewModel(app) { + + fun doAsync(function: () -> Unit) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + function() + } + } + } + + fun returnBlocking(function: () -> T): T = runBlocking { + withContext(Dispatchers.IO) { + function() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizd/qwark/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/denizd/qwark/viewmodel/SettingsViewModel.kt new file mode 100644 index 0000000..bdcab67 --- /dev/null +++ b/app/src/main/java/com/denizd/qwark/viewmodel/SettingsViewModel.kt @@ -0,0 +1,50 @@ +package com.denizd.qwark.viewmodel + +import android.app.Application +import androidx.lifecycle.LiveData +import com.denizd.qwark.database.QwarkClient +import com.denizd.qwark.util.QwarkPreferences +import com.denizd.qwark.database.SettingsRepository +import com.denizd.qwark.model.ScoreProfile +import com.denizd.qwark.model.SchoolYear + +class SettingsViewModel(private val app: Application) : QwarkViewModel(app) { + + private val repo = SettingsRepository(app) + val allYears: LiveData> = repo.allYears + val allScoreProfiles: LiveData> = repo.allScoreProfiles + private val prefs: QwarkPreferences = QwarkPreferences.getPrefs(app.applicationContext) + + // Preference getters + fun getAppTheme() = repo.getAppTheme() + fun getShowGradeAverage() = repo.getShowGradeAverage() + fun getCourseSortType(): Int = prefs.getCourseSortType() + fun getGradeType() = repo.getGradeType() + fun getScoreProfileName() = repo.getScoreProfileName() + fun getCurrentSchoolYear(): String = repo.getCurrentSchoolYear() + fun getCurrentSchoolYearId(): Int = repo.getCurrentSchoolYearId() + fun getParticipationDisplay(): Int = repo.getParticipationDisplay() + fun getParticipationDay(): Int = repo.getParticipationDay() + + // Preference setters + fun setAppTheme(value: Int) { repo.setAppTheme(value) } + fun setShowGradeAverage(value: Boolean) { repo.setShowGradeAverage(value) } + fun setCourseSortType(value: Int) { prefs.setCourseSortType(value) } + fun setGradeType(value: Int) { repo.setGradeType(value) } + fun setCurrentScoreProfile(scoreProfile: ScoreProfile) { repo.setCurrentScoreProfile(scoreProfile) } + fun updateSchoolYear(schoolYear: SchoolYear) { repo.setCurrentSchoolYear(schoolYear) } + fun setParticipationDisplay(value: Int) { repo.setParticipationDisplay(value) } + fun setParticipationDay(value: Int) { repo.setParticipationDay(value) } + + // Database functions + fun insert(schoolYear: SchoolYear) = doAsync { repo.insert(schoolYear) } + fun insert(scoreProfile: ScoreProfile) = doAsync { repo.insert(scoreProfile) } + fun initNewFinalScore(name: String) = doAsync { repo.initNewFinalScore(name) } + fun updateSchoolYear(name: String) = doAsync { repo.updateSchoolYear(name) } + fun updateScoreProfile(name: String) = doAsync { repo.updateScoreProfile(name) } + fun deleteSchoolYear() = doAsync { repo.deleteSchoolYear() } + fun deleteScoreProfile() = doAsync { repo.deleteScoreProfile() } + fun copyCourses(yearId: Int) = doAsync { repo.copyCourses(yearId) } + + fun send(update: () -> Unit) = doAsync { QwarkClient(app.applicationContext).send(update) } +} \ No newline at end of file diff --git a/app/src/main/res/anim/item_animation_fall_down.xml b/app/src/main/res/anim/item_animation_fall_down.xml new file mode 100644 index 0000000..801ea7a --- /dev/null +++ b/app/src/main/res/anim/item_animation_fall_down.xml @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/layout_animation_fall_down.xml b/app/src/main/res/anim/layout_animation_fall_down.xml new file mode 100644 index 0000000..6eb9447 --- /dev/null +++ b/app/src/main/res/anim/layout_animation_fall_down.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/color/bottom_nav_color.xml b/app/src/main/res/color/bottom_nav_color.xml new file mode 100644 index 0000000..992dad7 --- /dev/null +++ b/app/src/main/res/color/bottom_nav_color.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bolt.xml b/app/src/main/res/drawable/bolt.xml new file mode 100644 index 0000000..80ba978 --- /dev/null +++ b/app/src/main/res/drawable/bolt.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/calendar.xml b/app/src/main/res/drawable/calendar.xml new file mode 100644 index 0000000..0acda3d --- /dev/null +++ b/app/src/main/res/drawable/calendar.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/cogwheel.xml b/app/src/main/res/drawable/cogwheel.xml new file mode 100644 index 0000000..f4388a6 --- /dev/null +++ b/app/src/main/res/drawable/cogwheel.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/cross.xml b/app/src/main/res/drawable/cross.xml new file mode 100644 index 0000000..3cd7225 --- /dev/null +++ b/app/src/main/res/drawable/cross.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/delete.xml b/app/src/main/res/drawable/delete.xml new file mode 100644 index 0000000..7d06562 --- /dev/null +++ b/app/src/main/res/drawable/delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/edit.xml b/app/src/main/res/drawable/edit.xml new file mode 100644 index 0000000..9d00f0e --- /dev/null +++ b/app/src/main/res/drawable/edit.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/filter.xml b/app/src/main/res/drawable/filter.xml new file mode 100644 index 0000000..6e18fc9 --- /dev/null +++ b/app/src/main/res/drawable/filter.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/hand.xml b/app/src/main/res/drawable/hand.xml new file mode 100644 index 0000000..fe701f7 --- /dev/null +++ b/app/src/main/res/drawable/hand.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/history.xml b/app/src/main/res/drawable/history.xml new file mode 100644 index 0000000..f259e1c --- /dev/null +++ b/app/src/main/res/drawable/history.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arts.xml b/app/src/main/res/drawable/ic_arts.xml new file mode 100644 index 0000000..33f625e --- /dev/null +++ b/app/src/main/res/drawable/ic_arts.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_biology.xml b/app/src/main/res/drawable/ic_biology.xml new file mode 100644 index 0000000..b71545c --- /dev/null +++ b/app/src/main/res/drawable/ic_biology.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_chemistry.xml b/app/src/main/res/drawable/ic_chemistry.xml new file mode 100644 index 0000000..d4c52d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_chemistry.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_chinese.xml b/app/src/main/res/drawable/ic_chinese.xml new file mode 100644 index 0000000..30494ab --- /dev/null +++ b/app/src/main/res/drawable/ic_chinese.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_circle.xml b/app/src/main/res/drawable/ic_circle.xml new file mode 100644 index 0000000..f245661 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_cogwheel.xml b/app/src/main/res/drawable/ic_cogwheel.xml new file mode 100644 index 0000000..dc48532 --- /dev/null +++ b/app/src/main/res/drawable/ic_cogwheel.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_compsci.xml b/app/src/main/res/drawable/ic_compsci.xml new file mode 100644 index 0000000..11181fb --- /dev/null +++ b/app/src/main/res/drawable/ic_compsci.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_drama.xml b/app/src/main/res/drawable/ic_drama.xml new file mode 100644 index 0000000..bb0655c --- /dev/null +++ b/app/src/main/res/drawable/ic_drama.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_english.xml b/app/src/main/res/drawable/ic_english.xml new file mode 100644 index 0000000..380ec1a --- /dev/null +++ b/app/src/main/res/drawable/ic_english.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_french.xml b/app/src/main/res/drawable/ic_french.xml new file mode 100644 index 0000000..a7e28a0 --- /dev/null +++ b/app/src/main/res/drawable/ic_french.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_geography.xml b/app/src/main/res/drawable/ic_geography.xml new file mode 100644 index 0000000..37ba074 --- /dev/null +++ b/app/src/main/res/drawable/ic_geography.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_german.xml b/app/src/main/res/drawable/ic_german.xml new file mode 100644 index 0000000..f478e4d --- /dev/null +++ b/app/src/main/res/drawable/ic_german.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_gll.xml b/app/src/main/res/drawable/ic_gll.xml new file mode 100644 index 0000000..e446f8d --- /dev/null +++ b/app/src/main/res/drawable/ic_gll.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml new file mode 100644 index 0000000..799be2a --- /dev/null +++ b/app/src/main/res/drawable/ic_help.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 0000000..2d4fa89 --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_idea.xml b/app/src/main/res/drawable/ic_idea.xml new file mode 100644 index 0000000..27d1b91 --- /dev/null +++ b/app/src/main/res/drawable/ic_idea.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_information.xml b/app/src/main/res/drawable/ic_information.xml new file mode 100644 index 0000000..1a1b4d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_information.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_latin.xml b/app/src/main/res/drawable/ic_latin.xml new file mode 100644 index 0000000..83d61d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_latin.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_maths.xml b/app/src/main/res/drawable/ic_maths.xml new file mode 100644 index 0000000..661d21e --- /dev/null +++ b/app/src/main/res/drawable/ic_maths.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_met.xml b/app/src/main/res/drawable/ic_met.xml new file mode 100644 index 0000000..a93f775 --- /dev/null +++ b/app/src/main/res/drawable/ic_met.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_music.xml b/app/src/main/res/drawable/ic_music.xml new file mode 100644 index 0000000..3b8cfd5 --- /dev/null +++ b/app/src/main/res/drawable/ic_music.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_pe.xml b/app/src/main/res/drawable/ic_pe.xml new file mode 100644 index 0000000..b4c661c --- /dev/null +++ b/app/src/main/res/drawable/ic_pe.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_pencil.xml b/app/src/main/res/drawable/ic_pencil.xml new file mode 100644 index 0000000..3323522 --- /dev/null +++ b/app/src/main/res/drawable/ic_pencil.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_philosophy.xml b/app/src/main/res/drawable/ic_philosophy.xml new file mode 100644 index 0000000..7636111 --- /dev/null +++ b/app/src/main/res/drawable/ic_philosophy.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_physics.xml b/app/src/main/res/drawable/ic_physics.xml new file mode 100644 index 0000000..fe80a81 --- /dev/null +++ b/app/src/main/res/drawable/ic_physics.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_politics.xml b/app/src/main/res/drawable/ic_politics.xml new file mode 100644 index 0000000..f36990c --- /dev/null +++ b/app/src/main/res/drawable/ic_politics.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_religion.xml b/app/src/main/res/drawable/ic_religion.xml new file mode 100644 index 0000000..ea8cf27 --- /dev/null +++ b/app/src/main/res/drawable/ic_religion.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_spanish.xml b/app/src/main/res/drawable/ic_spanish.xml new file mode 100644 index 0000000..2767f6d --- /dev/null +++ b/app/src/main/res/drawable/ic_spanish.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_tomato.xml b/app/src/main/res/drawable/ic_tomato.xml new file mode 100644 index 0000000..1320cd3 --- /dev/null +++ b/app/src/main/res/drawable/ic_tomato.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_turkish.xml b/app/src/main/res/drawable/ic_turkish.xml new file mode 100644 index 0000000..aa17741 --- /dev/null +++ b/app/src/main/res/drawable/ic_turkish.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_wat.xml b/app/src/main/res/drawable/ic_wat.xml new file mode 100644 index 0000000..b877813 --- /dev/null +++ b/app/src/main/res/drawable/ic_wat.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/label.xml b/app/src/main/res/drawable/label.xml new file mode 100644 index 0000000..eb14988 --- /dev/null +++ b/app/src/main/res/drawable/label.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/label_dismissed.xml b/app/src/main/res/drawable/label_dismissed.xml new file mode 100644 index 0000000..5fac050 --- /dev/null +++ b/app/src/main/res/drawable/label_dismissed.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/launch_screen.xml b/app/src/main/res/drawable/launch_screen.xml new file mode 100644 index 0000000..3f0d793 --- /dev/null +++ b/app/src/main/res/drawable/launch_screen.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/link.xml b/app/src/main/res/drawable/link.xml new file mode 100644 index 0000000..203a346 --- /dev/null +++ b/app/src/main/res/drawable/link.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/list.xml b/app/src/main/res/drawable/list.xml new file mode 100644 index 0000000..3ba069e --- /dev/null +++ b/app/src/main/res/drawable/list.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/notepad.xml b/app/src/main/res/drawable/notepad.xml new file mode 100644 index 0000000..04f00d3 --- /dev/null +++ b/app/src/main/res/drawable/notepad.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/plus.xml b/app/src/main/res/drawable/plus.xml new file mode 100644 index 0000000..ae0b1da --- /dev/null +++ b/app/src/main/res/drawable/plus.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/speech.xml b/app/src/main/res/drawable/speech.xml new file mode 100644 index 0000000..f23e7d5 --- /dev/null +++ b/app/src/main/res/drawable/speech.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/font/manrope_bold.otf b/app/src/main/res/font/manrope_bold.otf new file mode 100644 index 0000000..f2e3bba Binary files /dev/null and b/app/src/main/res/font/manrope_bold.otf differ diff --git a/app/src/main/res/font/manrope_extrabold.ttf b/app/src/main/res/font/manrope_extrabold.ttf new file mode 100644 index 0000000..63388ff Binary files /dev/null and b/app/src/main/res/font/manrope_extrabold.ttf differ diff --git a/app/src/main/res/font/manrope_medium.otf b/app/src/main/res/font/manrope_medium.otf new file mode 100644 index 0000000..be87523 Binary files /dev/null and b/app/src/main/res/font/manrope_medium.otf differ diff --git a/app/src/main/res/font/manrope_regular.otf b/app/src/main/res/font/manrope_regular.otf new file mode 100644 index 0000000..d87c7b4 Binary files /dev/null and b/app/src/main/res/font/manrope_regular.otf differ diff --git a/app/src/main/res/font/manrope_semibold.otf b/app/src/main/res/font/manrope_semibold.otf new file mode 100644 index 0000000..e06b60f Binary files /dev/null and b/app/src/main/res/font/manrope_semibold.otf differ diff --git a/app/src/main/res/layout-land/finals_fragment.xml b/app/src/main/res/layout-land/finals_fragment.xml new file mode 100644 index 0000000..60280c6 --- /dev/null +++ b/app/src/main/res/layout-land/finals_fragment.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/grade_fragment.xml b/app/src/main/res/layout-land/grade_fragment.xml new file mode 100644 index 0000000..557675f --- /dev/null +++ b/app/src/main/res/layout-land/grade_fragment.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..9a94ad8 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/average_card.xml b/app/src/main/res/layout/average_card.xml new file mode 100644 index 0000000..deda6d1 --- /dev/null +++ b/app/src/main/res/layout/average_card.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/class_session_sheet.xml b/app/src/main/res/layout/class_session_sheet.xml new file mode 100644 index 0000000..edf0438 --- /dev/null +++ b/app/src/main/res/layout/class_session_sheet.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/confirmation_dialog.xml b/app/src/main/res/layout/confirmation_dialog.xml new file mode 100644 index 0000000..466ba4f --- /dev/null +++ b/app/src/main/res/layout/confirmation_dialog.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/course_card.xml b/app/src/main/res/layout/course_card.xml new file mode 100644 index 0000000..f0a0e1a --- /dev/null +++ b/app/src/main/res/layout/course_card.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/course_create_dialog.xml b/app/src/main/res/layout/course_create_dialog.xml new file mode 100644 index 0000000..47a87a0 --- /dev/null +++ b/app/src/main/res/layout/course_create_dialog.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/course_options_dialog.xml b/app/src/main/res/layout/course_options_dialog.xml new file mode 100644 index 0000000..647da69 --- /dev/null +++ b/app/src/main/res/layout/course_options_dialog.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dropdown_item.xml b/app/src/main/res/layout/dropdown_item.xml new file mode 100644 index 0000000..a68be01 --- /dev/null +++ b/app/src/main/res/layout/dropdown_item.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/final_grade_edit_dialog.xml b/app/src/main/res/layout/final_grade_edit_dialog.xml new file mode 100644 index 0000000..81439a4 --- /dev/null +++ b/app/src/main/res/layout/final_grade_edit_dialog.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/final_grade_item.xml b/app/src/main/res/layout/final_grade_item.xml new file mode 100644 index 0000000..38b4d84 --- /dev/null +++ b/app/src/main/res/layout/final_grade_item.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/final_grade_options_dialog.xml b/app/src/main/res/layout/final_grade_options_dialog.xml new file mode 100644 index 0000000..3242eae --- /dev/null +++ b/app/src/main/res/layout/final_grade_options_dialog.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/finals_fragment.xml b/app/src/main/res/layout/finals_fragment.xml new file mode 100644 index 0000000..7b6863a --- /dev/null +++ b/app/src/main/res/layout/finals_fragment.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/finals_overview.xml b/app/src/main/res/layout/finals_overview.xml new file mode 100644 index 0000000..888677e --- /dev/null +++ b/app/src/main/res/layout/finals_overview.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/finals_overview_item.xml b/app/src/main/res/layout/finals_overview_item.xml new file mode 100644 index 0000000..801527a --- /dev/null +++ b/app/src/main/res/layout/finals_overview_item.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/grade_card.xml b/app/src/main/res/layout/grade_card.xml new file mode 100644 index 0000000..08f1ade --- /dev/null +++ b/app/src/main/res/layout/grade_card.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/grade_create_dialog.xml b/app/src/main/res/layout/grade_create_dialog.xml new file mode 100644 index 0000000..cf7caca --- /dev/null +++ b/app/src/main/res/layout/grade_create_dialog.xml @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/grade_fragment.xml b/app/src/main/res/layout/grade_fragment.xml new file mode 100644 index 0000000..682f95e --- /dev/null +++ b/app/src/main/res/layout/grade_fragment.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/icon_dialog.xml b/app/src/main/res/layout/icon_dialog.xml new file mode 100644 index 0000000..80f4279 --- /dev/null +++ b/app/src/main/res/layout/icon_dialog.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/icon_item.xml b/app/src/main/res/layout/icon_item.xml new file mode 100644 index 0000000..1be586f --- /dev/null +++ b/app/src/main/res/layout/icon_item.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/info_display_dialog.xml b/app/src/main/res/layout/info_display_dialog.xml new file mode 100644 index 0000000..4e261f9 --- /dev/null +++ b/app/src/main/res/layout/info_display_dialog.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/note_bottom_sheet.xml b/app/src/main/res/layout/note_bottom_sheet.xml new file mode 100644 index 0000000..0b6838d --- /dev/null +++ b/app/src/main/res/layout/note_bottom_sheet.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/note_card.xml b/app/src/main/res/layout/note_card.xml new file mode 100644 index 0000000..c921b5c --- /dev/null +++ b/app/src/main/res/layout/note_card.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/note_create_dialog.xml b/app/src/main/res/layout/note_create_dialog.xml new file mode 100644 index 0000000..3e8492d --- /dev/null +++ b/app/src/main/res/layout/note_create_dialog.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/note_display_dialog.xml b/app/src/main/res/layout/note_display_dialog.xml new file mode 100644 index 0000000..eb612ba --- /dev/null +++ b/app/src/main/res/layout/note_display_dialog.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/note_options_dialog.xml b/app/src/main/res/layout/note_options_dialog.xml new file mode 100644 index 0000000..44b97d7 --- /dev/null +++ b/app/src/main/res/layout/note_options_dialog.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/padded_recycler_view.xml b/app/src/main/res/layout/padded_recycler_view.xml new file mode 100644 index 0000000..d6781d1 --- /dev/null +++ b/app/src/main/res/layout/padded_recycler_view.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/participation_card.xml b/app/src/main/res/layout/participation_card.xml new file mode 100644 index 0000000..b05a001 --- /dev/null +++ b/app/src/main/res/layout/participation_card.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/powered_by_qwark.xml b/app/src/main/res/layout/powered_by_qwark.xml new file mode 100644 index 0000000..ff0019c --- /dev/null +++ b/app/src/main/res/layout/powered_by_qwark.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recycler_dialog.xml b/app/src/main/res/layout/recycler_dialog.xml new file mode 100644 index 0000000..6176cd6 --- /dev/null +++ b/app/src/main/res/layout/recycler_dialog.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recycler_view.xml b/app/src/main/res/layout/recycler_view.xml new file mode 100644 index 0000000..28f3513 --- /dev/null +++ b/app/src/main/res/layout/recycler_view.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/score_profile_create_dialog.xml b/app/src/main/res/layout/score_profile_create_dialog.xml new file mode 100644 index 0000000..585a17b --- /dev/null +++ b/app/src/main/res/layout/score_profile_create_dialog.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml new file mode 100644 index 0000000..0701b2b --- /dev/null +++ b/app/src/main/res/layout/settings_fragment.xml @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/text_input_sheet.xml b/app/src/main/res/layout/text_input_sheet.xml new file mode 100644 index 0000000..1bad7f4 --- /dev/null +++ b/app/src/main/res/layout/text_input_sheet.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/year_card.xml b/app/src/main/res/layout/year_card.xml new file mode 100644 index 0000000..47bb7f6 --- /dev/null +++ b/app/src/main/res/layout/year_card.xml @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/nav_items.xml b/app/src/main/res/menu/nav_items.xml new file mode 100644 index 0000000..7281df3 --- /dev/null +++ b/app/src/main/res/menu/nav_items.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..809a656 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8503a5b Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..809a656 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..ed4cc00 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..b5a1375 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..ed4cc00 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..d644f07 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f8b6e71 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..d644f07 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..b384698 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..b807043 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b384698 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..37bc130 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..992972d Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..37bc130 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..0b809c4 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,202 @@ + + + + Hinzufügen + Feld darf nicht leer gelassen werden + Löschen + Aktion bestätigen + Bestätigen + Abbrechen + Fertig + Bearbeiten + Beenden + Erstellen + Allgemein + Erstelle ein Schuljahr + Bevor du Kurse und Abschlussnoten hinzufügst, gehe in die Einstellungen und erstelle ein Schuljahr und ein Abschlussprofil! + Wird noch hinzugefügt + + + Kurs + Kurse + Deine Kurse + Derzeitiger Durchschnitt: %s + Derzeitiger gesamter Durchschnitt: %s + Kein Schuljahr ausgewählt + Keine Noten für %s eingetragen + Kurs hinzufügen + Name + Leistungskurs + Wähle ein Symbol + Optionen für %s + Vorherige Durchschnitte für %s + Kurs bearbeiten + Vorherige Durchschnitte einsehen + Kurs und dazugehörige Daten löschen + Bist du dir sicher, dass du %s und alle dazugehörigen Daten löschen möchtest? Dies umfasst alle Noten und ehemaligen Durchschnitte und kann nicht rückgängig gemacht werden! + Ø für %s: %s + Ø für %s: %s ≙ %s + Ø für %s: %s + Ø: Note %s + %s (LK) + + Ø: %s Punkt + Ø: %s Punkte + + Keine Noten + + ; %s Woche bis zur Klausur + ; %s Woche bis zur Klausur + + + ; %s Tag bis zur Klausur + ; %s Tage bis zur Klausur + + ; Klausur heute + Überprüfe Notenart + + + Durschnitte für %s + + + Noten + Deine Noten + Note hinzufügen + Mündlich + Schriftlich + ; Klausur an %s + Mündlich + Schriftlich + Note + Gewichtung + Gewichtung darf 100% nicht überschreiten + Bist du dir sicher, dass du diese Note löschen möchtest? + Note bearbeiten + Klausur + Klausurdatum (yyyy-mm-dd) + Dieses Datum kann nicht verarbeitet werden + Vergangene Klausuren müssen benotet werden + + + Notizen + Deine Notizen + Erstellt: %s + Notiz hinzufügen + Inhalt + Kategorie + Optionen für Notiz + Bis du dir sicher, dass du diese Notiz löschen möchtest? + (nicht kategorisiert) + Notiz bearbeiten + Alle Notizen für Kategorie \'%s\' einsehen + Notiz löschen + Notiz als erledigt markieren + Notiz als nicht erledigt markieren + (Erledigt)\n\n%s + + + Beteiligung + Deine Beteiligung + Kurs verknüpfen + Bist du dir sicher, dass du die Verknüpfung zu %s am %s entfernen möchtest? Deine Beteiligungen werden beibehalten. + Klassenzimmermods starten + Gemeldet: %s, gesprochen: %s + Gemeldet : Gesprochen + Bist du dir sicher, dass du diese Beteiligung löschen möchtest? + Klassenzimmermodus + Lange drücken um abzubrechen + Lange drücken um zu beenden + + + Montag + Dienstag + Mittwoch + Donnerstag + Freitag + + + + Abschlussnoten + Deine Abschlussnoten + Note für %s + Kein Abschlussprofil ausgewählt + Optionen für %s + Abschlussnote + Grundkurse + Leistungskurse + Abiturklausuren + Abschlussnote bearbeiten + Kurs verknüpfen + Verknüpften Kurs aufrufen + Abschlussnote löschen + Bist du dir sicher, dass du diese %s-Abschlussnote löschen möchtest? + Note kann nicht gelöscht werden; ist bereits leer + Wähle ein Schuljahr + Wähle einen Kurs + + + Einstellungen + + Sortiere Kurse nach + + Zeit hinzugefügt (aufsteigend) + Zeit hinzugefügt (absteigend) + Name (aufsteigend) + Name (absteigend) + LK / GK + GK / LK + Durchschnitt (aufsteigend) + Durchschnitt (absteigend) + Nächste Klausur + + + App-Design + + Hell + Dunkel + System folgen + + + Gesamtdurchschnitt als Note zeigen + Wird nur neben Punkteschnitt gezeigt + Notenart + + Punkte (präzise) + Punkte (gerundet) + Nummerierte Noten (1–6) + Buchstabierte Noten (A–F) + + + Schuljahr + Schuljahr hinzufügen + Schuljahr bearbeiten + Kurse kopieren + Kurse erfolgreich kopiert + Schuljahr + Bist du dir sicher, dass du das Schuljahr %s löschen möchtest? + + Anzeige der Beteiligungen + + Durchschnitt pro Stunde + Gesamt + + Ausgewählter Tag + + Automatisch + Montag + Dienstag + Mittwoch + Donnerstag + Freitag + + + Abschlussnoten + Abschlussprofil + Abschlussprofil hinzufügen + Abschlussprofil bearbeiten + Bist du dir sicher, dass du das Abschlussprofil %s löschen möchtest? + + + Du musst an deinem Selbstvertrauen arbeiten :) + + \ No newline at end of file diff --git a/app/src/main/res/values-night/attr.xml b/app/src/main/res/values-night/attr.xml new file mode 100644 index 0000000..3b094f6 --- /dev/null +++ b/app/src/main/res/values-night/attr.xml @@ -0,0 +1,4 @@ + + + false + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..359058a --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,17 @@ + + + #6abf69 + #121212 + #E6121212 + #222222 + @color/colorAccent + #e0e0e0 + #9e9e9e + #313131 + #212121 + #757575 + #CF6679 + #e0e0e0 + @color/colorText + @color/colorText + \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp/attr.xml b/app/src/main/res/values-sw600dp/attr.xml new file mode 100644 index 0000000..d3a0e92 --- /dev/null +++ b/app/src/main/res/values-sw600dp/attr.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/main/res/values/attr.xml b/app/src/main/res/values/attr.xml new file mode 100644 index 0000000..a2ac3f5 --- /dev/null +++ b/app/src/main/res/values/attr.xml @@ -0,0 +1,6 @@ + + + 12dp + true + false + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..fcdf05c --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,19 @@ + + + #388e3c + #e0e0e0 + #E6e0e0e0 + #f5f5f5 + @color/colorAccent + #212121 + #757575 + #bdbdbd + #e0e0e0 + #9e9e9e + #B00020 + #e0e0e0 + @color/colorAccent + @color/colorAccent + + #757575 + diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..d4b05ef --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #388E3C + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..4978376 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,264 @@ + + + Qwark + Add + Field must not be left empty + Delete + Confirm action + Confirm + Cancel + Done + Edit + Finish + Create + General + Create a school year + Before adding courses and final grades, please do make sure to go into the settings and create a school year and a score profile! + To be implemented + Powered by Qwark + Made by Deniz + + + Course + Courses + Your Courses + Current average: %s + Current total average: %s + No school year selected + No grades entered for %s + Add course + Name + Advanced + Pick an icon + Options for %s + Historical data for %s + Edit course + View historical data + Delete course and associated data + Are you sure you want to delete %s and all associated information? This includes all grades and historical data and cannot be undone! + Ø: %s + Ø for %s: %s + Ø for %s: %s ≙ %s + Ø for %s: Grade %s + Ø: Grade %s + %s (Adv.) + + Ø: %s point + Ø: %s points + + No grades yet + + ; %s week until exam + ; %s weeks until exam + + + ; %s day until exam + ; %s days until exam + + ; exam today + Check grade type + + + Averages for %s + + + Grades + Your Grades + Add grade + Verbal + Written + ; exam on %s + Verbal + Written + + 15 + 14 + 13 + 12 + 11 + 10 + 09 + 08 + 07 + 06 + 05 + 04 + 03 + 02 + 01 + 00 + + + 1+ + 1 + 1- + 2+ + 2 + 2- + 3+ + 3 + 3- + 4+ + 4 + 4- + 5+ + 5 + 5- + 6 + + + A+ + A + A- + B+ + B + B- + C+ + C + C- + D+ + D + D- + E+ + E + E- + F + + Grade + Weighting + Weighting must not exceed 100% + Are you sure you want to delete this grade? + Edit grade + Exam + Exam date (yyyy-mm-dd) + This date can\'t be parsed + Passed exams must have a grade associated + Open participation + + + Notes + Your Notes + Created: %s + Add note + Content + Category + Options for note + Are you sure you want to delete this note? + (Uncategorized) + Edit note + View all notes for category \'%s\' + Delete note + Mark note as done + Undo marking note as done + (Dismissed)\n\n%s + \'%s\' + + + Participation + Your Participation + Link course + Are you sure you want to remove this link to %s on %s? This will not delete any participations. + Start class session + Hand raised: %s, spoken: %s + Hand raised : Spoken + Are you sure you want to delete this participation? + Class Session + Long-press to cancel + Long-press to finish + Open course + + + Monday + Tuesday + Wednesday + Thursday + Friday + + + + Finals + Your Finals + Score for %s + No score profile selected + Options for %s + final grade + Basic courses + Advanced courses + Finals + Edit final grade + Link course + View linked course + Clear final grade + Are you sure you want to clear this %s grade? + Grade cannot be cleared; is already empty + Pick a school year + Pick a course + 900 – 823 –> 1.0\n822 – 805 –> 1.1\n804 – 787 –> 1.2\n786 – 769 –> 1.3\n768 – 751 –> 1.4\n750 – 733 –> 1.5\n732 – 715 –> 1.6\n714 – 697 –> 1.7\n696 – 679 –> 1.8\n678 – 661 –> 1.9\n660 – 643 –> 2.0\n\n642 – 625 –> 2.1\n624 – 607 –> 2.2\n606 – 589 –> 2.3\n588 – 571 –> 2.4\n570 – 553 –> 2.5\n552 – 535 –> 2.6\n534 – 517 –> 2.7\n516 – 499 –> 2.8\n498 – 481 –> 2.9\n480 – 463 –> 3.0\n\n462 – 445 –> 3.1\n444 – 427 –> 3.2\n426 – 409 –> 3.3\n408 – 391 –> 3.4\n390 – 373 –> 3.5\n372 – 355 –> 3.6\n354 – 337 –> 3.7\n336 – 319 –> 3.8\n318 – 301 –> 3.9\n300 –> 4.0 + ≈ %s + + + Settings + + Sort courses by + + Time added (ascending) + Time added (descending) + Name (ascending) + Name (descending) + Advanced / basic + Basic / advanced + Average (ascending) + Average (descending) + Next exam + + + App theme + + Light + Dark + Follow system + + + Show grade average in course selector + Only applies to point averages + Grade type + + Points (precise) + Points (rounded) + Numbered grades (1–6) + Lettered grades (A–F) + + + School year + Add school year + Edit school year + Copy courses + Copied courses successfully + school year + Are you sure you want to delete the school year named %s? + + Participation display + + Average per lesson + Total + + Selected day + + Automatic + Monday + Tuesday + Wednesday + Thursday + Friday + + + Final grades + Score profile + Add score profile + Edit score profile + Are you sure you want to delete the score profile named %s? + + + You have got to work on your self-esteem :) + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..b16d4d3 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..ae5f183 --- /dev/null +++ b/build.gradle @@ -0,0 +1,28 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext.kotlin_version = '1.3.61' + repositories { + google() + jcenter() + + } + dependencies { + classpath 'com.android.tools.build:gradle:4.0.0-alpha09' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..23339e0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6b90e75 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Feb 03 16:46:00 CET 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-rc-1-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..4bf85a0 --- /dev/null +++ b/local.properties @@ -0,0 +1,10 @@ +## This file is automatically generated by Android Studio. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file should *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +sdk.dir=C\:\\Users\\kduez\\AppData\\Local\\Android\\Sdk diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..533fa1e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +include ':app' +rootProject.name='Qwark'