Compare commits
10 Commits
ad2506333b
...
04f7e29b8a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04f7e29b8a | ||
|
|
46e431b277 | ||
|
|
348fdbf5d6 | ||
|
|
c07730587d | ||
|
|
adff1fea0c | ||
|
|
cb905fc9a6 | ||
|
|
8302884fdc | ||
|
|
def91f28a2 | ||
|
|
f9c2f9e6cf | ||
|
|
f2133abea6 |
@@ -32,10 +32,12 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
compose true
|
||||||
}
|
}
|
||||||
|
|
||||||
dataBinding {
|
composeOptions {
|
||||||
enabled = true
|
kotlinCompilerVersion kotlin_version
|
||||||
|
kotlinCompilerExtensionVersion compose_version
|
||||||
}
|
}
|
||||||
|
|
||||||
// To inline the bytecode built with JVM target 1.8 into
|
// To inline the bytecode built with JVM target 1.8 into
|
||||||
@@ -54,7 +56,6 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.core:core-ktx:1.7.0'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
@@ -77,4 +78,16 @@ dependencies {
|
|||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
|
|
||||||
|
// Compose
|
||||||
|
implementation "androidx.compose.runtime:runtime:1.0.5"
|
||||||
|
implementation "androidx.compose.runtime:runtime-livedata:1.0.5"
|
||||||
|
implementation "androidx.compose.ui:ui:1.0.5"
|
||||||
|
implementation "androidx.compose.ui:ui-tooling:1.0.5"
|
||||||
|
implementation "androidx.compose.foundation:foundation:1.0.5"
|
||||||
|
implementation "androidx.compose.foundation:foundation-layout:1.0.5"
|
||||||
|
implementation "androidx.compose.material:material:1.0.5"
|
||||||
|
implementation "androidx.navigation:navigation-compose:2.4.0-beta02"
|
||||||
|
|
||||||
|
implementation "com.google.android.material:compose-theme-adapter:1.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,40 +3,35 @@ package de.sebse.fuplanner2
|
|||||||
import android.accounts.Account
|
import android.accounts.Account
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import androidx.activity.compose.setContent
|
||||||
import android.view.MenuItem
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.core.os.bundleOf
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.navigation.NavOptions
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
|
||||||
import androidx.navigation.ui.navigateUp
|
|
||||||
import androidx.navigation.ui.setupActionBarWithNavController
|
|
||||||
import androidx.navigation.ui.setupWithNavController
|
|
||||||
import de.sebse.fuplanner2.auth.AppAccounts
|
import de.sebse.fuplanner2.auth.AppAccounts
|
||||||
import de.sebse.fuplanner2.database.AppDatabase
|
import de.sebse.fuplanner2.database.AppDatabase
|
||||||
import de.sebse.fuplanner2.database.Course
|
import de.sebse.fuplanner2.database.Course
|
||||||
import de.sebse.fuplanner2.database.User
|
import de.sebse.fuplanner2.database.User
|
||||||
import de.sebse.fuplanner2.databinding.ActivityMainBinding
|
import de.sebse.fuplanner2.ui.theme.AppTheme
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
class MainActivity() : AppCompatActivity() {
|
class MainActivity() : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
|
||||||
private lateinit var activityViewModel: MainActivityViewModel
|
private lateinit var activityViewModel: MainActivityViewModel
|
||||||
private lateinit var binding: ActivityMainBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
activityViewModel = ViewModelProvider(this)[MainActivityViewModel::class.java]
|
||||||
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
setContent {
|
||||||
|
AppTheme {
|
||||||
|
MainActivityComposable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*setContentView(R.layout.activity_main)
|
||||||
|
val navHostFragment =
|
||||||
|
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||||
val navController = navHostFragment.navController
|
val navController = navHostFragment.navController
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setSupportActionBar(binding.appBarMain.toolbar)
|
setSupportActionBar(binding.appBarMain.toolbar)
|
||||||
@@ -45,7 +40,8 @@ class MainActivity() : AppCompatActivity() {
|
|||||||
|
|
||||||
activityViewModel.user.observe(this) {
|
activityViewModel.user.observe(this) {
|
||||||
binding.navView.getHeaderView(0).run {
|
binding.navView.getHeaderView(0).run {
|
||||||
findViewById<TextView>(R.id.nav_header_title).text = getString(R.string.full_name, it?.firstName, it?.lastName)
|
findViewById<TextView>(R.id.nav_header_title).text =
|
||||||
|
getString(R.string.full_name, it?.firstName, it?.lastName)
|
||||||
findViewById<TextView>(R.id.nav_header_subtitle).text = it?.email
|
findViewById<TextView>(R.id.nav_header_subtitle).text = it?.email
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +53,11 @@ class MainActivity() : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
actionView = if (it == 0)
|
actionView = if (it == 0)
|
||||||
null
|
null
|
||||||
else layoutInflater.inflate(R.layout.nav_action_view_counter, binding.navView, false).apply {
|
else layoutInflater.inflate(
|
||||||
|
R.layout.nav_action_view_counter,
|
||||||
|
binding.navView,
|
||||||
|
false
|
||||||
|
).apply {
|
||||||
findViewById<TextView>(R.id.counterText).text = when (it) {
|
findViewById<TextView>(R.id.counterText).text = when (it) {
|
||||||
in 0..99 -> it.toString()
|
in 0..99 -> it.toString()
|
||||||
else -> "99+"
|
else -> "99+"
|
||||||
@@ -68,7 +68,7 @@ class MainActivity() : AppCompatActivity() {
|
|||||||
activityViewModel.latestSemester.observe(this) {
|
activityViewModel.latestSemester.observe(this) {
|
||||||
val courseOrder = binding.navView.menu.findItem(R.id.nav_courses).order
|
val courseOrder = binding.navView.menu.findItem(R.id.nav_courses).order
|
||||||
var i = binding.navView.menu.size() - 1
|
var i = binding.navView.menu.size() - 1
|
||||||
while(i >= 0) {
|
while (i >= 0) {
|
||||||
val menuItem: MenuItem = binding.navView.menu.getItem(i--)
|
val menuItem: MenuItem = binding.navView.menu.getItem(i--)
|
||||||
if (menuItem.order / 100 == courseOrder / 100 && menuItem.order != courseOrder) {
|
if (menuItem.order / 100 == courseOrder / 100 && menuItem.order != courseOrder) {
|
||||||
binding.navView.menu.removeItem(menuItem.itemId)
|
binding.navView.menu.removeItem(menuItem.itemId)
|
||||||
@@ -76,20 +76,26 @@ class MainActivity() : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
it.mapIndexed { index, course ->
|
it.mapIndexed { index, course ->
|
||||||
val itemOrder = courseOrder / 100 * 100 + index + 2
|
val itemOrder = courseOrder / 100 * 100 + index + 2
|
||||||
binding.navView.menu.add(0, itemOrder, itemOrder, course.title).setOnMenuItemClickListener {
|
binding.navView.menu.add(0, itemOrder, itemOrder, course.title)
|
||||||
navController.navigate(
|
.setOnMenuItemClickListener {
|
||||||
R.id.course_details,
|
navController.navigate(
|
||||||
bundleOf("courseId" to course.uid, "title" to course.title),
|
R.id.course_details,
|
||||||
NavOptions.Builder().setPopUpTo(R.id.nav_courses, false).build()
|
bundleOf("courseId" to course.uid, "title" to course.title),
|
||||||
)
|
NavOptions.Builder().setPopUpTo(R.id.nav_courses, false).build()
|
||||||
binding.drawerLayout.closeDrawers()
|
)
|
||||||
false
|
binding.drawerLayout.closeDrawers()
|
||||||
}
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appBarConfiguration = AppBarConfiguration(
|
appBarConfiguration = AppBarConfiguration(
|
||||||
setOf(R.id.nav_courses, R.id.nav_canteen, R.id.nav_schedule, R.id.nav_notifications),
|
setOf(
|
||||||
|
R.id.nav_courses,
|
||||||
|
R.id.nav_canteen,
|
||||||
|
R.id.nav_schedule,
|
||||||
|
R.id.nav_notifications
|
||||||
|
),
|
||||||
binding.drawerLayout
|
binding.drawerLayout
|
||||||
)
|
)
|
||||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||||
@@ -98,7 +104,7 @@ class MainActivity() : AppCompatActivity() {
|
|||||||
if (intent.getBooleanExtra(EXTRA_OPEN_NOTIFICATIONS, false)) {
|
if (intent.getBooleanExtra(EXTRA_OPEN_NOTIFICATIONS, false)) {
|
||||||
navController.navigate(R.id.nav_notifications)
|
navController.navigate(R.id.nav_notifications)
|
||||||
intent.putExtra(EXTRA_OPEN_NOTIFICATIONS, false)
|
intent.putExtra(EXTRA_OPEN_NOTIFICATIONS, false)
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
@@ -115,7 +121,7 @@ class MainActivity() : AppCompatActivity() {
|
|||||||
activityViewModel.updateSelectedUser(selectedAccount)
|
activityViewModel.updateSelectedUser(selectedAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
/*override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
// Inflate the menu; this adds items to the action bar if it is present.
|
// Inflate the menu; this adds items to the action bar if it is present.
|
||||||
//menuInflater.inflate(R.menu.main, menu)
|
//menuInflater.inflate(R.menu.main, menu)
|
||||||
return true
|
return true
|
||||||
@@ -126,6 +132,13 @@ class MainActivity() : AppCompatActivity() {
|
|||||||
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return onNavDestinationSelected(
|
||||||
|
item,
|
||||||
|
findNavController(R.id.nav_host_fragment)
|
||||||
|
) || super.onOptionsItemSelected(item)
|
||||||
|
}*/
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_OPEN_NOTIFICATIONS: String = "EXTRA_OPEN_NOTIFICATIONS"
|
const val EXTRA_OPEN_NOTIFICATIONS: String = "EXTRA_OPEN_NOTIFICATIONS"
|
||||||
const val EXTRA_NETWORK_ERROR = "EXTRA_NETWORK_ERROR"
|
const val EXTRA_NETWORK_ERROR = "EXTRA_NETWORK_ERROR"
|
||||||
@@ -141,7 +154,6 @@ class MainActivityViewModel : ViewModel() {
|
|||||||
val notificationCnt: LiveData<Int> = database.notificationDao().getUnreadRowCount()
|
val notificationCnt: LiveData<Int> = database.notificationDao().getUnreadRowCount()
|
||||||
val latestSemester: LiveData<List<Course>> = database.courseDao().getLatestSemester()
|
val latestSemester: LiveData<List<Course>> = database.courseDao().getLatestSemester()
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
|
||||||
fun updateSelectedUser(account: Account) {
|
fun updateSelectedUser(account: Account) {
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
user.postValue(database.userDao().findByUsername(account.name))
|
user.postValue(database.userDao().findByUsername(account.name))
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ class StartupActivity : AppCompatActivity() {
|
|||||||
AppAccounts.RefreshResults.SUCCESS -> {
|
AppAccounts.RefreshResults.SUCCESS -> {
|
||||||
AppAccounts.getInstance().setPeriodicSync()
|
AppAccounts.getInstance().setPeriodicSync()
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME
|
intent.flags =
|
||||||
|
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME
|
||||||
if (it == AppAccounts.RefreshResults.NETWORK_ERROR)
|
if (it == AppAccounts.RefreshResults.NETWORK_ERROR)
|
||||||
intent.putExtra(MainActivity.EXTRA_NETWORK_ERROR, true)
|
intent.putExtra(MainActivity.EXTRA_NETWORK_ERROR, true)
|
||||||
if (it == AppAccounts.RefreshResults.UNSPECIFIED_ERROR)
|
if (it == AppAccounts.RefreshResults.UNSPECIFIED_ERROR)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import androidx.room.Index
|
|||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.beust.klaxon.JsonObject
|
import com.beust.klaxon.JsonObject
|
||||||
import de.sebse.fuplanner2.R
|
import de.sebse.fuplanner2.R
|
||||||
import de.sebse.fuplanner2.ui.notification.NotificationFragmentDirections
|
|
||||||
import de.sebse.fuplanner2.utils.*
|
import de.sebse.fuplanner2.utils.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -82,7 +81,7 @@ data class Announcement (
|
|||||||
?.let { AppDatabase.getInstance().courseDao().getCourseById2(it.courseId) }
|
?.let { AppDatabase.getInstance().courseDao().getCourseById2(it.courseId) }
|
||||||
?.let {
|
?.let {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
navController.navigate(NotificationFragmentDirections.actionNavNotificationsToCourseDetails(it.uid!!, it.title))
|
//navController.navigate(NotificationFragmentDirections.actionNavNotificationsToCourseDetails(it.uid!!, it.title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
package de.sebse.fuplanner2.database
|
package de.sebse.fuplanner2.database
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.paging.DataSource
|
import androidx.paging.DataSource
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import de.sebse.fuplanner2.utils.console
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface AnnouncementDao {
|
interface AnnouncementDao {
|
||||||
@Query("SELECT * FROM announcement WHERE courseId = :courseId ORDER BY createdOn ASC")
|
@Query("SELECT * FROM announcement WHERE courseId = :courseId ORDER BY createdOn DESC")
|
||||||
fun getAll1(courseId: Long): DataSource.Factory<Int, Announcement>
|
fun getAll1(courseId: Long): DataSource.Factory<Int, Announcement>
|
||||||
|
|
||||||
@Query("SELECT * FROM announcement WHERE courseId = :courseId ORDER BY createdOn ASC")
|
@Query("SELECT * FROM announcement WHERE courseId = :courseId ORDER BY createdOn DESC")
|
||||||
fun getAll2(courseId: Long): List<Announcement>
|
fun getAll2(courseId: Long): List<Announcement>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM announcement WHERE courseId = :courseId ORDER BY createdOn DESC")
|
||||||
|
fun getAll3(courseId: Long): LiveData<List<Announcement>>
|
||||||
|
|
||||||
@Query("SELECT * FROM announcement WHERE uid = :announcementId LIMIT 1")
|
@Query("SELECT * FROM announcement WHERE uid = :announcementId LIMIT 1")
|
||||||
fun getAnnouncementById(announcementId: Long): Announcement
|
fun getAnnouncementById(announcementId: Long): Announcement
|
||||||
|
|
||||||
|
@Query("SELECT * FROM announcement WHERE uid = :announcementId LIMIT 1")
|
||||||
|
fun getAnnouncementById2(announcementId: Long): LiveData<Announcement>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
fun insert(announcement: Announcement): Long
|
fun insert(announcement: Announcement): Long
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import androidx.room.Index
|
|||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.beust.klaxon.JsonObject
|
import com.beust.klaxon.JsonObject
|
||||||
import de.sebse.fuplanner2.R
|
import de.sebse.fuplanner2.R
|
||||||
import de.sebse.fuplanner2.ui.notification.NotificationFragmentDirections
|
|
||||||
import de.sebse.fuplanner2.utils.*
|
import de.sebse.fuplanner2.utils.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -63,7 +62,7 @@ data class Course (
|
|||||||
actCtx: Context,
|
actCtx: Context,
|
||||||
type: Notifications.CourseUpdateType,
|
type: Notifications.CourseUpdateType,
|
||||||
data: JsonObject
|
data: JsonObject
|
||||||
): CharSequence? = when (type) {
|
): CharSequence = when (type) {
|
||||||
Notifications.CourseUpdateType.REMOVED -> actCtx.getHtmlSpannedString(R.string.not_course_update_course_removed, data.string("title"))
|
Notifications.CourseUpdateType.REMOVED -> actCtx.getHtmlSpannedString(R.string.not_course_update_course_removed, data.string("title"))
|
||||||
Notifications.CourseUpdateType.UPDATED -> actCtx.getHtmlSpannedString(R.string.not_course_update_course_updated, data.string("title"))
|
Notifications.CourseUpdateType.UPDATED -> actCtx.getHtmlSpannedString(R.string.not_course_update_course_updated, data.string("title"))
|
||||||
Notifications.CourseUpdateType.ADDED -> actCtx.getHtmlSpannedString(R.string.not_course_update_course_added, data.string("title"))
|
Notifications.CourseUpdateType.ADDED -> actCtx.getHtmlSpannedString(R.string.not_course_update_course_added, data.string("title"))
|
||||||
@@ -73,7 +72,7 @@ data class Course (
|
|||||||
actCtx: Context,
|
actCtx: Context,
|
||||||
type: Notifications.CourseUpdateType,
|
type: Notifications.CourseUpdateType,
|
||||||
data: JsonObject
|
data: JsonObject
|
||||||
): CharSequence? = when (type) {
|
): CharSequence = when (type) {
|
||||||
Notifications.CourseUpdateType.REMOVED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_course_removed, data.string("title"))
|
Notifications.CourseUpdateType.REMOVED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_course_removed, data.string("title"))
|
||||||
Notifications.CourseUpdateType.UPDATED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_course_updated, data.string("title"))
|
Notifications.CourseUpdateType.UPDATED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_course_updated, data.string("title"))
|
||||||
Notifications.CourseUpdateType.ADDED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_course_added, data.string("title"))
|
Notifications.CourseUpdateType.ADDED -> actCtx.getHtmlSpannedString(R.string.adapter_course_update_course_added, data.string("title"))
|
||||||
@@ -91,7 +90,7 @@ data class Course (
|
|||||||
?.let { AppDatabase.getInstance().courseDao().getCourseById2(it) }
|
?.let { AppDatabase.getInstance().courseDao().getCourseById2(it) }
|
||||||
?.let {
|
?.let {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
navController.navigate(NotificationFragmentDirections.actionNavNotificationsToCourseDetails(it.uid!!, it.title))
|
//navController.navigate(NotificationFragmentDirections.actionNavNotificationsToCourseDetails(it.uid!!, it.title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface CourseDao {
|
|||||||
fun getAll(): LiveData<List<Course>>
|
fun getAll(): LiveData<List<Course>>
|
||||||
|
|
||||||
@Query("SELECT * FROM course INNER JOIN (SELECT year, CASE WHEN isSummerSemester THEN 1 ELSE 0 END AS semester FROM course GROUP BY year, isSummerSemester ORDER BY year DESC, semester ASC LIMIT 1) current WHERE current.year = course.year AND current.semester = CASE WHEN course.isSummerSemester THEN 1 ELSE 0 END ORDER BY title ASC")
|
@Query("SELECT * FROM course INNER JOIN (SELECT year, CASE WHEN isSummerSemester THEN 1 ELSE 0 END AS semester FROM course GROUP BY year, isSummerSemester ORDER BY year DESC, semester ASC LIMIT 1) current WHERE current.year = course.year AND current.semester = CASE WHEN course.isSummerSemester THEN 1 ELSE 0 END ORDER BY title ASC")
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
fun getLatestSemester(): LiveData<List<Course>>
|
fun getLatestSemester(): LiveData<List<Course>>
|
||||||
|
|
||||||
@Query("SELECT year, CASE WHEN isSummerSemester THEN 1 ELSE 0 END AS semester FROM course GROUP BY year, isSummerSemester ORDER BY year DESC, semester ASC LIMIT 1")
|
@Query("SELECT year, CASE WHEN isSummerSemester THEN 1 ELSE 0 END AS semester FROM course GROUP BY year, isSummerSemester ORDER BY year DESC, semester ASC LIMIT 1")
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import androidx.room.Index
|
|||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.beust.klaxon.JsonObject
|
import com.beust.klaxon.JsonObject
|
||||||
import de.sebse.fuplanner2.R
|
import de.sebse.fuplanner2.R
|
||||||
import de.sebse.fuplanner2.ui.notification.NotificationFragmentDirections
|
|
||||||
import de.sebse.fuplanner2.utils.*
|
import de.sebse.fuplanner2.utils.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -81,7 +80,7 @@ data class Event (
|
|||||||
?.let { AppDatabase.getInstance().courseDao().getCourseById2(it.courseId) }
|
?.let { AppDatabase.getInstance().courseDao().getCourseById2(it.courseId) }
|
||||||
?.let {
|
?.let {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
navController.navigate(NotificationFragmentDirections.actionNavNotificationsToCourseDetails(it.uid!!, it.title))
|
//navController.navigate(NotificationFragmentDirections.actionNavNotificationsToCourseDetails(it.uid!!, it.title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import com.beust.klaxon.KlaxonException
|
|||||||
|
|
||||||
|
|
||||||
data class Lecturer (
|
data class Lecturer (
|
||||||
val fistName: String,
|
val firstName: String,
|
||||||
val lastName: String,
|
val lastName: String,
|
||||||
val email: String,
|
val email: String,
|
||||||
val isResponsible: Boolean
|
val isResponsible: Boolean
|
||||||
|
|||||||
156
app/src/main/java/de/sebse/fuplanner2/drawerLayout.kt
Normal file
156
app/src/main/java/de/sebse/fuplanner2/drawerLayout.kt
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package de.sebse.fuplanner2
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import de.sebse.fuplanner2.ui.theme.AppTheme
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
abstract class NavLinks(
|
||||||
|
@StringRes val title: Int,
|
||||||
|
@DrawableRes val drawable: Int,
|
||||||
|
val route: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DrawerLayout(
|
||||||
|
appName: String,
|
||||||
|
menu: List<NavLinks>,
|
||||||
|
startDestination: String,
|
||||||
|
builder: NavGraphBuilder.(Tools) -> Unit
|
||||||
|
) {
|
||||||
|
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val navController = rememberNavController()
|
||||||
|
val (title, setTitle) = remember {
|
||||||
|
mutableStateOf(appName)
|
||||||
|
}
|
||||||
|
val tools = Tools(setTitle = { setTitle(it) }, navController)
|
||||||
|
Scaffold(
|
||||||
|
scaffoldState = scaffoldState,
|
||||||
|
topBar = {
|
||||||
|
TopBar(title = title) {
|
||||||
|
scope.launch {
|
||||||
|
scaffoldState.drawerState.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawerContent = {
|
||||||
|
Drawer(currentRoute = navController.currentDestination?.route, menu = menu) {
|
||||||
|
scope.launch { scaffoldState.drawerState.close() }
|
||||||
|
navController.navigate(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
NavHost(navController, startDestination = startDestination) {
|
||||||
|
this.apply { builder(tools) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tools(val setTitle: (String) -> Unit, private val navController: NavHostController) {
|
||||||
|
private val navOptions = NavOptions.Builder()
|
||||||
|
.setEnterAnim(R.anim.slide_in_right)
|
||||||
|
.setExitAnim(R.anim.slide_out_left)
|
||||||
|
.setPopEnterAnim(R.anim.slide_in_left)
|
||||||
|
.setPopExitAnim(R.anim.slide_out_right)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
fun navBack() {
|
||||||
|
this.navController.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navTo(route: String) {
|
||||||
|
this.navController.navigate(route, navOptions = navOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TopBar(title: String = "", onOpenClick: () -> Unit) {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = title
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onOpenClick) {
|
||||||
|
Icon(Icons.Filled.Menu, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Drawer(currentRoute: String?, menu: List<NavLinks>, onItemClick: (route: String) -> Unit) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.widthIn(min = 300.dp, max = 500.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_logo_mono),
|
||||||
|
contentDescription = "App icon"
|
||||||
|
)
|
||||||
|
DrawerItems(currentRoute, menu = menu, onItemClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DrawerItems(currentRoute: String?, menu: List<NavLinks>, onItemClick: (route: String) -> Unit) {
|
||||||
|
menu.forEach { screen ->
|
||||||
|
val isSelected = currentRoute == screen.route
|
||||||
|
val modifier = Modifier
|
||||||
|
.padding(top = 24.dp)
|
||||||
|
.selectable(isSelected) { onItemClick(screen.route) }
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(screen.drawable),
|
||||||
|
contentDescription = stringResource(screen.title)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(screen.title),
|
||||||
|
style = MaterialTheme.typography.h6
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TopBarPreview() {
|
||||||
|
AppTheme {
|
||||||
|
TopBar(
|
||||||
|
title = "A title"
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DrawerPreview() {
|
||||||
|
AppTheme {
|
||||||
|
Drawer(currentRoute = "courses", menu = screens) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package de.sebse.fuplanner2
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import de.sebse.fuplanner2.ui.courses.CoursesScreen
|
||||||
|
import de.sebse.fuplanner2.ui.details.CourseDetailsScreen
|
||||||
|
import de.sebse.fuplanner2.ui.details_announcements.CourseAnnouncementScreen
|
||||||
|
import de.sebse.fuplanner2.ui.details_announcements.CourseAnnouncementsScreen
|
||||||
|
import de.sebse.fuplanner2.ui.details_description.CourseDescriptionScreen
|
||||||
|
|
||||||
|
sealed class MenuItem {
|
||||||
|
object Courses : NavLinks(R.string.menu_courses, R.drawable.ic_menu_courses, "courses")
|
||||||
|
object Canteen : NavLinks(R.string.menu_canteen, R.drawable.ic_menu_canteen, "canteen")
|
||||||
|
object Schedule : NavLinks(R.string.menu_schedule, R.drawable.ic_menu_event, "schedule")
|
||||||
|
object Notifications :
|
||||||
|
NavLinks(R.string.menu_notifications, R.drawable.ic_menu_notifications, "navigation")
|
||||||
|
}
|
||||||
|
|
||||||
|
val screens = listOf(
|
||||||
|
MenuItem.Courses,
|
||||||
|
MenuItem.Canteen,
|
||||||
|
MenuItem.Schedule,
|
||||||
|
MenuItem.Notifications
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainActivityComposable() {
|
||||||
|
DrawerLayout(
|
||||||
|
appName = stringResource(R.string.app_name),
|
||||||
|
menu = screens,
|
||||||
|
startDestination = MenuItem.Courses.route
|
||||||
|
) { tools ->
|
||||||
|
composable(route = MenuItem.Courses.route) {
|
||||||
|
CoursesScreen(tools)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("id") { type = NavType.LongType }
|
||||||
|
),
|
||||||
|
route = "${MenuItem.Courses.route}/{id}"
|
||||||
|
) {
|
||||||
|
val id = it.arguments!!.getLong("id")
|
||||||
|
CourseDetailsScreen(tools, id)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("id") { type = NavType.LongType }
|
||||||
|
),
|
||||||
|
route = "${MenuItem.Courses.route}/{id}/description"
|
||||||
|
) {
|
||||||
|
val id = it.arguments!!.getLong("id")
|
||||||
|
CourseDescriptionScreen(tools, id)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("id") { type = NavType.LongType }
|
||||||
|
),
|
||||||
|
route = "${MenuItem.Courses.route}/{id}/announcements"
|
||||||
|
) {
|
||||||
|
val id = it.arguments!!.getLong("id")
|
||||||
|
CourseAnnouncementsScreen(tools, id)
|
||||||
|
}
|
||||||
|
composable(
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("id") { type = NavType.LongType },
|
||||||
|
navArgument("announceId") { type = NavType.LongType }
|
||||||
|
),
|
||||||
|
route = "${MenuItem.Courses.route}/{id}/announcements/{announceId}"
|
||||||
|
) {
|
||||||
|
val id = it.arguments!!.getLong("id")
|
||||||
|
val announceId = it.arguments!!.getLong("announceId")
|
||||||
|
CourseAnnouncementScreen(tools, id, announceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package de.sebse.fuplanner2.ui.courses
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import de.sebse.fuplanner2.R
|
|
||||||
import de.sebse.fuplanner2.database.Course
|
|
||||||
import de.sebse.fuplanner2.ui.CaptionHolder
|
|
||||||
import de.sebse.fuplanner2.ui.CustomHolder
|
|
||||||
import de.sebse.fuplanner2.ui.ListItemHolder
|
|
||||||
import de.sebse.fuplanner2.ui.ViewHolderGenerator
|
|
||||||
import de.sebse.fuplanner2.utils.cast
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class CoursesAdapter(private val onclick: (Course) -> Unit) : RecyclerView.Adapter<CustomHolder>() {
|
|
||||||
|
|
||||||
private val positionalData: ArrayList<Any> = arrayListOf()
|
|
||||||
var dataset: List<Course> = listOf()
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
positionalData.clear()
|
|
||||||
var last: Course? = null
|
|
||||||
dataset.forEach {
|
|
||||||
if (last?.let { last -> it < last } != false)
|
|
||||||
positionalData.add(Pair(it.isSummerSemester, it.year))
|
|
||||||
positionalData.add(it)
|
|
||||||
last = it
|
|
||||||
}
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomHolder {
|
|
||||||
return ViewHolderGenerator.getHolderByType(parent, viewType)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: CustomHolder, position: Int) {
|
|
||||||
// val viewType = getItemViewType(position)
|
|
||||||
val res = holder.itemView.resources
|
|
||||||
when (holder) {
|
|
||||||
is CaptionHolder -> cast<Pair<Boolean, Int?>>(positionalData[position])?.let { (isSummer, year) ->
|
|
||||||
holder.string.text = when {
|
|
||||||
year == null -> res.getString(R.string.projects)
|
|
||||||
isSummer -> res.getString(R.string.summer_semester, year)
|
|
||||||
else -> res.getString(R.string.winter_semester, year, year+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is ListItemHolder -> cast<Course>(positionalData[position])?.let {
|
|
||||||
holder.title.text = it.title
|
|
||||||
holder.subLeft.text = it.lecturers.filter { lecturer -> lecturer.isResponsible }.joinToString { lecturer ->
|
|
||||||
res.getString(R.string.full_name, lecturer.fistName.substring(0, 1)+".", lecturer.lastName)
|
|
||||||
}
|
|
||||||
holder.subRight.text = it.type
|
|
||||||
holder.itemView.setOnClickListener { _ -> this.onclick(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
|
||||||
return when (positionalData[position]) {
|
|
||||||
is Course -> ViewHolderGenerator.HolderType.ITEM.ordinal
|
|
||||||
else -> ViewHolderGenerator.HolderType.HEADER.ordinal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the size of your dataset (invoked by the layout manager)
|
|
||||||
override fun getItemCount() = positionalData.size
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package de.sebse.fuplanner2.ui.courses
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import androidx.work.workDataOf
|
|
||||||
import de.sebse.fuplanner2.R
|
|
||||||
import de.sebse.fuplanner2.auth.AppAccounts
|
|
||||||
import de.sebse.fuplanner2.database.Course
|
|
||||||
import de.sebse.fuplanner2.databinding.ActivityMainBinding
|
|
||||||
import de.sebse.fuplanner2.databinding.FragmentRefreshRecyclerBinding
|
|
||||||
import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_ACCOUNT_NAME
|
|
||||||
import de.sebse.fuplanner2.worker.CourseWorker
|
|
||||||
|
|
||||||
class CoursesFragment : Fragment() {
|
|
||||||
|
|
||||||
private lateinit var coursesViewModel: CoursesViewModel
|
|
||||||
private lateinit var navController: NavController
|
|
||||||
private lateinit var binding: FragmentRefreshRecyclerBinding
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
val viewManager = LinearLayoutManager(context)
|
|
||||||
val viewAdapter = CoursesAdapter(this::onClick)
|
|
||||||
coursesViewModel = ViewModelProvider(this).get(CoursesViewModel::class.java)
|
|
||||||
binding = FragmentRefreshRecyclerBinding.inflate(inflater, container, false)
|
|
||||||
return inflater.inflate(R.layout.fragment_refresh_recycler, container, false).apply {
|
|
||||||
binding.recyclerView.apply {
|
|
||||||
//setHasFixedSize(true)
|
|
||||||
layoutManager = viewManager
|
|
||||||
adapter = viewAdapter
|
|
||||||
}
|
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
|
||||||
val work = OneTimeWorkRequestBuilder<CourseWorker>()
|
|
||||||
.setInputData(workDataOf(
|
|
||||||
KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name
|
|
||||||
))
|
|
||||||
.build()
|
|
||||||
WorkManager.getInstance(context.applicationContext)
|
|
||||||
.enqueue(work)
|
|
||||||
WorkManager.getInstance(context.applicationContext)
|
|
||||||
.getWorkInfoByIdLiveData(work.id)
|
|
||||||
.observe(viewLifecycleOwner, {
|
|
||||||
if (it.state.isFinished)
|
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
coursesViewModel.text.observe(viewLifecycleOwner, Observer {
|
|
||||||
viewAdapter.dataset = it
|
|
||||||
})
|
|
||||||
navController = findNavController()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onClick(course: Course) {
|
|
||||||
course.uid?.let {
|
|
||||||
navController.navigate(CoursesFragmentDirections.actionNavHomeToCourseDetails(it, course.title))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.courses
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.material.icons.outlined.Person
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||||
|
import de.sebse.fuplanner2.MenuItem
|
||||||
|
import de.sebse.fuplanner2.R
|
||||||
|
import de.sebse.fuplanner2.Tools
|
||||||
|
import de.sebse.fuplanner2.database.Course
|
||||||
|
import de.sebse.fuplanner2.ui.theme.*
|
||||||
|
import de.sebse.fuplanner2.ui.tools.previews.CoursePreviewProvider
|
||||||
|
import de.sebse.fuplanner2.ui.tools.viewmodels.CoursesViewModel
|
||||||
|
import de.sebse.fuplanner2.utils.color.getColor
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CoursesScreen(tools: Tools) {
|
||||||
|
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
|
||||||
|
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
|
||||||
|
}
|
||||||
|
val coursesViewModel = ViewModelProvider(viewModelStoreOwner)[CoursesViewModel::class.java]
|
||||||
|
val courses = coursesViewModel.list.observeAsState(listOf()).value
|
||||||
|
val groups =
|
||||||
|
courses.groupBy { (if (it.isSummerSemester) "+" else "-") + (it.year ?: 0) }
|
||||||
|
|
||||||
|
val title = stringResource(R.string.menu_courses)
|
||||||
|
LaunchedEffect(title) {
|
||||||
|
tools.setTitle(title)
|
||||||
|
}
|
||||||
|
AppTheme {
|
||||||
|
GroupedCourseList(groups = groups) { course ->
|
||||||
|
course.uid?.let {
|
||||||
|
tools.navTo("${MenuItem.Courses.route}/$it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun GroupedCourseList(groups: Map<String, List<Course>>, onclick: (course: Course) -> Unit) {
|
||||||
|
val headerBg =
|
||||||
|
if (MaterialTheme.colors.isLight) md_theme_light_secondaryContainer
|
||||||
|
else md_theme_dark_secondaryContainer
|
||||||
|
val headerColor =
|
||||||
|
if (MaterialTheme.colors.isLight) md_theme_light_onSecondaryContainer
|
||||||
|
else md_theme_dark_onSecondaryContainer
|
||||||
|
LazyColumn {
|
||||||
|
for ((key, value) in groups.entries) {
|
||||||
|
stickyHeader(key = key) {
|
||||||
|
Text(
|
||||||
|
text = key,
|
||||||
|
color = headerColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(headerBg)
|
||||||
|
.padding()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(value) { course ->
|
||||||
|
CourseItem(course) { onclick(course) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseList(courses: List<Course>, onclick: () -> Unit) {
|
||||||
|
LazyColumn {
|
||||||
|
items(courses) { course -> CourseItem(course, onclick) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseItem(course: Course, onclick: () -> Unit) {
|
||||||
|
@Suppress("SimplifiableCallChain")
|
||||||
|
CourseItem(
|
||||||
|
id = course.uid,
|
||||||
|
title = course.title,
|
||||||
|
lecturers = course.lecturers
|
||||||
|
.filter { lecturer -> lecturer.isResponsible }
|
||||||
|
.map { lecturer ->
|
||||||
|
stringResource(
|
||||||
|
R.string.full_name,
|
||||||
|
lecturer.firstName.substring(0, 1) + ".",
|
||||||
|
lecturer.lastName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.joinToString(),
|
||||||
|
type = course.type,
|
||||||
|
onclick = onclick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseItem(id: Long?, title: String, lecturers: String, type: String, onclick: () -> Unit) {
|
||||||
|
val color = Color(getColor(LocalContext.current, id ?: 0, highContrast = true))
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onclick)
|
||||||
|
.padding(dimensionResource(R.dimen.card_view_margin)),
|
||||||
|
elevation = dimensionResource(R.dimen.card_view_elevation)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(1.5.dp)
|
||||||
|
.background(
|
||||||
|
Brush.horizontalGradient(
|
||||||
|
colors = listOf(color, MaterialTheme.colors.surface)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(dimensionResource(R.dimen.card_view_padding))
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = MaterialTheme.colors.primaryVariant,
|
||||||
|
style = MaterialTheme.typography.h6
|
||||||
|
)
|
||||||
|
CourseItemHint(
|
||||||
|
icon = Icons.Outlined.Person,
|
||||||
|
imageAltRes = R.string.lecturers,
|
||||||
|
text = lecturers
|
||||||
|
)
|
||||||
|
CourseItemHint(
|
||||||
|
icon = Icons.Outlined.Info,
|
||||||
|
imageAltRes = R.string.course_type,
|
||||||
|
text = type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseItemHint(icon: ImageVector, @StringRes imageAltRes: Int, text: String) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = stringResource(imageAltRes),
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = dimensionResource(R.dimen.card_view_padding)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.subtitle1,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = dimensionResource(R.dimen.card_view_padding)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun CourseListPreview() {
|
||||||
|
AppTheme {
|
||||||
|
CourseList(
|
||||||
|
CoursePreviewProvider().values.take(3).toList()
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun CourseItemPreview(@PreviewParameter(CoursePreviewProvider::class, 1) course: Course) {
|
||||||
|
AppTheme {
|
||||||
|
CourseItem(course) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.details
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.sebse.fuplanner2.MenuItem
|
||||||
|
import de.sebse.fuplanner2.R
|
||||||
|
import de.sebse.fuplanner2.Tools
|
||||||
|
import de.sebse.fuplanner2.database.Announcement
|
||||||
|
import de.sebse.fuplanner2.database.Course
|
||||||
|
import de.sebse.fuplanner2.database.Event
|
||||||
|
import de.sebse.fuplanner2.database.Lecturer
|
||||||
|
import de.sebse.fuplanner2.ui.details.components.AnnouncementItem
|
||||||
|
import de.sebse.fuplanner2.ui.details.components.EventItem
|
||||||
|
import de.sebse.fuplanner2.ui.details.components.LecturerItem
|
||||||
|
import de.sebse.fuplanner2.ui.details.components.QuickLinks
|
||||||
|
import de.sebse.fuplanner2.ui.shared.Heading
|
||||||
|
import de.sebse.fuplanner2.ui.theme.AppTheme
|
||||||
|
import de.sebse.fuplanner2.ui.tools.previews.AnnouncementPreviewProvider
|
||||||
|
import de.sebse.fuplanner2.ui.tools.previews.CoursePreviewProvider
|
||||||
|
import de.sebse.fuplanner2.ui.tools.previews.EventPreviewProvider
|
||||||
|
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModel
|
||||||
|
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModelFactory
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseDetailsScreen(tools: Tools, id: Long) {
|
||||||
|
val coursesViewModel: DetailsViewModel = viewModel(factory = DetailsViewModelFactory(id))
|
||||||
|
val course by coursesViewModel.course.observeAsState()
|
||||||
|
val announcements by coursesViewModel.announcements.observeAsState()
|
||||||
|
val events by coursesViewModel.events.observeAsState()
|
||||||
|
val title = course?.title
|
||||||
|
val context = LocalContext.current
|
||||||
|
LaunchedEffect(title) {
|
||||||
|
title?.let { tools.setTitle(it) }
|
||||||
|
}
|
||||||
|
LaunchedEffect(true) {
|
||||||
|
coursesViewModel.refresh(context)
|
||||||
|
}
|
||||||
|
CourseDetailsScreen(
|
||||||
|
course?.lecturers ?: emptyList(),
|
||||||
|
announcements ?: emptyList(),
|
||||||
|
events ?: emptyList(),
|
||||||
|
id,
|
||||||
|
course?.title ?: ""
|
||||||
|
) {
|
||||||
|
tools.navTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseDetailsScreen(
|
||||||
|
lecturers: List<Lecturer>,
|
||||||
|
announcements: List<Announcement>,
|
||||||
|
events: List<Event>,
|
||||||
|
id: Long,
|
||||||
|
title: String,
|
||||||
|
onClick: (String) -> Unit
|
||||||
|
) {
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
QuickLinks(courseId = id, onClick)
|
||||||
|
if (lecturers.isNotEmpty()) Heading(stringResource(R.string.lecturers))
|
||||||
|
}
|
||||||
|
items(lecturers.sortedBy { (if (it.isResponsible) "AAAA" else "ZZZZ") + it.lastName }) {
|
||||||
|
LecturerItem(lecturer = it, courseTitle = title)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
if (announcements.isNotEmpty()) Heading(stringResource(R.string.announcements))
|
||||||
|
}
|
||||||
|
items(items = announcements.subList(0, min(announcements.size, 3)) ?: listOf()) {
|
||||||
|
AnnouncementItem(it) {
|
||||||
|
onClick("${MenuItem.Courses.route}/$id/announcements/${it.uid}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
if (events.isNotEmpty()) Heading(stringResource(R.string.events))
|
||||||
|
}
|
||||||
|
items(items = events.subList(0, min(events.size, 3))) {
|
||||||
|
EventItem(it)
|
||||||
|
}
|
||||||
|
// TODO: Add current assignments, upcoming events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun CourseDetailsScreenPreview(@PreviewParameter(CoursePreviewProvider::class, 1) course: Course) {
|
||||||
|
AppTheme {
|
||||||
|
CourseDetailsScreen(
|
||||||
|
course.lecturers,
|
||||||
|
AnnouncementPreviewProvider().values.take(3).toList(),
|
||||||
|
EventPreviewProvider().values.take(3).toList(),
|
||||||
|
course.uid!!,
|
||||||
|
course.title
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package de.sebse.fuplanner2.ui.details
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import de.sebse.fuplanner2.R
|
|
||||||
import de.sebse.fuplanner2.database.Lecturer
|
|
||||||
import de.sebse.fuplanner2.ui.*
|
|
||||||
import de.sebse.fuplanner2.utils.cast
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class DetailsAdapter(private val onQuickLink: (ButtonTypes) -> Unit, private val onMailTo: (Lecturer) -> Unit) : RecyclerView.Adapter<CustomHolder>() {
|
|
||||||
|
|
||||||
enum class HeaderTypes {
|
|
||||||
BUTTONS, LECTURER, ANNOUNCEMENTS, ASSIGNMENTS, EVENTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class ButtonTypes {
|
|
||||||
DESCRIPTION, RESOURCES, GRADEBOOK, ANNOUNCEMENTS, ASSIGNMENTS, EVENTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private val positionalData: ArrayList<Any> = arrayListOf()
|
|
||||||
|
|
||||||
var lecturers: List<Lecturer> = listOf()
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
updatePositionalData()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePositionalData() {
|
|
||||||
positionalData.clear()
|
|
||||||
positionalData.add(HeaderTypes.BUTTONS)
|
|
||||||
positionalData.add(ViewHolderGenerator.HolderType.BUTTONS)
|
|
||||||
positionalData.add(HeaderTypes.LECTURER)
|
|
||||||
positionalData.addAll(lecturers.sortedBy { it.lastName }.sortedBy { !it.isResponsible })
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomHolder {
|
|
||||||
return ViewHolderGenerator.getHolderByType(parent, viewType)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: CustomHolder, position: Int) {
|
|
||||||
// val viewType = getItemViewType(position)
|
|
||||||
val res = holder.itemView.resources
|
|
||||||
when (holder) {
|
|
||||||
is CaptionHolder -> cast<HeaderTypes>(positionalData[position])?.let { type ->
|
|
||||||
holder.string.text = when (type) {
|
|
||||||
HeaderTypes.BUTTONS -> res.getString(R.string.quick_links)
|
|
||||||
HeaderTypes.LECTURER -> res.getString(R.string.lecturers)
|
|
||||||
HeaderTypes.ANNOUNCEMENTS -> res.getString(R.string.announcements)
|
|
||||||
HeaderTypes.ASSIGNMENTS -> res.getString(R.string.assignments)
|
|
||||||
HeaderTypes.EVENTS -> res.getString(R.string.events)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is QuickLinksHolder -> {
|
|
||||||
holder.btnAnnouncements.setOnClickListener { this.onQuickLink(ButtonTypes.ANNOUNCEMENTS) }
|
|
||||||
holder.btnAssignments.setOnClickListener { this.onQuickLink(ButtonTypes.ASSIGNMENTS) }
|
|
||||||
holder.btnDescription.setOnClickListener { this.onQuickLink(ButtonTypes.DESCRIPTION) }
|
|
||||||
holder.btnEvents.setOnClickListener { this.onQuickLink(ButtonTypes.EVENTS) }
|
|
||||||
holder.btnGradebook.setOnClickListener { this.onQuickLink(ButtonTypes.GRADEBOOK) }
|
|
||||||
holder.btnResources.setOnClickListener { this.onQuickLink(ButtonTypes.RESOURCES) }
|
|
||||||
}
|
|
||||||
is MailHolder -> cast<Lecturer>(positionalData[position])?.let { type ->
|
|
||||||
holder.title.text = res.getString(R.string.full_name, type.fistName, type.lastName)
|
|
||||||
holder.subLeft.text = type.email
|
|
||||||
holder.itemView.setOnClickListener { this.onMailTo(type) }
|
|
||||||
}
|
|
||||||
is ListItemHolder -> when (positionalData[position]) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
|
||||||
return when (positionalData[position]) {
|
|
||||||
is HeaderTypes -> ViewHolderGenerator.HolderType.HEADER.ordinal
|
|
||||||
ViewHolderGenerator.HolderType.BUTTONS -> ViewHolderGenerator.HolderType.BUTTONS.ordinal
|
|
||||||
is Lecturer -> ViewHolderGenerator.HolderType.MAIL.ordinal
|
|
||||||
else -> ViewHolderGenerator.HolderType.ITEM.ordinal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the size of your dataset (invoked by the layout manager)
|
|
||||||
override fun getItemCount() = positionalData.size
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package de.sebse.fuplanner2.ui.details
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import androidx.navigation.fragment.navArgs
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.work.workDataOf
|
|
||||||
import de.sebse.fuplanner2.R
|
|
||||||
import de.sebse.fuplanner2.auth.AppAccounts
|
|
||||||
import de.sebse.fuplanner2.database.Lecturer
|
|
||||||
import de.sebse.fuplanner2.databinding.FragmentRefreshRecyclerBinding
|
|
||||||
import de.sebse.fuplanner2.utils.enqueueOneTimeWork
|
|
||||||
import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_ACCOUNT_NAME
|
|
||||||
import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_COURSE_ID
|
|
||||||
import de.sebse.fuplanner2.worker.CourseWorker
|
|
||||||
|
|
||||||
|
|
||||||
class DetailsFragment : Fragment() {
|
|
||||||
|
|
||||||
private var title: String = ""
|
|
||||||
private val args: DetailsFragmentArgs by navArgs()
|
|
||||||
private val detailsViewModel: DetailsViewModel by viewModels { DetailsViewModelFactory(args.courseId) }
|
|
||||||
private lateinit var navController: NavController
|
|
||||||
private lateinit var binding: FragmentRefreshRecyclerBinding
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
val viewManager = LinearLayoutManager(context)
|
|
||||||
val viewAdapter = DetailsAdapter(this::launchFragment, this::sendMail)
|
|
||||||
return inflater.inflate(R.layout.fragment_refresh_recycler, container, false).apply {
|
|
||||||
binding.recyclerView.apply {
|
|
||||||
setHasFixedSize(true)
|
|
||||||
|
|
||||||
layoutManager = viewManager
|
|
||||||
adapter = viewAdapter
|
|
||||||
}
|
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
|
||||||
enqueueOneTimeWork<CourseWorker>(context.applicationContext) {
|
|
||||||
it.setInputData(workDataOf(
|
|
||||||
KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name,
|
|
||||||
KEY_COURSE_ID to args.courseId
|
|
||||||
))
|
|
||||||
}.observe(viewLifecycleOwner, Observer {
|
|
||||||
if (it.state.isFinished)
|
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
detailsViewModel.course.observe(viewLifecycleOwner, Observer {
|
|
||||||
viewAdapter.lecturers = it.lecturers
|
|
||||||
this@DetailsFragment.title = it.title
|
|
||||||
})
|
|
||||||
navController = findNavController()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendMail(lecturer: Lecturer) {
|
|
||||||
val intent = Intent(Intent.ACTION_SENDTO)
|
|
||||||
intent.type = "text/html"
|
|
||||||
intent.data = Uri.fromParts("mailto", lecturer.email, null)
|
|
||||||
intent.putExtra(Intent.EXTRA_SUBJECT, this.title)
|
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.email_preview, lecturer.fistName, lecturer.lastName))
|
|
||||||
startActivity(Intent.createChooser(intent, getString(R.string.send_email, lecturer.fistName, lecturer.lastName)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchFragment(btnType: DetailsAdapter.ButtonTypes) {
|
|
||||||
when (btnType) {
|
|
||||||
DetailsAdapter.ButtonTypes.DESCRIPTION ->
|
|
||||||
this.navController.navigate(DetailsFragmentDirections.actionCourseDetailsToDescriptionFragment(args.courseId, title))
|
|
||||||
DetailsAdapter.ButtonTypes.RESOURCES -> TODO()
|
|
||||||
DetailsAdapter.ButtonTypes.GRADEBOOK -> TODO()
|
|
||||||
DetailsAdapter.ButtonTypes.ANNOUNCEMENTS ->
|
|
||||||
this.navController.navigate(DetailsFragmentDirections.actionCourseDetailsToCourseAnnouncements(args.courseId, title))
|
|
||||||
DetailsAdapter.ButtonTypes.ASSIGNMENTS -> TODO()
|
|
||||||
DetailsAdapter.ButtonTypes.EVENTS ->
|
|
||||||
this.navController.navigate(DetailsFragmentDirections.actionCourseDetailsToCourseEvents(args.courseId, title))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package de.sebse.fuplanner2.ui.details
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import de.sebse.fuplanner2.database.AppDatabase
|
|
||||||
import de.sebse.fuplanner2.database.Course
|
|
||||||
|
|
||||||
|
|
||||||
class DetailsViewModelFactory(private val courseId: Long): ViewModelProvider.NewInstanceFactory() {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = DetailsViewModel(courseId) as T
|
|
||||||
}
|
|
||||||
|
|
||||||
class DetailsViewModel(courseId: Long) : ViewModel() {
|
|
||||||
val course: LiveData<Course> = AppDatabase.getInstance().courseDao().getCourseById(courseId)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import de.sebse.fuplanner2.database.Announcement
|
||||||
|
import de.sebse.fuplanner2.ui.shared.FuCardColumn
|
||||||
|
import de.sebse.fuplanner2.ui.theme.AppTheme
|
||||||
|
import de.sebse.fuplanner2.ui.tools.previews.AnnouncementPreviewProvider
|
||||||
|
import de.sebse.fuplanner2.utils.toDateTimeString
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnnouncementItem(announcement: Announcement, click: () -> Unit) {
|
||||||
|
FuCardColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(true, onClick = click)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = announcement.title,
|
||||||
|
style = MaterialTheme.typography.h6
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = announcement.createdOn.toDateTimeString(LocalContext.current) ?: "",
|
||||||
|
style = MaterialTheme.typography.subtitle1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AnnouncementItemPreview(@PreviewParameter(AnnouncementPreviewProvider::class, 5) announcement: Announcement) {
|
||||||
|
AppTheme {
|
||||||
|
AnnouncementItem(announcement) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import de.sebse.fuplanner2.database.Event
|
||||||
|
import de.sebse.fuplanner2.ui.shared.FuCardColumn
|
||||||
|
import de.sebse.fuplanner2.ui.theme.AppTheme
|
||||||
|
import de.sebse.fuplanner2.ui.tools.previews.EventPreviewProvider
|
||||||
|
import de.sebse.fuplanner2.utils.toDateTimeString
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EventItem(event: Event) {
|
||||||
|
EventItem(event) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EventItem(event: Event, click: () -> Unit) {
|
||||||
|
FuCardColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(true, onClick = click)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = event.title,
|
||||||
|
style = MaterialTheme.typography.h6
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = event.startDateTime.toDateTimeString(LocalContext.current) ?: "",
|
||||||
|
style = MaterialTheme.typography.subtitle1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun EventItemPreview(@PreviewParameter(EventPreviewProvider::class, 5) event: Event) {
|
||||||
|
AppTheme {
|
||||||
|
EventItem(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.details.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Email
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import de.sebse.fuplanner2.R
|
||||||
|
import de.sebse.fuplanner2.database.Lecturer
|
||||||
|
import de.sebse.fuplanner2.ui.shared.FuCardRow
|
||||||
|
import de.sebse.fuplanner2.ui.theme.AppTheme
|
||||||
|
import de.sebse.fuplanner2.ui.tools.previews.LecturerPreviewProvider
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LecturerItem(lecturer: Lecturer, courseTitle: String) {
|
||||||
|
val mailTemplate = stringResource(R.string.email_preview, lecturer.firstName, lecturer.lastName)
|
||||||
|
val fullName = stringResource(R.string.send_email, lecturer.firstName, lecturer.lastName)
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
LecturerItem(lecturer) {
|
||||||
|
sendMail(ctx, mailTemplate, fullName, lecturer.email, courseTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LecturerItem(lecturer: Lecturer, click: () -> Unit) {
|
||||||
|
FuCardRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(true, onClick = click)
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
val textDecor = if (lecturer.isResponsible) TextDecoration.Underline else null
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
R.string.full_name,
|
||||||
|
lecturer.firstName,
|
||||||
|
lecturer.lastName
|
||||||
|
),
|
||||||
|
textDecoration = textDecor,
|
||||||
|
style = MaterialTheme.typography.h6
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = lecturer.email,
|
||||||
|
style = MaterialTheme.typography.subtitle1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Email,
|
||||||
|
contentDescription = stringResource(R.string.mail_icon),
|
||||||
|
tint = MaterialTheme.colors.secondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.aspectRatio(1f, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun LecturerItemPreview(@PreviewParameter(LecturerPreviewProvider::class, 3) lecturer: Lecturer) {
|
||||||
|
AppTheme {
|
||||||
|
LecturerItem(lecturer, "Course Name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendMail(
|
||||||
|
ctx: Context,
|
||||||
|
mailTemplate: String,
|
||||||
|
fullName: String,
|
||||||
|
mail: String,
|
||||||
|
courseTitle: String
|
||||||
|
) {
|
||||||
|
val intent = Intent(Intent.ACTION_SENDTO)
|
||||||
|
intent.type = "text/html"
|
||||||
|
intent.data = Uri.fromParts("mailto", mail, null)
|
||||||
|
intent.putExtra(Intent.EXTRA_SUBJECT, courseTitle)
|
||||||
|
intent.putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
mailTemplate
|
||||||
|
)
|
||||||
|
ctx.startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
intent,
|
||||||
|
fullName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.details.components
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.GridCells
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonDefaults.buttonColors
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.sebse.fuplanner2.R
|
||||||
|
import de.sebse.fuplanner2.ui.shared.VerticalGrid
|
||||||
|
import de.sebse.fuplanner2.ui.theme.AppTheme
|
||||||
|
|
||||||
|
|
||||||
|
data class QuickLinkProps(@StringRes val name: Int, val route: String)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun QuickLinks(courseId: Long, onClick: (String) -> Unit) {
|
||||||
|
val list = listOf(
|
||||||
|
QuickLinkProps(R.string.description, "courses/$courseId/description"),
|
||||||
|
QuickLinkProps(R.string.resources, "courses/$courseId/resources"),
|
||||||
|
QuickLinkProps(R.string.assignments, "courses/$courseId/assignments"),
|
||||||
|
QuickLinkProps(R.string.announcements, "courses/$courseId/announcements"),
|
||||||
|
QuickLinkProps(R.string.events, "courses/$courseId/events")
|
||||||
|
)
|
||||||
|
VerticalGrid(
|
||||||
|
cells = GridCells.Adaptive(150.dp)
|
||||||
|
) {
|
||||||
|
list.forEach {
|
||||||
|
Button(
|
||||||
|
colors = buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.secondary,
|
||||||
|
contentColor = MaterialTheme.colors.onSecondary,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(dimensionResource(R.dimen.card_view_margin))
|
||||||
|
.fillMaxWidth(),
|
||||||
|
onClick = { onClick(it.route) }
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(it.name),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.h6
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun QuickLinksPreview() {
|
||||||
|
AppTheme {
|
||||||
|
QuickLinks(3) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package de.sebse.fuplanner2.ui.details_announcements
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.paging.PagedListAdapter
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import de.sebse.fuplanner2.database.Announcement
|
|
||||||
import de.sebse.fuplanner2.ui.ListItemHolder
|
|
||||||
import de.sebse.fuplanner2.utils.toDateTimeString
|
|
||||||
|
|
||||||
class AnnouncementsAdapter() : PagedListAdapter<Announcement, ListItemHolder>(EventDiffCallback()) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemHolder {
|
|
||||||
return ListItemHolder.invoke(parent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ListItemHolder, position: Int) {
|
|
||||||
val event = getItem(position)
|
|
||||||
val actCtx = holder.itemView.context
|
|
||||||
|
|
||||||
event?.let {
|
|
||||||
holder.title.text = it.title
|
|
||||||
holder.subLeft.text = it.createdOn.toDateTimeString(actCtx)
|
|
||||||
holder.subRight.text = it.createdBy
|
|
||||||
} ?: run {
|
|
||||||
holder.clear()
|
|
||||||
holder.itemView.setOnClickListener(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EventDiffCallback : DiffUtil.ItemCallback<Announcement>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Announcement, newItem: Announcement): Boolean {
|
|
||||||
return oldItem.uid == newItem.uid
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Announcement, newItem: Announcement): Boolean {
|
|
||||||
return oldItem.getHash() == newItem.getHash()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package de.sebse.fuplanner2.ui.details_announcements
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import androidx.navigation.fragment.navArgs
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.work.workDataOf
|
|
||||||
import de.sebse.fuplanner2.R
|
|
||||||
import de.sebse.fuplanner2.auth.AppAccounts
|
|
||||||
import de.sebse.fuplanner2.databinding.FragmentRefreshRecyclerBinding
|
|
||||||
import de.sebse.fuplanner2.utils.console
|
|
||||||
import de.sebse.fuplanner2.utils.enqueueOneTimeWork
|
|
||||||
import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_ACCOUNT_NAME
|
|
||||||
import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_COURSE_ID
|
|
||||||
import de.sebse.fuplanner2.worker.AnnouncementWorker
|
|
||||||
|
|
||||||
class AnnouncementsFragment : Fragment() {
|
|
||||||
|
|
||||||
private var title: String = ""
|
|
||||||
private val args: AnnouncementsFragmentArgs by navArgs()
|
|
||||||
private val announcementsViewModel: AnnouncementsViewModel by viewModels { AnnouncementsViewModelFactory(args.courseId) }
|
|
||||||
private lateinit var navController: NavController
|
|
||||||
private lateinit var binding: FragmentRefreshRecyclerBinding
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
val viewManager = LinearLayoutManager(context)
|
|
||||||
val viewAdapter = AnnouncementsAdapter()
|
|
||||||
return inflater.inflate(R.layout.fragment_refresh_recycler, container, false).apply {
|
|
||||||
console.log(findViewById(R.id.recycler_view), binding.recyclerView)
|
|
||||||
binding.recyclerView.apply {
|
|
||||||
setHasFixedSize(true)
|
|
||||||
|
|
||||||
layoutManager = viewManager
|
|
||||||
adapter = viewAdapter
|
|
||||||
}
|
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
|
||||||
enqueueOneTimeWork<AnnouncementWorker>(context.applicationContext) {
|
|
||||||
it.setInputData(workDataOf(
|
|
||||||
KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name,
|
|
||||||
KEY_COURSE_ID to args.courseId
|
|
||||||
))
|
|
||||||
}.observe(viewLifecycleOwner, Observer {
|
|
||||||
if (it.state.isFinished)
|
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
announcementsViewModel.events.observe(viewLifecycleOwner, Observer {
|
|
||||||
viewAdapter.submitList(it)
|
|
||||||
this@AnnouncementsFragment.title = args.title
|
|
||||||
})
|
|
||||||
navController = findNavController()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package de.sebse.fuplanner2.ui.details_announcements
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.paging.LivePagedListBuilder
|
|
||||||
import androidx.paging.PagedList
|
|
||||||
import de.sebse.fuplanner2.database.Announcement
|
|
||||||
import de.sebse.fuplanner2.database.AppDatabase
|
|
||||||
|
|
||||||
class AnnouncementsViewModelFactory(private val courseId: Long): ViewModelProvider.NewInstanceFactory() {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = AnnouncementsViewModel(courseId) as T
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnnouncementsViewModel(courseId: Long) : ViewModel() {
|
|
||||||
private val factory = AppDatabase.getInstance().announcementDao().getAll1(courseId)
|
|
||||||
val events: LiveData<PagedList<Announcement>> = LivePagedListBuilder(factory, 50).build()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.details_announcements
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.sebse.fuplanner2.R
|
||||||
|
import de.sebse.fuplanner2.Tools
|
||||||
|
import de.sebse.fuplanner2.database.Announcement
|
||||||
|
import de.sebse.fuplanner2.ui.shared.Heading
|
||||||
|
import de.sebse.fuplanner2.ui.shared.HtmlText
|
||||||
|
import de.sebse.fuplanner2.ui.theme.AppTheme
|
||||||
|
import de.sebse.fuplanner2.ui.tools.previews.AnnouncementPreviewProvider
|
||||||
|
import de.sebse.fuplanner2.ui.tools.viewmodels.AnnouncementViewModel
|
||||||
|
import de.sebse.fuplanner2.ui.tools.viewmodels.AnnouncementViewModelFactory
|
||||||
|
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModel
|
||||||
|
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModelFactory
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseAnnouncementScreen(tools: Tools, id: Long, announcementId: Long) {
|
||||||
|
val coursesViewModel: DetailsViewModel =
|
||||||
|
viewModel(factory = DetailsViewModelFactory(id))
|
||||||
|
val announcementViewModel: AnnouncementViewModel =
|
||||||
|
viewModel(factory = AnnouncementViewModelFactory(announcementId))
|
||||||
|
val course by coursesViewModel.course.observeAsState()
|
||||||
|
val announcement by announcementViewModel.observeAsState()
|
||||||
|
val title = course?.title ?: stringResource(id = R.string.description)
|
||||||
|
LaunchedEffect(title) {
|
||||||
|
tools.setTitle(title)
|
||||||
|
}
|
||||||
|
announcement?.let { CourseAnnouncementScreen(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseAnnouncementScreen(announcement: Announcement) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
Heading(text = announcement.title)
|
||||||
|
HtmlText(
|
||||||
|
html = announcement.body,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun CourseAnnouncementScreenPreview(@PreviewParameter(AnnouncementPreviewProvider::class, 2) announcement: Announcement) {
|
||||||
|
AppTheme {
|
||||||
|
CourseAnnouncementScreen(announcement)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.details_announcements
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.sebse.fuplanner2.MenuItem
|
||||||
|
import de.sebse.fuplanner2.R
|
||||||
|
import de.sebse.fuplanner2.Tools
|
||||||
|
import de.sebse.fuplanner2.database.Announcement
|
||||||
|
import de.sebse.fuplanner2.ui.details.components.AnnouncementItem
|
||||||
|
import de.sebse.fuplanner2.ui.theme.AppTheme
|
||||||
|
import de.sebse.fuplanner2.ui.tools.previews.AnnouncementPreviewProvider
|
||||||
|
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModel
|
||||||
|
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModelFactory
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseAnnouncementsScreen(tools: Tools, id: Long) {
|
||||||
|
val coursesViewModel: DetailsViewModel =
|
||||||
|
viewModel(factory = DetailsViewModelFactory(id))
|
||||||
|
val course by coursesViewModel.course.observeAsState()
|
||||||
|
val announcements by coursesViewModel.announcements.observeAsState()
|
||||||
|
val title = course?.title ?: stringResource(id = R.string.description)
|
||||||
|
LaunchedEffect(title) {
|
||||||
|
tools.setTitle(title)
|
||||||
|
}
|
||||||
|
announcements?.let { CourseAnnouncementsScreen(it) { uid ->
|
||||||
|
tools.navTo("${MenuItem.Courses.route}/$id/announcements/${uid}")
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseAnnouncementsScreen(announcements: List<Announcement>, onClick: (Long) -> Unit) {
|
||||||
|
LazyColumn {
|
||||||
|
items(announcements) {
|
||||||
|
AnnouncementItem(it) {
|
||||||
|
it.uid?.let { uid -> onClick(uid) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun CourseAnnouncementsScreenPreview() {
|
||||||
|
AppTheme {
|
||||||
|
CourseAnnouncementsScreen(
|
||||||
|
AnnouncementPreviewProvider().values.take(10).toList()
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.details_description
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.sebse.fuplanner2.R
|
||||||
|
import de.sebse.fuplanner2.Tools
|
||||||
|
import de.sebse.fuplanner2.database.Course
|
||||||
|
import de.sebse.fuplanner2.ui.shared.Heading
|
||||||
|
import de.sebse.fuplanner2.ui.shared.HtmlText
|
||||||
|
import de.sebse.fuplanner2.ui.theme.AppTheme
|
||||||
|
import de.sebse.fuplanner2.ui.tools.previews.CoursePreviewProvider
|
||||||
|
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModel
|
||||||
|
import de.sebse.fuplanner2.ui.tools.viewmodels.DetailsViewModelFactory
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseDescriptionScreen(tools: Tools, id: Long) {
|
||||||
|
val coursesViewModel: DetailsViewModel = viewModel(factory = DetailsViewModelFactory(id))
|
||||||
|
val course by coursesViewModel.course.observeAsState()
|
||||||
|
val title = course?.title ?: stringResource(id = R.string.description)
|
||||||
|
LaunchedEffect(title) {
|
||||||
|
tools.setTitle(title)
|
||||||
|
}
|
||||||
|
CourseDescriptionScreen(course?.description ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CourseDescriptionScreen(description: String) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
Heading(text = stringResource(id = R.string.description))
|
||||||
|
HtmlText(
|
||||||
|
html = description,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun CourseDescriptionScreenPreview(@PreviewParameter(CoursePreviewProvider::class, 1) course: Course) {
|
||||||
|
AppTheme {
|
||||||
|
CourseDescriptionScreen(
|
||||||
|
"scrip<b>fsdfsfg</b><br><a href='example.com'>ti</a>on"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package de.sebse.fuplanner2.ui.details_description
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.webkit.WebSettings
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import androidx.navigation.fragment.navArgs
|
|
||||||
import de.sebse.fuplanner2.R
|
|
||||||
import de.sebse.fuplanner2.databinding.FragmentDescriptionBinding
|
|
||||||
import de.sebse.fuplanner2.ui.details.DetailsViewModel
|
|
||||||
import de.sebse.fuplanner2.ui.details.DetailsViewModelFactory
|
|
||||||
|
|
||||||
|
|
||||||
class DescriptionFragment : Fragment() {
|
|
||||||
|
|
||||||
private var title: String = ""
|
|
||||||
private val args: DescriptionFragmentArgs by navArgs()
|
|
||||||
private val detailsViewModel: DetailsViewModel by viewModels { DetailsViewModelFactory(args.courseId) }
|
|
||||||
private lateinit var navController: NavController
|
|
||||||
private lateinit var binding: FragmentDescriptionBinding
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
return inflater.inflate(R.layout.fragment_description, container, false).apply {
|
|
||||||
val nightModeFlags = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
|
||||||
if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
binding.description.settings.forceDark = WebSettings.FORCE_DARK_ON
|
|
||||||
}
|
|
||||||
detailsViewModel.course.observe(viewLifecycleOwner, Observer {
|
|
||||||
binding.description.loadDataWithBaseURL("", it.description, "text/html", "UTF-8", "")
|
|
||||||
})
|
|
||||||
navController = findNavController()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +1,21 @@
|
|||||||
package de.sebse.fuplanner2.ui.schedule
|
package de.sebse.fuplanner2.ui.schedule
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.alamkanak.weekview.WeekView
|
import com.alamkanak.weekview.WeekView
|
||||||
import com.alamkanak.weekview.WeekViewDisplayable
|
|
||||||
import com.alamkanak.weekview.WeekViewEntity
|
import com.alamkanak.weekview.WeekViewEntity
|
||||||
import com.alamkanak.weekview.WeekViewEvent
|
|
||||||
import de.sebse.fuplanner2.R
|
import de.sebse.fuplanner2.R
|
||||||
import de.sebse.fuplanner2.database.AppDatabase
|
import de.sebse.fuplanner2.database.AppDatabase
|
||||||
import de.sebse.fuplanner2.database.Course
|
import de.sebse.fuplanner2.database.Course
|
||||||
import de.sebse.fuplanner2.database.Event
|
import de.sebse.fuplanner2.database.Event
|
||||||
import de.sebse.fuplanner2.ui.schedule.MyCustomPagingAdapter.LoadMoreHandler
|
import de.sebse.fuplanner2.ui.schedule.MyCustomPagingAdapter.LoadMoreHandler
|
||||||
|
import de.sebse.fuplanner2.utils.color.getColor
|
||||||
import de.sebse.fuplanner2.utils.getHtmlSpannedString
|
import de.sebse.fuplanner2.utils.getHtmlSpannedString
|
||||||
import de.sebse.fuplanner2.utils.toTimeString
|
import de.sebse.fuplanner2.utils.toTimeString
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -26,8 +23,6 @@ import kotlinx.coroutines.GlobalScope
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class ScheduleFragment : Fragment() {
|
class ScheduleFragment : Fragment() {
|
||||||
|
|
||||||
@@ -58,7 +53,7 @@ class ScheduleFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
.setPositiveButton(R.string.view_course) { _, _ ->
|
.setPositiveButton(R.string.view_course) { _, _ ->
|
||||||
alertCourse?.let { course ->
|
alertCourse?.let { course ->
|
||||||
navController.navigate(ScheduleFragmentDirections.actionNavScheduleToCourseDetails(course.uid!!, course.title))
|
//navController.navigate(ScheduleFragmentDirections.actionNavScheduleToCourseDetails(course.uid!!, course.title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.create()
|
.create()
|
||||||
@@ -164,56 +159,6 @@ data class ContextEvent(val actCtx: Context, val event: Event) {
|
|||||||
.setStyle(style)
|
.setStyle(style)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
fun getColor(actCtx: Context, seed: Long): Int {
|
|
||||||
var h = Random(seed).nextInt(0xFFFF)
|
|
||||||
h = h * 360 / 0xffff
|
|
||||||
//int s = 0xff & encodedHash[2];
|
|
||||||
//s = s * 100 / 0xffff;
|
|
||||||
//int v = 0xff & encodedHash[3];
|
|
||||||
//v = v * 100 / 0xffff;
|
|
||||||
|
|
||||||
// range for more beautiful colors
|
|
||||||
h = h / 30 * 30
|
|
||||||
val (s, v) =
|
|
||||||
if (actCtx.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
Pair(100, 20)//Pair(100, 80)
|
|
||||||
else
|
|
||||||
Pair(60, 100)
|
|
||||||
val (r, g, b) = hsvToRgb(h / 360.0, s / 100.0, v / 100.0)
|
|
||||||
return rgbToColorInt(r, g, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hsvToRgb(
|
|
||||||
hue: Double,
|
|
||||||
saturation: Double,
|
|
||||||
value: Double
|
|
||||||
): Triple<Double, Double, Double> {
|
|
||||||
val h = (hue * 6).toInt()
|
|
||||||
val f = hue * 6 - h
|
|
||||||
val p = value * (1 - saturation)
|
|
||||||
val q = value * (1 - f * saturation)
|
|
||||||
val t = value * (1 - (1 - f) * saturation)
|
|
||||||
return when (h) {
|
|
||||||
0 -> Triple(value, t, p)
|
|
||||||
1 -> Triple(q, value, p)
|
|
||||||
2 -> Triple(p, value, t)
|
|
||||||
3 -> Triple(p, q, value)
|
|
||||||
4 -> Triple(t, p, value)
|
|
||||||
5 -> Triple(value, p, q)
|
|
||||||
else -> Triple(0.0, 0.0, 0.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
private fun rgbToColorInt(
|
|
||||||
r: Double,
|
|
||||||
g: Double,
|
|
||||||
b: Double
|
|
||||||
): Int {
|
|
||||||
return 0xFF000000.toInt() + (r*0xFF).roundToInt() * 0x10000 + (g*0xFF).roundToInt() * 0x100 + (b*0xFF).roundToInt()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyCustomPagingAdapter(
|
class MyCustomPagingAdapter(
|
||||||
|
|||||||
46
app/src/main/java/de/sebse/fuplanner2/ui/shared/FuCard.kt
Normal file
46
app/src/main/java/de/sebse/fuplanner2/ui/shared/FuCard.kt
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.shared
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import de.sebse.fuplanner2.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FuCardColumn(
|
||||||
|
modifier: Modifier,
|
||||||
|
content :@Composable (ColumnScope.() -> Unit)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(dimensionResource(R.dimen.card_view_margin)),
|
||||||
|
elevation = dimensionResource(R.dimen.card_view_elevation)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(dimensionResource(R.dimen.card_view_padding)),
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FuCardRow(
|
||||||
|
modifier: Modifier,
|
||||||
|
content :@Composable (RowScope.() -> Unit)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(dimensionResource(R.dimen.card_view_margin)),
|
||||||
|
elevation = dimensionResource(R.dimen.card_view_elevation)
|
||||||
|
) {
|
||||||
|
Row (
|
||||||
|
modifier = modifier
|
||||||
|
.padding(dimensionResource(R.dimen.card_view_padding)),
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/src/main/java/de/sebse/fuplanner2/ui/shared/Heading.kt
Normal file
49
app/src/main/java/de/sebse/fuplanner2/ui/shared/Heading.kt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.shared
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import de.sebse.fuplanner2.R
|
||||||
|
import de.sebse.fuplanner2.ui.theme.AppTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Heading(text: String, onClick: (() -> Unit)? = null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.h5,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = dimensionResource(id = R.dimen.header_padding))
|
||||||
|
)
|
||||||
|
if (onClick != null) Text(
|
||||||
|
text = "More >>",
|
||||||
|
style = MaterialTheme.typography.subtitle2,
|
||||||
|
textDecoration = TextDecoration.Underline,
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = dimensionResource(id = R.dimen.header_padding))
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun HeadingPreview() {
|
||||||
|
AppTheme {
|
||||||
|
Heading("Super cool") { }
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/src/main/java/de/sebse/fuplanner2/ui/shared/HtmlText.kt
Normal file
21
app/src/main/java/de/sebse/fuplanner2/ui/shared/HtmlText.kt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.shared
|
||||||
|
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HtmlText(html: String, modifier: Modifier = Modifier) {
|
||||||
|
AndroidView(
|
||||||
|
modifier = modifier,
|
||||||
|
factory = { context -> TextView(context) },
|
||||||
|
update = {
|
||||||
|
it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
|
||||||
|
it.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
it.setTextIsSelectable(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.shared
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.lazy.GridCells
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun VerticalGrid(
|
||||||
|
cells: GridCells,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val columns: Int = when (cells) {
|
||||||
|
is GridCells.Fixed -> {
|
||||||
|
cells.count
|
||||||
|
}
|
||||||
|
is GridCells.Adaptive -> {
|
||||||
|
val width = LocalContext.current.resources.displayMetrics.widthPixels
|
||||||
|
val columnWidthPx = with(LocalDensity.current) { cells.minSize.toPx() }
|
||||||
|
((width / columnWidthPx).toInt()).coerceAtLeast(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout(
|
||||||
|
content = content,
|
||||||
|
modifier = modifier
|
||||||
|
) { measurables, constraints ->
|
||||||
|
val itemWidth = constraints.maxWidth / columns
|
||||||
|
// Keep given height constraints, but set an exact width
|
||||||
|
val itemConstraints = constraints.copy(
|
||||||
|
minWidth = itemWidth,
|
||||||
|
maxWidth = itemWidth
|
||||||
|
)
|
||||||
|
// Measure each item with these constraints
|
||||||
|
val placeables = measurables.map { measurable ->
|
||||||
|
measurable.measure(itemConstraints)
|
||||||
|
}
|
||||||
|
// Track each columns height so we can calculate the overall height
|
||||||
|
val columnHeights = Array(columns) { 0 }
|
||||||
|
placeables.forEachIndexed { index, placeable ->
|
||||||
|
val column = index % columns
|
||||||
|
columnHeights[column] += placeable.height
|
||||||
|
}
|
||||||
|
val height = (columnHeights.maxOrNull() ?: constraints.minHeight)
|
||||||
|
.coerceAtMost(constraints.maxHeight)
|
||||||
|
layout(constraints.maxWidth, height) {
|
||||||
|
// Track the Y co-ord per column we have placed up to
|
||||||
|
val columnY = Array(columns) { 0 }
|
||||||
|
placeables.forEachIndexed { index, placeable ->
|
||||||
|
val column = index % columns
|
||||||
|
placeable.place(
|
||||||
|
x = column * itemWidth,
|
||||||
|
y = columnY[column]
|
||||||
|
)
|
||||||
|
columnY[column] += placeable.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
app/src/main/java/de/sebse/fuplanner2/ui/theme/Color.kt
Normal file
58
app/src/main/java/de/sebse/fuplanner2/ui/theme/Color.kt
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.theme
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
|
||||||
|
val md_theme_light_primary = Color(0xFF245fa7)
|
||||||
|
val md_theme_light_onPrimary = Color(0xFFffffff)
|
||||||
|
val md_theme_light_primaryContainer = Color(0xFFd4e3ff)
|
||||||
|
val md_theme_light_onPrimaryContainer = Color(0xFF001b3d)
|
||||||
|
val md_theme_light_secondary = Color(0xFF4a6800)
|
||||||
|
val md_theme_light_onSecondary = Color(0xFFffffff)
|
||||||
|
val md_theme_light_secondaryContainer = Color(0xFFbef43c)
|
||||||
|
val md_theme_light_onSecondaryContainer = Color(0xFF131f00)
|
||||||
|
val md_theme_light_tertiary = Color(0xFF8b5000)
|
||||||
|
val md_theme_light_onTertiary = Color(0xFFffffff)
|
||||||
|
val md_theme_light_tertiaryContainer = Color(0xFFffdcba)
|
||||||
|
val md_theme_light_onTertiaryContainer = Color(0xFF2d1600)
|
||||||
|
val md_theme_light_error = Color(0xFFba1b1b)
|
||||||
|
val md_theme_light_errorContainer = Color(0xFFffdad4)
|
||||||
|
val md_theme_light_onError = Color(0xFFffffff)
|
||||||
|
val md_theme_light_onErrorContainer = Color(0xFF410001)
|
||||||
|
val md_theme_light_background = Color(0xFFfdfbff)
|
||||||
|
val md_theme_light_onBackground = Color(0xFF1b1b1d)
|
||||||
|
val md_theme_light_surface = Color(0xFFfdfbff)
|
||||||
|
val md_theme_light_onSurface = Color(0xFF1b1b1d)
|
||||||
|
val md_theme_light_surfaceVariant = Color(0xFFe0e2eb)
|
||||||
|
val md_theme_light_onSurfaceVariant = Color(0xFF44474f)
|
||||||
|
val md_theme_light_outline = Color(0xFF74777f)
|
||||||
|
val md_theme_light_inverseOnSurface = Color(0xFFf1f0f4)
|
||||||
|
val md_theme_light_inverseSurface = Color(0xFF2f3033)
|
||||||
|
|
||||||
|
val md_theme_dark_primary = Color(0xFFa6c8ff)
|
||||||
|
val md_theme_dark_onPrimary = Color(0xFF003063)
|
||||||
|
val md_theme_dark_primaryContainer = Color(0xFF00468b)
|
||||||
|
val md_theme_dark_onPrimaryContainer = Color(0xFFd4e3ff)
|
||||||
|
val md_theme_dark_secondary = Color(0xFFa3d719)
|
||||||
|
val md_theme_dark_onSecondary = Color(0xFF253600)
|
||||||
|
val md_theme_dark_secondaryContainer = Color(0xFF374e00)
|
||||||
|
val md_theme_dark_onSecondaryContainer = Color(0xFFbef43c)
|
||||||
|
val md_theme_dark_tertiary = Color(0xFFffb86b)
|
||||||
|
val md_theme_dark_onTertiary = Color(0xFF4a2800)
|
||||||
|
val md_theme_dark_tertiaryContainer = Color(0xFF6a3c00)
|
||||||
|
val md_theme_dark_onTertiaryContainer = Color(0xFFffdcba)
|
||||||
|
val md_theme_dark_error = Color(0xFFffb4a9)
|
||||||
|
val md_theme_dark_errorContainer = Color(0xFF930006)
|
||||||
|
val md_theme_dark_onError = Color(0xFF680003)
|
||||||
|
val md_theme_dark_onErrorContainer = Color(0xFFffdad4)
|
||||||
|
val md_theme_dark_background = Color(0xFF1b1b1d)
|
||||||
|
val md_theme_dark_onBackground = Color(0xFFe3e2e6)
|
||||||
|
val md_theme_dark_surface = Color(0xFF1b1b1d)
|
||||||
|
val md_theme_dark_onSurface = Color(0xFFe3e2e6)
|
||||||
|
val md_theme_dark_surfaceVariant = Color(0xFF44474f)
|
||||||
|
val md_theme_dark_onSurfaceVariant = Color(0xFFc3c6cf)
|
||||||
|
val md_theme_dark_outline = Color(0xFF8e919a)
|
||||||
|
val md_theme_dark_inverseOnSurface = Color(0xFF1b1b1d)
|
||||||
|
val md_theme_dark_inverseSurface = Color(0xFFe3e2e6)
|
||||||
|
|
||||||
|
val seed = Color(0xFF003366)
|
||||||
|
val error = Color(0xFFba1b1b)
|
||||||
102
app/src/main/java/de/sebse/fuplanner2/ui/theme/Theme.kt
Normal file
102
app/src/main/java/de/sebse/fuplanner2/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.darkColors
|
||||||
|
import androidx.compose.material.lightColors
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
// Comments are changes in Material 3 (Material You)
|
||||||
|
|
||||||
|
//private val LightThemeColors = lightColorScheme(
|
||||||
|
private val LightThemeColors = lightColors(
|
||||||
|
primary = md_theme_light_primary,
|
||||||
|
onPrimary = md_theme_light_onPrimary,
|
||||||
|
//primaryContainer = md_theme_light_primaryContainer,
|
||||||
|
//onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||||
|
primaryVariant = md_theme_light_onPrimaryContainer,
|
||||||
|
|
||||||
|
secondary = md_theme_light_secondary,
|
||||||
|
onSecondary = md_theme_light_onSecondary,
|
||||||
|
//secondaryContainer = md_theme_light_secondaryContainer,
|
||||||
|
//onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||||
|
secondaryVariant = md_theme_light_onSecondaryContainer,
|
||||||
|
|
||||||
|
//tertiary = md_theme_light_tertiary,
|
||||||
|
//onTertiary = md_theme_light_onTertiary,
|
||||||
|
//tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||||
|
//onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||||
|
|
||||||
|
error = md_theme_light_error,
|
||||||
|
onError = md_theme_light_onError,
|
||||||
|
//errorContainer = md_theme_light_errorContainer,
|
||||||
|
//onErrorContainer = md_theme_light_onErrorContainer,
|
||||||
|
|
||||||
|
background = md_theme_light_background,
|
||||||
|
onBackground = md_theme_light_onBackground,
|
||||||
|
|
||||||
|
surface = md_theme_light_surface,
|
||||||
|
onSurface = md_theme_light_onSurface,
|
||||||
|
//surfaceVariant = md_theme_light_surfaceVariant,
|
||||||
|
//onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||||
|
|
||||||
|
//outline = md_theme_light_outline,
|
||||||
|
//inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||||
|
//inverseSurface = md_theme_light_inverseSurface,
|
||||||
|
)
|
||||||
|
|
||||||
|
//private val DarkThemeColors = darkColorScheme(
|
||||||
|
private val DarkThemeColors = darkColors(
|
||||||
|
|
||||||
|
primary = md_theme_dark_primary,
|
||||||
|
onPrimary = md_theme_dark_onPrimary,
|
||||||
|
//primaryContainer = md_theme_dark_primaryContainer,
|
||||||
|
//onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||||
|
primaryVariant = md_theme_dark_onPrimaryContainer,
|
||||||
|
|
||||||
|
secondary = md_theme_dark_secondary,
|
||||||
|
onSecondary = md_theme_dark_onSecondary,
|
||||||
|
//secondaryContainer = md_theme_dark_secondaryContainer,
|
||||||
|
//onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||||
|
secondaryVariant = md_theme_dark_onSecondaryContainer,
|
||||||
|
|
||||||
|
//tertiary = md_theme_dark_tertiary,
|
||||||
|
//onTertiary = md_theme_dark_onTertiary,
|
||||||
|
//tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||||
|
//onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||||
|
|
||||||
|
error = md_theme_dark_error,
|
||||||
|
onError = md_theme_dark_onError,
|
||||||
|
//errorContainer = md_theme_dark_errorContainer,
|
||||||
|
//onErrorContainer = md_theme_dark_onErrorContainer,
|
||||||
|
|
||||||
|
background = md_theme_dark_background,
|
||||||
|
onBackground = md_theme_dark_onBackground,
|
||||||
|
|
||||||
|
surface = md_theme_dark_surface,
|
||||||
|
onSurface = md_theme_dark_onSurface,
|
||||||
|
//surfaceVariant = md_theme_dark_surfaceVariant,
|
||||||
|
//onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||||
|
|
||||||
|
//outline = md_theme_dark_outline,
|
||||||
|
//inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||||
|
//inverseSurface = md_theme_dark_inverseSurface,
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun AppTheme(
|
||||||
|
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
content: @Composable() () -> Unit
|
||||||
|
) {
|
||||||
|
val colors = if (!useDarkTheme) {
|
||||||
|
LightThemeColors
|
||||||
|
} else {
|
||||||
|
DarkThemeColors
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
//colorScheme = colors,
|
||||||
|
colors = colors,
|
||||||
|
// typography = AppTypography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
117
app/src/main/java/de/sebse/fuplanner2/ui/theme/Type.kt
Normal file
117
app/src/main/java/de/sebse/fuplanner2/ui/theme/Type.kt
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.theme
|
||||||
|
|
||||||
|
// Comments are changes in Material 3 (Material You)
|
||||||
|
// Use default instead
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
|
||||||
|
//Replace with your font locations
|
||||||
|
val Roboto = FontFamily.Default
|
||||||
|
|
||||||
|
/*val AppTypography = Typography(
|
||||||
|
displayLarge = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.W400,
|
||||||
|
fontSize = 57.sp,
|
||||||
|
lineHeight = 64.sp,
|
||||||
|
letterSpacing = -0.25.sp,
|
||||||
|
),
|
||||||
|
displayMedium = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.W400,
|
||||||
|
fontSize = 45.sp,
|
||||||
|
lineHeight = 52.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
displaySmall = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.W400,
|
||||||
|
fontSize = 36.sp,
|
||||||
|
lineHeight = 44.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
headlineLarge = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.W400,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
lineHeight = 40.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
headlineMedium = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.W400,
|
||||||
|
fontSize = 28.sp,
|
||||||
|
lineHeight = 36.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
headlineSmall = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.W400,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
lineHeight = 32.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.W400,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
),
|
||||||
|
titleMedium = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.1.sp,
|
||||||
|
),
|
||||||
|
titleSmall = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.1.sp,
|
||||||
|
),
|
||||||
|
labelLarge = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.1.sp,
|
||||||
|
),
|
||||||
|
bodyLarge = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.W400,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.5.sp,
|
||||||
|
),
|
||||||
|
bodyMedium = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.W400,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.25.sp,
|
||||||
|
),
|
||||||
|
bodySmall = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.W400,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.4.sp,
|
||||||
|
),
|
||||||
|
labelMedium = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp,
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontFamily = Roboto,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp,
|
||||||
|
),
|
||||||
|
)*/
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.tools.previews
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import de.sebse.fuplanner2.database.Announcement
|
||||||
|
import de.sebse.fuplanner2.database.Attachment
|
||||||
|
import de.sebse.fuplanner2.utils.Faker
|
||||||
|
import de.sebse.fuplanner2.utils.getFaker
|
||||||
|
|
||||||
|
class AnnouncementPreviewProvider(private val faker: Faker = getFaker()) : PreviewParameterProvider<Announcement> {
|
||||||
|
override val values = generateSequence { getItem() }
|
||||||
|
|
||||||
|
private fun getItem(): Announcement {
|
||||||
|
val title = faker.strings.title()
|
||||||
|
return Announcement(
|
||||||
|
faker.primitive.long(0, 100),
|
||||||
|
faker.primitive.long(0, 100),
|
||||||
|
faker.other.lastRefreshed(),
|
||||||
|
faker.strings.uuid(title),
|
||||||
|
title,
|
||||||
|
faker.strings.lorem(100, 1000),
|
||||||
|
faker.other.date(-20, -2),
|
||||||
|
"${faker.name.firstName()} ${faker.name.lastName()}",
|
||||||
|
AttachmentPreviewProvider(faker).values
|
||||||
|
.take(faker.primitive.int(1, 4))
|
||||||
|
.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AttachmentPreviewProvider(private val faker: Faker = getFaker()) : PreviewParameterProvider<Attachment> {
|
||||||
|
override val values = generateSequence { getItem() }
|
||||||
|
|
||||||
|
private fun getItem(): Attachment {
|
||||||
|
return Attachment(
|
||||||
|
faker.internet.url(),
|
||||||
|
faker.strings.title(),
|
||||||
|
faker.internet.mime()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.tools.previews
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import de.sebse.fuplanner2.database.Course
|
||||||
|
import de.sebse.fuplanner2.database.Lecturer
|
||||||
|
import de.sebse.fuplanner2.utils.Faker
|
||||||
|
import de.sebse.fuplanner2.utils.getFaker
|
||||||
|
|
||||||
|
class LecturerPreviewProvider(private val faker: Faker = getFaker()) : PreviewParameterProvider<Lecturer> {
|
||||||
|
override val values = sequence {
|
||||||
|
yield(getItem(true))
|
||||||
|
yield(getItem(faker.primitive.bool(.5f)))
|
||||||
|
yieldAll(generateSequence { getItem(false) })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getItem(isResponsible: Boolean): Lecturer {
|
||||||
|
val firstName = faker.name.firstName()
|
||||||
|
val lastName = faker.name.lastName()
|
||||||
|
return Lecturer(
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
faker.internet.email("$firstName $lastName"),
|
||||||
|
isResponsible
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoursePreviewProvider(private val faker: Faker = getFaker()) : PreviewParameterProvider<Course> {
|
||||||
|
private var isSummer = false
|
||||||
|
private var year = 21
|
||||||
|
override val values = sequence {
|
||||||
|
yield(getItem())
|
||||||
|
val reduce = faker.primitive.bool(.3f)
|
||||||
|
year = if (reduce && isSummer) year-1 else year
|
||||||
|
isSummer = if (reduce) !isSummer else isSummer
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getItem(): Course {
|
||||||
|
val diff = 1000L*60*60*24*5
|
||||||
|
val title =
|
||||||
|
"${faker.strings.title()} ${if (isSummer) "S" else "W"} ${if (isSummer) year else "$year/${year + 1}"}"
|
||||||
|
return Course(
|
||||||
|
faker.primitive.long(0, 100),
|
||||||
|
10,
|
||||||
|
faker.primitive.long(System.currentTimeMillis()-diff, System.currentTimeMillis()+diff),
|
||||||
|
isSummer,
|
||||||
|
year,
|
||||||
|
(0..10)
|
||||||
|
.map { faker.primitive.int(10000, 20000) }
|
||||||
|
.map { toString() }
|
||||||
|
.toHashSet(),
|
||||||
|
title,
|
||||||
|
faker.strings.courseTypes(),
|
||||||
|
faker.strings.lorem(100, 1000),
|
||||||
|
faker.strings.uuid(title),
|
||||||
|
faker.primitive.int(1, 2),
|
||||||
|
LecturerPreviewProvider(faker).values
|
||||||
|
.take(faker.primitive.int(1, 4))
|
||||||
|
.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.tools.previews
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import de.sebse.fuplanner2.database.Event
|
||||||
|
import de.sebse.fuplanner2.utils.Faker
|
||||||
|
import de.sebse.fuplanner2.utils.getFaker
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class EventPreviewProvider(private val faker: Faker = getFaker()) : PreviewParameterProvider<Event> {
|
||||||
|
val title = faker.strings.title()
|
||||||
|
override val values = generateSequence(0) { it + 1 }
|
||||||
|
.map { getItem(it) }
|
||||||
|
|
||||||
|
private fun getItem(num: Int): Event {
|
||||||
|
val isExam = num == 20
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
cal.add(Calendar.DATE, num / 2 * 7 + num % 2 * 3)
|
||||||
|
cal.set(Calendar.HOUR, 14 + (num % 2) * 2)
|
||||||
|
cal.set(Calendar.MINUTE, 30)
|
||||||
|
cal.set(Calendar.SECOND, 0)
|
||||||
|
cal.set(Calendar.MILLISECOND, 0)
|
||||||
|
|
||||||
|
return Event(
|
||||||
|
faker.primitive.long(0, 100),
|
||||||
|
faker.primitive.long(0, 100),
|
||||||
|
faker.other.lastRefreshed(),
|
||||||
|
title,
|
||||||
|
90*60*1000 + (if (isExam) 1L else 0),
|
||||||
|
cal.timeInMillis,
|
||||||
|
"Hörsaal T9",
|
||||||
|
if (isExam) "Klausur" else "Vorlesung",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.tools.viewmodels
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import de.sebse.fuplanner2.database.Announcement
|
||||||
|
import de.sebse.fuplanner2.database.AppDatabase
|
||||||
|
|
||||||
|
|
||||||
|
class AnnouncementViewModelFactory(private val announcementId: Long): ViewModelProvider.NewInstanceFactory() {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = AnnouncementViewModel(announcementId) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnnouncementViewModel(private val announcementId: Long) : ViewModel() {
|
||||||
|
private val announcement: LiveData<Announcement> =
|
||||||
|
AppDatabase.getInstance().announcementDao().getAnnouncementById2(announcementId)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun observeAsState(): State<Announcement?> {
|
||||||
|
return announcement.observeAsState()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package de.sebse.fuplanner2.ui.courses
|
package de.sebse.fuplanner2.ui.tools.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@@ -6,5 +6,5 @@ import de.sebse.fuplanner2.database.AppDatabase
|
|||||||
import de.sebse.fuplanner2.database.Course
|
import de.sebse.fuplanner2.database.Course
|
||||||
|
|
||||||
class CoursesViewModel : ViewModel() {
|
class CoursesViewModel : ViewModel() {
|
||||||
val text: LiveData<List<Course>> = AppDatabase.getInstance().courseDao().getAll()
|
val list: LiveData<List<Course>> = AppDatabase.getInstance().courseDao().getAll()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package de.sebse.fuplanner2.ui.tools.viewmodels
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.paging.LivePagedListBuilder
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import androidx.work.workDataOf
|
||||||
|
import de.sebse.fuplanner2.auth.AppAccounts
|
||||||
|
import de.sebse.fuplanner2.database.Announcement
|
||||||
|
import de.sebse.fuplanner2.database.AppDatabase
|
||||||
|
import de.sebse.fuplanner2.database.Course
|
||||||
|
import de.sebse.fuplanner2.database.Event
|
||||||
|
import de.sebse.fuplanner2.utils.enqueueOneTimeWork
|
||||||
|
import de.sebse.fuplanner2.worker.AbstractAccountWorker
|
||||||
|
import de.sebse.fuplanner2.worker.CourseWorker
|
||||||
|
|
||||||
|
|
||||||
|
class DetailsViewModelFactory(private val courseId: Long): ViewModelProvider.NewInstanceFactory() {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = DetailsViewModel(courseId) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
class DetailsViewModel(private val courseId: Long) : ViewModel() {
|
||||||
|
fun refresh(ctx: Context) {
|
||||||
|
enqueueOneTimeWork<CourseWorker>(ctx) {
|
||||||
|
it.setInputData(workDataOf(
|
||||||
|
AbstractAccountWorker.KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name,
|
||||||
|
AbstractAccountWorker.KEY_COURSE_ID to courseId,
|
||||||
|
AbstractAccountWorker.KEY_FORCE_FETCH to true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val course: LiveData<Course> = AppDatabase.getInstance().courseDao().getCourseById(courseId)
|
||||||
|
val announcements: LiveData<PagedList<Announcement>> = LivePagedListBuilder(
|
||||||
|
AppDatabase.getInstance().announcementDao().getAll1(courseId),
|
||||||
|
50
|
||||||
|
).build()
|
||||||
|
val events: LiveData<PagedList<Event>> = LivePagedListBuilder(
|
||||||
|
AppDatabase.getInstance().eventDao().getAll1(courseId),
|
||||||
|
50
|
||||||
|
).build()
|
||||||
|
}
|
||||||
160
app/src/main/java/de/sebse/fuplanner2/utils/faking.kt
Normal file
160
app/src/main/java/de/sebse/fuplanner2/utils/faking.kt
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package de.sebse.fuplanner2.utils
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
|
||||||
|
fun getFaker(): Faker {
|
||||||
|
return Faker(42)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Faker(randomSeed: Int) {
|
||||||
|
private val random = Random(randomSeed)
|
||||||
|
|
||||||
|
val name: FakerName
|
||||||
|
get() = FakerName(random)
|
||||||
|
|
||||||
|
val internet: FakerInternet
|
||||||
|
get() = FakerInternet(random)
|
||||||
|
|
||||||
|
val primitive: FakerPrimitive
|
||||||
|
get() = FakerPrimitive(random)
|
||||||
|
|
||||||
|
val other: FakerOther
|
||||||
|
get() = FakerOther(random)
|
||||||
|
|
||||||
|
val strings: FakerStrings
|
||||||
|
get() = FakerStrings(random)
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakerName(private val random: Random) {
|
||||||
|
fun lastName(): String {
|
||||||
|
return LASTNAMES.random(random)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun firstName(): String {
|
||||||
|
return FIRSTNAMES.random(random)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val LASTNAMES = listOf("Müller", "Schmidt", "Schneider", "Fischer", "Weber",
|
||||||
|
"Meyer", "Wagner", "Becker", "Schulz", "Hoffmann", "Schäfer", "Koch", "Bauer",
|
||||||
|
"Richter", "Klein", "Schröder", "Wolf", "Neumann", "Schwarz", "Zimmermann", "Braun",
|
||||||
|
"Hofmann", "Hartmann", "Schmitt", "Krüger", "Schmitz", "Lange", "Werner", "Krause",
|
||||||
|
"Meier")
|
||||||
|
private val FIRSTNAMES = listOf("Daisie", "Elisabeth", "Dyane", "Valry", "Terrye",
|
||||||
|
"Susette", "Karen", "Cecile", "Hesther", "Faunie", "Cissy", "Babb", "Cristionna",
|
||||||
|
"Paola", "Claribel", "Eveline", "June", "Ange", "Ebba", "Willow", "Aggi",
|
||||||
|
"Anne-Corinne", "Merl", "Di", "Kit", "Dreddy", "Fayette", "Griselda", "Emmey",
|
||||||
|
"Timothea", "Lucia", "Jacky", "Aline", "Rikki", "Sandye", "Aura", "Cordula", "Linell",
|
||||||
|
"Adeline", "Jessalyn", "Mariya", "Ema", "Hermine", "Annalee", "Agatha", "Wrennie",
|
||||||
|
"Florinda", "Malynda", "Bess", "Binni")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakerInternet(private val random: Random) {
|
||||||
|
fun email(name: String?): String {
|
||||||
|
val salt = name ?: FakerName(random).let {
|
||||||
|
"${it.firstName()} ${it.lastName()}"
|
||||||
|
}
|
||||||
|
val user = salt
|
||||||
|
.lowercase()
|
||||||
|
.replace(Regex("[ -]"), ".")
|
||||||
|
.replace(Regex("[^a-z0-9.]"), "")
|
||||||
|
return "$user@fu-berlin.de"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mime(): String {
|
||||||
|
return MIME_TYPES.random(random)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun url(): String {
|
||||||
|
return "https://example.de/file"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val MIME_TYPES = listOf("application/pdf", "text/plain", "text/csv",
|
||||||
|
"application/msword")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakerPrimitive(private val random: Random) {
|
||||||
|
fun int(min: Int, max: Int): Int {
|
||||||
|
return (min..max).random(random)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun long(min: Long, max: Long): Long {
|
||||||
|
return (min..max).random(random)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bool(trueProbability: Float): Boolean {
|
||||||
|
return random.nextFloat() < trueProbability
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakerOther(private val random: Random) {
|
||||||
|
fun lastRefreshed(): Long {
|
||||||
|
return FakerPrimitive(random).long(
|
||||||
|
System.currentTimeMillis() - DIFF,
|
||||||
|
System.currentTimeMillis() + DIFF
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fun date(startDay: Int, endDay: Int): Long {
|
||||||
|
return FakerPrimitive(random).long(
|
||||||
|
System.currentTimeMillis() + DAY * startDay,
|
||||||
|
System.currentTimeMillis() + DAY * endDay
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DAY = 1000L*60*60*24
|
||||||
|
const val DIFF = DAY*5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakerStrings(private val random: Random) {
|
||||||
|
fun lorem(min: Int, max: Int): String {
|
||||||
|
val length = (min..max).random(random)
|
||||||
|
val loremArray = LOREM.split(" ").size
|
||||||
|
val n = ceil(length.toFloat() / loremArray).toInt() + 1
|
||||||
|
val repeatedArray = ("$LOREM ")
|
||||||
|
.repeat(n)
|
||||||
|
.trim()
|
||||||
|
.split(" ")
|
||||||
|
return repeatedArray.run {
|
||||||
|
if (size-length < 1) throw Error("$n + $size<$length")
|
||||||
|
val from = (0 until size-length).random()
|
||||||
|
subList(from, from+length)
|
||||||
|
}.joinToString(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun title(): String {
|
||||||
|
return lorem(3, 7)
|
||||||
|
.split(" ")
|
||||||
|
.joinToString(" ") { word ->
|
||||||
|
word.replaceFirstChar { it.uppercase() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uuid(namespace: String): String {
|
||||||
|
return UUID.nameUUIDFromBytes(namespace.encodeToByteArray()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun courseTypes(): String {
|
||||||
|
return COURSE_TYPES.random(random)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LOREM = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy "+
|
||||||
|
"eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam "+
|
||||||
|
"voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita "+
|
||||||
|
"kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem "+
|
||||||
|
"ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor "+
|
||||||
|
"invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos "+
|
||||||
|
"et accusam et justo duo dolores et ea rebum."
|
||||||
|
|
||||||
|
val COURSE_TYPES = listOf("Vorlesung + Übung", "Vorlesung + Seminar am PC", "Praktikum",
|
||||||
|
"Vorlesung/Übung")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ object Notifications {
|
|||||||
|
|
||||||
enum class CourseUpdateType {
|
enum class CourseUpdateType {
|
||||||
REMOVED, UPDATED, ADDED;
|
REMOVED, UPDATED, ADDED;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val values: List<CourseUpdateType> = values().toList()
|
val values: List<CourseUpdateType> = values().toList()
|
||||||
}
|
}
|
||||||
@@ -32,13 +33,19 @@ object Notifications {
|
|||||||
|
|
||||||
enum class CourseUpdateEntity {
|
enum class CourseUpdateEntity {
|
||||||
COURSE, ANNOUNCEMENT, ASSIGNMENT, GRADE, RESOURCE, EVENT;
|
COURSE, ANNOUNCEMENT, ASSIGNMENT, GRADE, RESOURCE, EVENT;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val values: List<CourseUpdateEntity> = values().toList()
|
val values: List<CourseUpdateEntity> = values().toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(actCtx: Context) {
|
fun init(actCtx: Context) {
|
||||||
createNotificationChannel(actCtx, COURSE_UPDATE_CHANNEL_ID, R.string.channel_name, R.string.channel_description)
|
createNotificationChannel(
|
||||||
|
actCtx,
|
||||||
|
COURSE_UPDATE_CHANNEL_ID,
|
||||||
|
R.string.channel_name,
|
||||||
|
R.string.channel_description
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotificationChannel(
|
private fun createNotificationChannel(
|
||||||
@@ -62,7 +69,11 @@ object Notifications {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T: Updatable> courseUpdates(updates: UpdateResult<T>, database: AppDatabase, actCtx: Context) {
|
fun <T : Updatable> courseUpdates(
|
||||||
|
updates: UpdateResult<T>,
|
||||||
|
database: AppDatabase,
|
||||||
|
actCtx: Context
|
||||||
|
) {
|
||||||
val newNotifications = listOf(
|
val newNotifications = listOf(
|
||||||
CourseUpdateType.REMOVED to updates.removed,
|
CourseUpdateType.REMOVED to updates.removed,
|
||||||
CourseUpdateType.ADDED to updates.added,
|
CourseUpdateType.ADDED to updates.added,
|
||||||
@@ -95,7 +106,13 @@ object Notifications {
|
|||||||
val notification = NotificationCompat.Builder(actCtx, COURSE_UPDATE_CHANNEL_ID)
|
val notification = NotificationCompat.Builder(actCtx, COURSE_UPDATE_CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.ic_logo_mono)
|
.setSmallIcon(R.drawable.ic_logo_mono)
|
||||||
.setContentTitle(actCtx.getString(R.string.not_course_update_title))
|
.setContentTitle(actCtx.getString(R.string.not_course_update_title))
|
||||||
.setContentText(actCtx.getQuantityString(R.plurals.not_course_update_text, unread.size, unread.size))
|
.setContentText(
|
||||||
|
actCtx.getQuantityString(
|
||||||
|
R.plurals.not_course_update_text,
|
||||||
|
unread.size,
|
||||||
|
unread.size
|
||||||
|
)
|
||||||
|
)
|
||||||
.setStyle(NotificationCompat.InboxStyle().also { inboxStyle ->
|
.setStyle(NotificationCompat.InboxStyle().also { inboxStyle ->
|
||||||
unread.forEach { not ->
|
unread.forEach { not ->
|
||||||
not.getJsonData()?.let { json ->
|
not.getJsonData()?.let { json ->
|
||||||
@@ -122,7 +139,7 @@ object Notifications {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UpdateResult<T: Updatable>(
|
data class UpdateResult<T : Updatable>(
|
||||||
val removed: ArrayList<T> = arrayListOf(),
|
val removed: ArrayList<T> = arrayListOf(),
|
||||||
val added: ArrayList<T> = arrayListOf(),
|
val added: ArrayList<T> = arrayListOf(),
|
||||||
val updated: ArrayList<T> = arrayListOf(),
|
val updated: ArrayList<T> = arrayListOf(),
|
||||||
@@ -137,12 +154,15 @@ data class UpdateResult<T: Updatable>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val values: List<T>
|
val values: List<T>
|
||||||
get() {
|
get() {
|
||||||
return this.added + this.updated + this.unmodified
|
return this.added + this.updated + this.unmodified
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <S: Updatable> mergeUpdatable(source: UpdateResult<Updatable>, plus: UpdateResult<S>): UpdateResult<Updatable> {
|
fun <S : Updatable> mergeUpdatable(
|
||||||
|
source: UpdateResult<Updatable>,
|
||||||
|
plus: UpdateResult<S>
|
||||||
|
): UpdateResult<Updatable> {
|
||||||
source.removed.addAll(plus.removed)
|
source.removed.addAll(plus.removed)
|
||||||
source.added.addAll(plus.added)
|
source.added.addAll(plus.added)
|
||||||
source.updated.addAll(plus.updated)
|
source.updated.addAll(plus.updated)
|
||||||
@@ -163,11 +183,13 @@ interface UpdatableCompanion {
|
|||||||
type: Notifications.CourseUpdateType,
|
type: Notifications.CourseUpdateType,
|
||||||
data: JsonObject
|
data: JsonObject
|
||||||
): CharSequence?
|
): CharSequence?
|
||||||
|
|
||||||
fun adapterText(
|
fun adapterText(
|
||||||
actCtx: Context,
|
actCtx: Context,
|
||||||
type: Notifications.CourseUpdateType,
|
type: Notifications.CourseUpdateType,
|
||||||
data: JsonObject
|
data: JsonObject
|
||||||
): CharSequence?
|
): CharSequence?
|
||||||
|
|
||||||
fun adapterCallback(
|
fun adapterCallback(
|
||||||
actCtx: Context,
|
actCtx: Context,
|
||||||
type: Notifications.CourseUpdateType,
|
type: Notifications.CourseUpdateType,
|
||||||
@@ -176,7 +198,7 @@ interface UpdatableCompanion {
|
|||||||
): () -> Unit
|
): () -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T: Updatable> updateResultOf(old: List<T>, new: List<T>): UpdateResult<T> {
|
fun <T : Updatable> updateResultOf(old: List<T>, new: List<T>): UpdateResult<T> {
|
||||||
val newIds = new
|
val newIds = new
|
||||||
.map { it.getIdentifier() to Pair(it.getHash(), it) }
|
.map { it.getIdentifier() to Pair(it.getHash(), it) }
|
||||||
.toMap()
|
.toMap()
|
||||||
@@ -200,4 +222,3 @@ fun <T: Updatable> updateResultOf(old: List<T>, new: List<T>): UpdateResult<T> {
|
|||||||
}
|
}
|
||||||
return UpdateResult(removed, added, updated, unmodified)
|
return UpdateResult(removed, added, updated, unmodified)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ package de.sebse.fuplanner2.utils
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.format.DateFormat
|
import android.text.format.DateFormat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.PluralsRes
|
import androidx.annotation.PluralsRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import de.sebse.fuplanner2.R
|
import de.sebse.fuplanner2.R
|
||||||
@@ -20,6 +23,8 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
|
||||||
object console {
|
object console {
|
||||||
@@ -52,7 +57,7 @@ object console {
|
|||||||
object xml {
|
object xml {
|
||||||
fun decode(xml: String): String {
|
fun decode(xml: String): String {
|
||||||
return if (Build.VERSION.SDK_INT >= 24) {
|
return if (Build.VERSION.SDK_INT >= 24) {
|
||||||
Html.fromHtml(xml , Html.FROM_HTML_MODE_LEGACY).toString()
|
Html.fromHtml(xml, Html.FROM_HTML_MODE_LEGACY).toString()
|
||||||
} else {
|
} else {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
Html.fromHtml(xml).toString()
|
Html.fromHtml(xml).toString()
|
||||||
@@ -60,24 +65,82 @@ object xml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T: ListenableWorker> enqueueOneTimeWork(appCtx: Context, workBuilder: (OneTimeWorkRequest.Builder) -> OneTimeWorkRequest.Builder): LiveData<WorkInfo> {
|
object color {
|
||||||
|
@ColorInt
|
||||||
|
fun getColor(actCtx: Context, seed: Long, highContrast: Boolean = false): Int {
|
||||||
|
var h = Random(seed).nextInt(0xFFFF)
|
||||||
|
h = h * 360 / 0xffff
|
||||||
|
//int s = 0xff & encodedHash[2];
|
||||||
|
//s = s * 100 / 0xffff;
|
||||||
|
//int v = 0xff & encodedHash[3];
|
||||||
|
//v = v * 100 / 0xffff;
|
||||||
|
|
||||||
|
// range for more beautiful colors
|
||||||
|
h = h / 30 * 30
|
||||||
|
val isNightMode =
|
||||||
|
actCtx.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
val doDarkColors = if (highContrast) !isNightMode else isNightMode
|
||||||
|
val (s, v) =
|
||||||
|
if (doDarkColors)
|
||||||
|
Pair(100, 50)//Pair(100, 80)
|
||||||
|
else
|
||||||
|
Pair(100, 70)
|
||||||
|
val (r, g, b) = hsvToRgb(h / 360.0, s / 100.0, v / 100.0)
|
||||||
|
return rgbToColorInt(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
private fun rgbToColorInt(
|
||||||
|
r: Double,
|
||||||
|
g: Double,
|
||||||
|
b: Double
|
||||||
|
): Int {
|
||||||
|
return 0xFF000000.toInt() + (r * 0xFF).roundToInt() * 0x10000 + (g * 0xFF).roundToInt() * 0x100 + (b * 0xFF).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hsvToRgb(
|
||||||
|
hue: Double,
|
||||||
|
saturation: Double,
|
||||||
|
value: Double
|
||||||
|
): Triple<Double, Double, Double> {
|
||||||
|
val h = (hue * 6).toInt()
|
||||||
|
val f = hue * 6 - h
|
||||||
|
val p = value * (1 - saturation)
|
||||||
|
val q = value * (1 - f * saturation)
|
||||||
|
val t = value * (1 - (1 - f) * saturation)
|
||||||
|
return when (h) {
|
||||||
|
0 -> Triple(value, t, p)
|
||||||
|
1 -> Triple(q, value, p)
|
||||||
|
2 -> Triple(p, value, t)
|
||||||
|
3 -> Triple(p, q, value)
|
||||||
|
4 -> Triple(t, p, value)
|
||||||
|
5 -> Triple(value, p, q)
|
||||||
|
else -> Triple(0.0, 0.0, 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : ListenableWorker> enqueueOneTimeWork(
|
||||||
|
appCtx: Context,
|
||||||
|
workBuilder: (OneTimeWorkRequest.Builder) -> OneTimeWorkRequest.Builder
|
||||||
|
): LiveData<WorkInfo> {
|
||||||
val work = workBuilder(OneTimeWorkRequestBuilder<T>()).build()
|
val work = workBuilder(OneTimeWorkRequestBuilder<T>()).build()
|
||||||
val workManager = WorkManager.getInstance(appCtx)
|
val workManager = WorkManager.getInstance(appCtx)
|
||||||
workManager.enqueue(work)
|
workManager.enqueue(work)
|
||||||
return workManager.getWorkInfoByIdLiveData(work.id)
|
return workManager.getWorkInfoByIdLiveData(work.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <A, B>List<A>.pmap(f: suspend (A) -> B): List<B> = runBlocking {
|
fun <A, B> List<A>.pmap(f: suspend (A) -> B): List<B> = runBlocking {
|
||||||
map { async { f(it) } }.map { it.await() }
|
map { async { f(it) } }.map { it.await() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hashCodeOf(vararg elements: Any?): Int {
|
fun hashCodeOf(vararg elements: Any?): Int {
|
||||||
var code = 0
|
var code = 0
|
||||||
elements.forEach { code = code*31 + (it?.hashCode() ?: 0) }
|
elements.forEach { code = code * 31 + (it?.hashCode() ?: 0) }
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T> cast(any: Any?) : T? = any as? T?
|
inline fun <reified T> cast(any: Any?): T? = any as? T?
|
||||||
|
|
||||||
|
|
||||||
fun String.toHtmlSpan(): Spanned = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
fun String.toHtmlSpan(): Spanned = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
@@ -89,17 +152,23 @@ fun String.toHtmlSpan(): Spanned = if (Build.VERSION.SDK_INT >= Build.VERSION_CO
|
|||||||
|
|
||||||
fun Context.getHtmlSpannedString(@StringRes id: Int): Spanned = getString(id).toHtmlSpan()
|
fun Context.getHtmlSpannedString(@StringRes id: Int): Spanned = getString(id).toHtmlSpan()
|
||||||
|
|
||||||
fun Context.getHtmlSpannedString(@StringRes id: Int, vararg formatArgs: Any?): Spanned = getString(id, *formatArgs).toHtmlSpan()
|
fun Context.getHtmlSpannedString(@StringRes id: Int, vararg formatArgs: Any?): Spanned =
|
||||||
|
getString(id, *formatArgs).toHtmlSpan()
|
||||||
|
|
||||||
fun Context.getQuantityString(@PluralsRes id: Int, quantity: Int): String = resources.getQuantityString(id, quantity)
|
fun Context.getQuantityString(@PluralsRes id: Int, quantity: Int): String =
|
||||||
|
resources.getQuantityString(id, quantity)
|
||||||
|
|
||||||
fun Context.getQuantityString(@PluralsRes id: Int, quantity: Int, vararg formatArgs: Any?): String = resources.getQuantityString(id, quantity, *formatArgs)
|
fun Context.getQuantityString(@PluralsRes id: Int, quantity: Int, vararg formatArgs: Any?): String =
|
||||||
|
resources.getQuantityString(id, quantity, *formatArgs)
|
||||||
fun Context.getQuantityHtmlSpannedString(@PluralsRes id: Int, quantity: Int): Spanned = getQuantityString(id, quantity).toHtmlSpan()
|
|
||||||
|
|
||||||
fun Context.getQuantityHtmlSpannedString(@PluralsRes id: Int, quantity: Int, vararg formatArgs: Any?): Spanned = getQuantityString(id, quantity, *formatArgs).toHtmlSpan()
|
|
||||||
|
|
||||||
|
fun Context.getQuantityHtmlSpannedString(@PluralsRes id: Int, quantity: Int): Spanned =
|
||||||
|
getQuantityString(id, quantity).toHtmlSpan()
|
||||||
|
|
||||||
|
fun Context.getQuantityHtmlSpannedString(
|
||||||
|
@PluralsRes id: Int,
|
||||||
|
quantity: Int,
|
||||||
|
vararg formatArgs: Any?
|
||||||
|
): Spanned = getQuantityString(id, quantity, *formatArgs).toHtmlSpan()
|
||||||
|
|
||||||
|
|
||||||
fun Long.timeAgoString(actCtx: Context): String {
|
fun Long.timeAgoString(actCtx: Context): String {
|
||||||
@@ -115,9 +184,21 @@ fun Long.timeAgoString(actCtx: Context): String {
|
|||||||
val diff = now - this
|
val diff = now - this
|
||||||
return when {
|
return when {
|
||||||
diff < MINUTE_MILLIS -> actCtx.getString(R.string.time_just_now)
|
diff < MINUTE_MILLIS -> actCtx.getString(R.string.time_just_now)
|
||||||
diff < 60 * MINUTE_MILLIS -> actCtx.getQuantityString(R.plurals.time_minutes_ago, (diff / MINUTE_MILLIS).toInt(), diff / MINUTE_MILLIS)
|
diff < 60 * MINUTE_MILLIS -> actCtx.getQuantityString(
|
||||||
diff < 24 * HOUR_MILLIS -> actCtx.getQuantityString(R.plurals.time_hours_ago, (diff / HOUR_MILLIS).toInt(), diff / HOUR_MILLIS)
|
R.plurals.time_minutes_ago,
|
||||||
else -> actCtx.getQuantityString(R.plurals.time_days_ago, (diff / DAY_MILLIS).toInt(), diff / DAY_MILLIS)
|
(diff / MINUTE_MILLIS).toInt(),
|
||||||
|
diff / MINUTE_MILLIS
|
||||||
|
)
|
||||||
|
diff < 24 * HOUR_MILLIS -> actCtx.getQuantityString(
|
||||||
|
R.plurals.time_hours_ago,
|
||||||
|
(diff / HOUR_MILLIS).toInt(),
|
||||||
|
diff / HOUR_MILLIS
|
||||||
|
)
|
||||||
|
else -> actCtx.getQuantityString(
|
||||||
|
R.plurals.time_days_ago,
|
||||||
|
(diff / DAY_MILLIS).toInt(),
|
||||||
|
diff / DAY_MILLIS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,3 +260,23 @@ fun String.dateStringToLong(format: String, locale: Locale): Long? {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Either<A, B> private constructor(val _a: A? = null, val _b: B? = null) {
|
||||||
|
@Composable
|
||||||
|
fun <C> toValue(aTransform: @Composable (A) -> C, bTransform: @Composable (B) -> C): C {
|
||||||
|
return if (_a != null)
|
||||||
|
aTransform(_a)
|
||||||
|
else
|
||||||
|
bTransform(_b!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <A, B> a(a: A): Either<A, B> {
|
||||||
|
return Either(a, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <A, B> b(b: B): Either<A, B> {
|
||||||
|
return Either(null, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ abstract class AbstractAccountWorker(context: Context, params: WorkerParameters)
|
|||||||
companion object {
|
companion object {
|
||||||
const val KEY_ACCOUNT_NAME = AccountManager.KEY_ACCOUNT_NAME
|
const val KEY_ACCOUNT_NAME = AccountManager.KEY_ACCOUNT_NAME
|
||||||
const val KEY_COURSE_ID = "KEY_COURSE_ID"
|
const val KEY_COURSE_ID = "KEY_COURSE_ID"
|
||||||
const val OUT_ERROR_CODE = "OUT_CODE"
|
const val OUT_ERROR_CODE = "OUT_ERROR_CODE"
|
||||||
|
const val KEY_FORCE_FETCH = "KEY_FORCE_FETCH"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ErrorCodes {
|
enum class ErrorCodes {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import de.sebse.fuplanner2.database.Course
|
|||||||
import de.sebse.fuplanner2.database.User
|
import de.sebse.fuplanner2.database.User
|
||||||
import de.sebse.fuplanner2.utils.Notifications
|
import de.sebse.fuplanner2.utils.Notifications
|
||||||
import de.sebse.fuplanner2.utils.UpdateResult
|
import de.sebse.fuplanner2.utils.UpdateResult
|
||||||
import de.sebse.fuplanner2.utils.console
|
|
||||||
import de.sebse.fuplanner2.whiteboard.Whiteboard
|
import de.sebse.fuplanner2.whiteboard.Whiteboard
|
||||||
import de.sebse.fuplanner2.whiteboard.getCourse
|
import de.sebse.fuplanner2.whiteboard.getCourse
|
||||||
import de.sebse.fuplanner2.whiteboard.getCourses
|
import de.sebse.fuplanner2.whiteboard.getCourses
|
||||||
@@ -34,11 +33,19 @@ class CourseWorker(context: Context, params: WorkerParameters) : AbstractAccount
|
|||||||
override suspend fun doActualWork(database: AppDatabase, account: Account, user: User): Data {
|
override suspend fun doActualWork(database: AppDatabase, account: Account, user: User): Data {
|
||||||
val courseId = inputData.getLong(KEY_COURSE_ID, -1)
|
val courseId = inputData.getLong(KEY_COURSE_ID, -1)
|
||||||
.let { if (it == -1L) null else it }
|
.let { if (it == -1L) null else it }
|
||||||
|
val isForceFetch = inputData.getBoolean(KEY_FORCE_FETCH, false)
|
||||||
|
|
||||||
val updates = work(applicationContext, database, user, courseId)
|
val updates = work(applicationContext, database, user, courseId)
|
||||||
val latestSemester = database.courseDao().getLatestSemesterName()
|
val latestSemester = database.courseDao().getLatestSemesterName()
|
||||||
updates.added.forEach {
|
|
||||||
if (it.isSummerSemester == latestSemester.semester && it.year == latestSemester.year) {
|
val detailsToFetch =
|
||||||
|
if (isForceFetch) updates.added + updates.updated
|
||||||
|
else updates.added
|
||||||
|
detailsToFetch.forEach {
|
||||||
|
val isLatestSemester = it.isSummerSemester == latestSemester.semester && it.year == latestSemester.year
|
||||||
|
if (isForceFetch || isLatestSemester) {
|
||||||
EventWorker.work(applicationContext, database, user, it)
|
EventWorker.work(applicationContext, database, user, it)
|
||||||
|
AnnouncementWorker.work(applicationContext, database, user, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Notifications.courseUpdates(updates, database, applicationContext)
|
Notifications.courseUpdates(updates, database, applicationContext)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import de.sebse.fuplanner2.auth.AppAccounts
|
|||||||
import de.sebse.fuplanner2.database.AppDatabase
|
import de.sebse.fuplanner2.database.AppDatabase
|
||||||
import de.sebse.fuplanner2.database.Course
|
import de.sebse.fuplanner2.database.Course
|
||||||
import de.sebse.fuplanner2.utils.Notifications
|
import de.sebse.fuplanner2.utils.Notifications
|
||||||
import de.sebse.fuplanner2.utils.Updatable
|
|
||||||
import de.sebse.fuplanner2.utils.UpdateResult
|
import de.sebse.fuplanner2.utils.UpdateResult
|
||||||
import de.sebse.fuplanner2.utils.mergeUpdatable
|
import de.sebse.fuplanner2.utils.mergeUpdatable
|
||||||
|
|
||||||
@@ -52,6 +51,7 @@ class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(c
|
|||||||
courseCreations.forEach { course ->
|
courseCreations.forEach { course ->
|
||||||
if (course.isSummerSemester == latestSemester.semester && course.year == latestSemester.year) {
|
if (course.isSummerSemester == latestSemester.semester && course.year == latestSemester.year) {
|
||||||
EventWorker.work(applicationContext, database, it, course)
|
EventWorker.work(applicationContext, database, it, course)
|
||||||
|
AnnouncementWorker.work(applicationContext, database, it, course)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Notifications.courseUpdates(notifications, database, applicationContext)
|
Notifications.courseUpdates(notifications, database, applicationContext)
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/drawer_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:fitsSystemWindows="true"
|
|
||||||
tools:openDrawer="start">
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/app_bar_main"
|
|
||||||
layout="@layout/app_bar_main"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.navigation.NavigationView
|
|
||||||
android:id="@+id/nav_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="start"
|
|
||||||
android:fitsSystemWindows="true"
|
|
||||||
app:headerLayout="@layout/nav_header_main"
|
|
||||||
app:menu="@menu/activity_main_drawer" />
|
|
||||||
|
|
||||||
</androidx.drawerlayout.widget.DrawerLayout>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:theme="@style/FUTheme.AppBarOverlay">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?attr/actionBarSize"
|
|
||||||
android:background="?attr/colorPrimary"
|
|
||||||
app:popupTheme="@style/FUTheme.PopupOverlay" />
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
|
|
||||||
<include layout="@layout/content_main" />
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
|
||||||
tools:showIn="@layout/app_bar_main">
|
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
|
||||||
android:id="@+id/nav_host_fragment"
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:defaultNavHost="true"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:navGraph="@navigation/nav_graph" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/linear_layout"
|
android:id="@+id/linear_layout"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
tools:showIn="@layout/activity_main"
|
|
||||||
tools:context=".ui.courses.CoursesFragment">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_view"
|
android:id="@+id/recycler_view"
|
||||||
android:scrollbars="vertical"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/swipe_refresh_layout"
|
android:id="@+id/swipe_refresh_layout"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
tools:showIn="@layout/activity_main"
|
|
||||||
tools:context=".ui.courses.CoursesFragment">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_view"
|
android:id="@+id/recycler_view"
|
||||||
android:scrollbars="vertical"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical" />
|
||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
@@ -2,29 +2,7 @@
|
|||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/nav_graph"
|
android:id="@+id/nav_graph">
|
||||||
app:startDestination="@+id/nav_courses">
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/nav_courses"
|
|
||||||
android:name="de.sebse.fuplanner2.ui.courses.CoursesFragment"
|
|
||||||
android:label="@string/menu_courses"
|
|
||||||
tools:layout="@layout/fragment_refresh_recycler">
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_nav_home_to_course_details"
|
|
||||||
app:destination="@id/course_details"
|
|
||||||
app:enterAnim="@anim/slide_in_right"
|
|
||||||
app:exitAnim="@anim/slide_out_left"
|
|
||||||
app:popEnterAnim="@anim/slide_in_left"
|
|
||||||
app:popExitAnim="@anim/slide_out_right" />
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_nav_courses_to_notificationFragment"
|
|
||||||
app:destination="@id/nav_notifications"
|
|
||||||
app:enterAnim="@anim/slide_in_right"
|
|
||||||
app:exitAnim="@anim/slide_out_left"
|
|
||||||
app:popEnterAnim="@anim/slide_in_left"
|
|
||||||
app:popExitAnim="@anim/slide_out_right" />
|
|
||||||
</fragment>
|
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/nav_canteen"
|
android:id="@+id/nav_canteen"
|
||||||
@@ -36,47 +14,7 @@
|
|||||||
android:id="@+id/nav_schedule"
|
android:id="@+id/nav_schedule"
|
||||||
android:name="de.sebse.fuplanner2.ui.schedule.ScheduleFragment"
|
android:name="de.sebse.fuplanner2.ui.schedule.ScheduleFragment"
|
||||||
android:label="@string/menu_schedule"
|
android:label="@string/menu_schedule"
|
||||||
tools:layout="@layout/fragment_schedule">
|
tools:layout="@layout/fragment_schedule"/>
|
||||||
<action
|
|
||||||
android:id="@+id/action_nav_schedule_to_course_details"
|
|
||||||
app:destination="@id/course_details"
|
|
||||||
app:enterAnim="@anim/slide_in_right"
|
|
||||||
app:exitAnim="@anim/slide_out_left"
|
|
||||||
app:popEnterAnim="@anim/slide_in_left"
|
|
||||||
app:popExitAnim="@anim/slide_out_right" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/course_details"
|
|
||||||
android:name="de.sebse.fuplanner2.ui.details.DetailsFragment"
|
|
||||||
android:label="{title}">
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_course_details_to_descriptionFragment"
|
|
||||||
app:destination="@id/course_description"
|
|
||||||
app:enterAnim="@anim/slide_in_right"
|
|
||||||
app:exitAnim="@anim/slide_out_left"
|
|
||||||
app:popEnterAnim="@anim/slide_in_left"
|
|
||||||
app:popExitAnim="@anim/slide_out_right" />
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_course_details_to_course_events"
|
|
||||||
app:destination="@id/course_events"
|
|
||||||
app:enterAnim="@anim/slide_in_right"
|
|
||||||
app:exitAnim="@anim/slide_out_left"
|
|
||||||
app:popEnterAnim="@anim/slide_in_left"
|
|
||||||
app:popExitAnim="@anim/slide_out_right" />
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_course_details_to_course_announcements"
|
|
||||||
app:destination="@id/course_announcements"
|
|
||||||
app:enterAnim="@anim/slide_in_right"
|
|
||||||
app:exitAnim="@anim/slide_out_left"
|
|
||||||
app:popEnterAnim="@anim/slide_in_left"
|
|
||||||
app:popExitAnim="@anim/slide_out_right" />
|
|
||||||
<argument
|
|
||||||
android:name="courseId"
|
|
||||||
app:argType="long" />
|
|
||||||
<argument
|
|
||||||
android:name="title"
|
|
||||||
app:argType="string" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/course_events"
|
android:id="@+id/course_events"
|
||||||
android:name="de.sebse.fuplanner2.ui.details_events.EventsFragment"
|
android:name="de.sebse.fuplanner2.ui.details_events.EventsFragment"
|
||||||
@@ -89,37 +27,9 @@
|
|||||||
android:name="title"
|
android:name="title"
|
||||||
app:argType="string" />
|
app:argType="string" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
|
||||||
android:id="@+id/course_announcements"
|
|
||||||
android:name="de.sebse.fuplanner2.ui.details_announcements.AnnouncementsFragment"
|
|
||||||
android:label="{title}"
|
|
||||||
tools:layout="@layout/fragment_refresh_recycler">
|
|
||||||
<argument
|
|
||||||
android:name="courseId"
|
|
||||||
app:argType="long" />
|
|
||||||
<argument
|
|
||||||
android:name="title"
|
|
||||||
app:argType="string" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/course_description"
|
|
||||||
android:name="de.sebse.fuplanner2.ui.details_description.DescriptionFragment"
|
|
||||||
android:label="{title}">
|
|
||||||
<argument
|
|
||||||
android:name="courseId"
|
|
||||||
app:argType="long" />
|
|
||||||
<argument
|
|
||||||
android:name="title"
|
|
||||||
app:argType="string" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/nav_notifications"
|
android:id="@+id/nav_notifications"
|
||||||
android:name="de.sebse.fuplanner2.ui.notification.NotificationFragment"
|
android:name="de.sebse.fuplanner2.ui.notification.NotificationFragment"
|
||||||
android:label="@string/menu_notifications"
|
android:label="@string/menu_notifications"
|
||||||
tools:layout="@layout/notification_fragment">
|
tools:layout="@layout/notification_fragment"/>
|
||||||
<action
|
|
||||||
android:id="@+id/action_nav_notifications_to_course_details"
|
|
||||||
app:destination="@id/course_details"
|
|
||||||
app:popUpTo="@id/nav_courses" />
|
|
||||||
</fragment>
|
|
||||||
</navigation>
|
</navigation>
|
||||||
@@ -9,4 +9,5 @@
|
|||||||
<dimen name="card_view_margin">4dp</dimen>
|
<dimen name="card_view_margin">4dp</dimen>
|
||||||
<dimen name="card_view_padding">5dp</dimen>
|
<dimen name="card_view_padding">5dp</dimen>
|
||||||
<dimen name="card_view_elevation">4dp</dimen>
|
<dimen name="card_view_elevation">4dp</dimen>
|
||||||
|
<dimen name="header_padding">16dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -59,6 +59,10 @@
|
|||||||
<string name="dialog_location_br"><![CDATA[<b>Location:</b><br>%1$s<br>]]></string>
|
<string name="dialog_location_br"><![CDATA[<b>Location:</b><br>%1$s<br>]]></string>
|
||||||
<string name="dialog_time"><![CDATA[<b>Time:</b><br>%1$s - %2$s]]></string>
|
<string name="dialog_time"><![CDATA[<b>Time:</b><br>%1$s - %2$s]]></string>
|
||||||
<string name="dialog_course_br"><![CDATA[<b>Course:</b><br>%1$s<br>]]></string>
|
<string name="dialog_course_br"><![CDATA[<b>Course:</b><br>%1$s<br>]]></string>
|
||||||
|
<string name="course_type">Course Type</string>
|
||||||
|
<string name="description">Description</string>
|
||||||
|
<string name="resources">Resources</string>
|
||||||
|
<string name="announcement">Announcement</string>
|
||||||
<plurals name="not_course_update_text">
|
<plurals name="not_course_update_text">
|
||||||
<item quantity="one">One course message</item>
|
<item quantity="one">One course message</item>
|
||||||
<item quantity="other">%1$d course messages</item>
|
<item quantity="other">%1$d course messages</item>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="FUTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
|
<style name="FUTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
package de.sebse.fuplanner2
|
package de.sebse.fuplanner2
|
||||||
|
|
||||||
|
import de.sebse.fuplanner2.ui.tools.previews.AnnouncementPreviewProvider
|
||||||
|
import de.sebse.fuplanner2.utils.getFaker
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
*/
|
*/
|
||||||
class ExampleUnitTest {
|
class FakerTest {
|
||||||
@Test
|
@Test
|
||||||
fun addition_isCorrect() {
|
fun lorem() {
|
||||||
assertEquals(4, 2 + 2)
|
getFaker().strings.lorem(100, 1000)
|
||||||
|
getFaker().strings.lorem(100, 1000)
|
||||||
|
getFaker().strings.lorem(100, 1000)
|
||||||
|
getFaker().strings.lorem(100, 1000)
|
||||||
|
getFaker().strings.lorem(100, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun announcement() {
|
||||||
|
AnnouncementPreviewProvider().values.take(10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.5.31'
|
ext.kotlin_version = '1.5.31'
|
||||||
|
ext.compose_version = '1.0.5'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -10,7 +11,7 @@ buildscript {
|
|||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5'
|
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.4.0-beta02'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
Reference in New Issue
Block a user