diff --git a/mobile/app/build.gradle.kts b/mobile/app/build.gradle.kts index c3ec446..272effe 100644 --- a/mobile/app/build.gradle.kts +++ b/mobile/app/build.gradle.kts @@ -37,18 +37,19 @@ android { dependencies { - implementation("androidx.core:core-ktx:1.10.1") + implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") - implementation("com.google.android.material:material:1.9.0") + implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") - implementation("androidx.navigation:navigation-fragment-ktx:2.6.0") - implementation("androidx.navigation:navigation-ui-ktx:2.6.0") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.6") + implementation("androidx.navigation:navigation-ui-ktx:2.7.6") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("androidx.viewpager2:viewpager2:1.0.0") implementation("com.github.bumptech.glide:glide:4.16.0") + implementation("com.google.android.material:material:1.11.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test.ext:junit:1.1.5") testImplementation("junit:junit:4.13.2") diff --git a/mobile/app/src/main/AndroidManifest.xml b/mobile/app/src/main/AndroidManifest.xml index e67eba3..3b47675 100644 --- a/mobile/app/src/main/AndroidManifest.xml +++ b/mobile/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + + android:theme="@style/Theme.Solen"> diff --git a/mobile/app/src/main/java/com/odweta/solen/Account.kt b/mobile/app/src/main/java/com/odweta/solen/Account.kt new file mode 100644 index 0000000..b10a952 --- /dev/null +++ b/mobile/app/src/main/java/com/odweta/solen/Account.kt @@ -0,0 +1,72 @@ +package com.odweta.solen + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.io.File +import java.nio.charset.Charset + +class Account(val username: String, val password: String, private val error: Int) { + companion object { + private fun parseAccountDataFile(file: File): Account { + val lines: List = file.readLines() + + if (lines.size >= 2) { + if (lines[0] != "" && lines[1] != "") { + return Account(lines[0], lines[1], 0) + } + } + + return Account("", "", 1) + } + + fun clearAccountFile() { + val file = File(Vars.fileDir, Vars.accountDataFilePath) + file.delete() + file.createNewFile() + } + + fun haveSaved(): Boolean { + /* check if a file exists + * no -> create it and return false + * yes -> read the contents and use them as the username and password + */ + + // check if file exists + val file = File(Vars.fileDir, Vars.accountDataFilePath) + + return if (!file.exists()) { + // it does not exist + file.createNewFile() + false + } else { + // it exists, so it shall be parsed + val acc: Account = parseAccountDataFile(file) + Vars.username = acc.username + Vars.password = acc.password + acc.error == 0 + } + } + + fun save(acc: Account): Boolean { + return try { + val file = File(Vars.fileDir, Vars.accountDataFilePath) + file.writeText("${acc.username}\n${acc.password}\n", Charset.forName("UTF-8")) + + true + } catch (e: Exception) { + false + } + } + + suspend fun isValid(acc: Account): Boolean { + return withContext(Dispatchers.IO) { + Vars.username = acc.username + Vars.password = acc.password + + val token = Network.getToken() + !JSONObject(token).has("error") + } + } + } +} \ No newline at end of file diff --git a/mobile/app/src/main/java/com/odweta/solen/CalWeekFragment.kt b/mobile/app/src/main/java/com/odweta/solen/CalWeekFragment.kt index 7e6bf37..eaf80da 100644 --- a/mobile/app/src/main/java/com/odweta/solen/CalWeekFragment.kt +++ b/mobile/app/src/main/java/com/odweta/solen/CalWeekFragment.kt @@ -5,6 +5,7 @@ import android.content.res.Resources import android.graphics.Typeface import android.graphics.drawable.GradientDrawable import android.os.Bundle +import android.util.Log import android.view.Gravity import android.view.LayoutInflater import android.view.View @@ -23,14 +24,7 @@ import androidx.lifecycle.Lifecycle import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 -interface MainActivityListener { - fun onLaunch(view: View) -} - -@Suppress("INFERRED_TYPE_VARIABLE_INTO_EMPTY_INTERSECTION_WARNING") class CalWeekFragment : Fragment() { - //private var mainActivityListener: MainActivityListener? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -41,12 +35,7 @@ class CalWeekFragment : Fragment() { val viewPager: ViewPager2 = view.findViewById(R.id.dayPager) val adapter = DayPagerAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) - for (i in 0 until adapter.itemCount) { - childFragmentManager.beginTransaction().remove(requireActivity().findViewById(adapter.getItemId(i) - .toInt())).commit() - } - - for (i in 0 until MainActivity.days.size) { + for (i in 0 until Vars.days.size) { adapter.addFragment(DayFragment(i)) } @@ -84,7 +73,7 @@ class DayFragment(private val position: Int) : Fragment(R.layout.fragment_day) { val view = inflater.inflate(R.layout.fragment_day, container, false) val dayLayout: LinearLayout = view.findViewById(R.id.layoutDayMain) - MainActivity.dayMap[position]?.let { createDayView(it, dayLayout, resources) } + Vars.dayMap[position]?.let { createDayView(it, dayLayout, resources) } return view } @@ -98,16 +87,34 @@ class DayFragment(private val position: Int) : Fragment(R.layout.fragment_day) { val nameDateString = day.name + " " + day.date nameDateTextView.text = nameDateString nameDateTextView.textSize = 30f - nameDateTextView.setPadding(10, 10, 10, 40) + nameDateTextView.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.white + ) + ) + nameDateTextView.setPadding(10, 40, 10, 40) nameDateTextView.gravity = Gravity.CENTER_HORIZONTAL rootLayout.addView(nameDateTextView) for (subj in day.subjects) { val subjectBackground = when (subj.substitute) { - "0" -> if (subj.place != "Volno") { R.color.cal_week_subject } else { R.color.cal_week_free } - "1" -> if (subj.place != "Volno") { R.color.cal_week_substitute } else { R.color.cal_week_free } - "2" -> if (subj.place != "Volno") { R.color.cal_week_event } else { R.color.cal_week_free } + "0" -> if (subj.place != "Volno") { + R.color.cal_week_subject + } else { + R.color.cal_week_free + } + "1" -> if (subj.place != "Volno") { + R.color.cal_week_substitute + } else { + R.color.cal_week_free + } + "2" -> if (subj.place != "Volno") { + R.color.cal_week_event + } else { + R.color.cal_week_free + } else -> R.color.cal_week_subject } @@ -187,7 +194,7 @@ class DayFragment(private val position: Int) : Fragment(R.layout.fragment_day) { ) ) - if (subj.name == "Volno") { + if (subj.free) { subjectNameTextView.setPadding(260, 35, 20, 35) } @@ -195,7 +202,7 @@ class DayFragment(private val position: Int) : Fragment(R.layout.fragment_day) { val subjectPlaceTextView = TextView(requireContext()) subjectPlaceTextView.text = subj.place - if (subj.name == "Volno") { + if (subj.free) { // underline subjectPlaceTextView.setTypeface(null, Typeface.ITALIC) } @@ -291,15 +298,6 @@ class DayFragment(private val position: Int) : Fragment(R.layout.fragment_day) { // Remove the listener to avoid multiple callbacks rootLayout.viewTreeObserver.removeOnPreDrawListener(this) - if (dayLayout.height < rootLayout.height) { - val sepr = Space(requireContext()) - sepr.layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - rootLayout.height - dayLayout.height - ) - rootLayout.addView(sepr) - } - // Call your function or perform an action here if (day.subjects[0].substitute != "2") { subjectPlaceTextView.setPadding( @@ -315,15 +313,6 @@ class DayFragment(private val position: Int) : Fragment(R.layout.fragment_day) { subjectNameTextView.gravity = Gravity.CENTER_VERTICAL } - // check for substitute hours and change the background - /*for (s in day.subjects) { - when (s.substitute) { - "1" -> subjectLayout.background = substituteDrawable - "2" -> subjectLayout.background = eventDrawable - else -> break - } - }*/ - // Return true to continue with the drawing, or false to cancel return true } @@ -387,17 +376,27 @@ class DayFragment(private val position: Int) : Fragment(R.layout.fragment_day) { scrollView.addView(dayLayout) rootLayout.addView(scrollView) + + // fill in the rest with empty space + val sepr = Space(requireContext()) + sepr.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + Vars.height - rootLayout.height + ) + + rootLayout.addView(sepr) } } class Subject { - var name: String = "" - var place: String = "" - var teacher: String = "" - var substitute: String = "" - var number: String = "" - var start: String = "" - var end: String = "" + lateinit var name: String + lateinit var place: String + lateinit var teacher: String + lateinit var substitute: String + lateinit var number: String + lateinit var start: String + lateinit var end: String + var free: Boolean = false companion object { fun freeSubject(number: String, start: String, end: String): Subject { @@ -410,6 +409,7 @@ class Subject { s.number = number s.start = start s.end = end + s.free = true return s } diff --git a/mobile/app/src/main/java/com/odweta/solen/Dialogs.kt b/mobile/app/src/main/java/com/odweta/solen/Dialogs.kt new file mode 100644 index 0000000..049a777 --- /dev/null +++ b/mobile/app/src/main/java/com/odweta/solen/Dialogs.kt @@ -0,0 +1,63 @@ +package com.odweta.solen + +import android.app.AlertDialog +import android.content.Context +import android.view.LayoutInflater +import androidx.fragment.app.DialogFragment + +class Dialogs { + class SignOut(context: Context) : AlertDialog(context) { + init { + // Initialize your dialog properties + setTitle("Upozornění") + setMessage("Opravdu se chcete odhlásit? Tato akce vymaže uložené přihlašovací údaje a zavře aplikaci. Budete se poté muset znovu přihlásit.") + setCancelable(true) // Set whether the dialog can be canceled by tapping outside + + // Set positive button (e.g., "OK" button) + setButton( + BUTTON_POSITIVE, + "Ano" + ) { _, _ -> + Account.clearAccountFile() + MainActivity.quit(context) + } + + // Set negative button (e.g., "Cancel" button) + setButton( + BUTTON_NEGATIVE, + "Ne" + ) { _, _ -> + dismiss() + } + } + } + + class LanguageChange(context: Context) : AlertDialog(context) { + init { + // Initialize your dialog properties + setTitle("Změnit jazyk") + setCancelable(true) + // Inflate the layout for the dialog + val view = LayoutInflater.from(context).inflate(R.layout.radio_group_language, null) + setView(view) + + // Set positive button (e.g., "OK" button) + setButton( + BUTTON_POSITIVE, + "OK" + ) { _, _ -> + // functionality here + + dismiss() + } + + // Set negative button (e.g., "Cancel" button) + setButton( + BUTTON_NEGATIVE, + "Zrušit" + ) { _, _ -> + dismiss() + } + } + } +} \ No newline at end of file diff --git a/mobile/app/src/main/java/com/odweta/solen/MainActivity.kt b/mobile/app/src/main/java/com/odweta/solen/MainActivity.kt index 4fa88e9..44c89e4 100644 --- a/mobile/app/src/main/java/com/odweta/solen/MainActivity.kt +++ b/mobile/app/src/main/java/com/odweta/solen/MainActivity.kt @@ -1,70 +1,54 @@ package com.odweta.solen -import android.R.attr.maxHeight +import android.content.Context +import android.content.Intent import android.graphics.ImageDecoder import android.graphics.drawable.AnimatedImageDrawable +import android.media.Image import android.os.Bundle +import android.util.Log import android.view.Menu import android.view.MenuItem -import android.view.View -import android.view.View.MeasureSpec import android.widget.Button import android.widget.EditText import android.widget.ImageView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.FormBody -import okhttp3.OkHttpClient -import okhttp3.Request -import org.json.JSONException -import org.json.JSONObject -import java.io.File -import java.io.IOException -import java.nio.charset.Charset -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -class MainActivity : AppCompatActivity(), MainActivityListener { +class MainActivity : AppCompatActivity() { + // @section COMPANION companion object { - val dayMap = mutableMapOf() - var days = listOf() - } + fun quit(context: Context) { + // Create an Intent to launch the home screen + val homeIntent = Intent(Intent.ACTION_MAIN) + homeIntent.addCategory(Intent.CATEGORY_HOME) + homeIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - private val accountDataFilePath: String = "account_data.txt" + // Start the home screen activity + context.startActivity(homeIntent) - private val client = OkHttpClient() - - private lateinit var username: String - private lateinit var password: String - private val apiBaseUrl = "https://aplikace.skolaonline.cz/solapi/api" - private val tokenUrl = "$apiBaseUrl/connect/token" - private lateinit var token: String - private val userDataUrl = "$apiBaseUrl/v1/user" - private lateinit var userData: String - private lateinit var studentId: String - private val timeTableUrl = "$apiBaseUrl/v1/timeTable" - //private val marksUrl = "$apiBaseUrl/v1/students/$studentId/marks/list" - - private suspend fun setupAPI() { - return withContext(Dispatchers.IO) { - token = JSONObject(getToken()).get("access_token").toString() - if (token == "") { - return@withContext + // If the context is an Activity, finish it to close the current activity + if (context is AppCompatActivity) { + context.finish() } - userData = getUserDataJSON() - studentId = getStudentId() } } + + // @section LISTENERS override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - showSplashScreen() + // set variables + Vars.fileDir = applicationContext.filesDir + Vars.width = Util.getDisplayWidth(this) + Vars.height = Util.getDisplayHeight(this) + + // start the app (coroutine because splashscreen) lifecycleScope.launch { launch() } @@ -76,24 +60,6 @@ class MainActivity : AppCompatActivity(), MainActivityListener { return true } - private fun showSignInScreen() { - setContentView(R.layout.activity_sign_in) - setSignInButtonOnClick() - } - - private fun showSettingsScreen() { - setContentView(R.layout.activity_settings) - findViewById