Migrate to Compose
This commit is contained in:
@@ -87,5 +87,7 @@ dependencies {
|
||||
implementation "androidx.compose.foundation:foundation:$compose_version"
|
||||
implementation "androidx.compose.foundation:foundation-layout:$compose_version"
|
||||
implementation "androidx.compose.material:material:$compose_version"
|
||||
implementation "androidx.navigation:navigation-compose:2.4.0-beta02"
|
||||
|
||||
implementation "com.google.android.material:compose-theme-adapter:$compose_version"
|
||||
}
|
||||
|
||||
@@ -3,40 +3,35 @@ package de.sebse.fuplanner2
|
||||
import android.accounts.Account
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.TextView
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.*
|
||||
import androidx.navigation.NavOptions
|
||||
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 androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.composethemeadapter.MdcTheme
|
||||
import de.sebse.fuplanner2.auth.AppAccounts
|
||||
import de.sebse.fuplanner2.database.AppDatabase
|
||||
import de.sebse.fuplanner2.database.Course
|
||||
import de.sebse.fuplanner2.database.User
|
||||
import de.sebse.fuplanner2.databinding.ActivityMainBinding
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class MainActivity() : AppCompatActivity() {
|
||||
|
||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||
private lateinit var activityViewModel: MainActivityViewModel
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||
activityViewModel = ViewModelProvider(this)[MainActivityViewModel::class.java]
|
||||
setContent {
|
||||
MdcTheme {
|
||||
MainActivityComposable()
|
||||
}
|
||||
}
|
||||
/*setContentView(R.layout.activity_main)
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||
val navController = navHostFragment.navController
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setSupportActionBar(binding.appBarMain.toolbar)
|
||||
@@ -45,7 +40,8 @@ class MainActivity() : AppCompatActivity() {
|
||||
|
||||
activityViewModel.user.observe(this) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -57,7 +53,11 @@ class MainActivity() : AppCompatActivity() {
|
||||
)
|
||||
actionView = if (it == 0)
|
||||
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) {
|
||||
in 0..99 -> it.toString()
|
||||
else -> "99+"
|
||||
@@ -68,7 +68,7 @@ class MainActivity() : AppCompatActivity() {
|
||||
activityViewModel.latestSemester.observe(this) {
|
||||
val courseOrder = binding.navView.menu.findItem(R.id.nav_courses).order
|
||||
var i = binding.navView.menu.size() - 1
|
||||
while(i >= 0) {
|
||||
while (i >= 0) {
|
||||
val menuItem: MenuItem = binding.navView.menu.getItem(i--)
|
||||
if (menuItem.order / 100 == courseOrder / 100 && menuItem.order != courseOrder) {
|
||||
binding.navView.menu.removeItem(menuItem.itemId)
|
||||
@@ -76,20 +76,26 @@ class MainActivity() : AppCompatActivity() {
|
||||
}
|
||||
it.mapIndexed { index, course ->
|
||||
val itemOrder = courseOrder / 100 * 100 + index + 2
|
||||
binding.navView.menu.add(0, itemOrder, itemOrder, course.title).setOnMenuItemClickListener {
|
||||
navController.navigate(
|
||||
R.id.course_details,
|
||||
bundleOf("courseId" to course.uid, "title" to course.title),
|
||||
NavOptions.Builder().setPopUpTo(R.id.nav_courses, false).build()
|
||||
)
|
||||
binding.drawerLayout.closeDrawers()
|
||||
false
|
||||
}
|
||||
binding.navView.menu.add(0, itemOrder, itemOrder, course.title)
|
||||
.setOnMenuItemClickListener {
|
||||
navController.navigate(
|
||||
R.id.course_details,
|
||||
bundleOf("courseId" to course.uid, "title" to course.title),
|
||||
NavOptions.Builder().setPopUpTo(R.id.nav_courses, false).build()
|
||||
)
|
||||
binding.drawerLayout.closeDrawers()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||
@@ -98,7 +104,7 @@ class MainActivity() : AppCompatActivity() {
|
||||
if (intent.getBooleanExtra(EXTRA_OPEN_NOTIFICATIONS, false)) {
|
||||
navController.navigate(R.id.nav_notifications)
|
||||
intent.putExtra(EXTRA_OPEN_NOTIFICATIONS, false)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -115,7 +121,7 @@ class MainActivity() : AppCompatActivity() {
|
||||
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.
|
||||
//menuInflater.inflate(R.menu.main, menu)
|
||||
return true
|
||||
@@ -126,6 +132,13 @@ class MainActivity() : AppCompatActivity() {
|
||||
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 {
|
||||
const val EXTRA_OPEN_NOTIFICATIONS: String = "EXTRA_OPEN_NOTIFICATIONS"
|
||||
const val EXTRA_NETWORK_ERROR = "EXTRA_NETWORK_ERROR"
|
||||
@@ -141,7 +154,6 @@ class MainActivityViewModel : ViewModel() {
|
||||
val notificationCnt: LiveData<Int> = database.notificationDao().getUnreadRowCount()
|
||||
val latestSemester: LiveData<List<Course>> = database.courseDao().getLatestSemester()
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
fun updateSelectedUser(account: Account) {
|
||||
GlobalScope.launch {
|
||||
user.postValue(database.userDao().findByUsername(account.name))
|
||||
|
||||
@@ -43,7 +43,8 @@ class StartupActivity : AppCompatActivity() {
|
||||
AppAccounts.RefreshResults.SUCCESS -> {
|
||||
AppAccounts.getInstance().setPeriodicSync()
|
||||
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)
|
||||
intent.putExtra(MainActivity.EXTRA_NETWORK_ERROR, true)
|
||||
if (it == AppAccounts.RefreshResults.UNSPECIFIED_ERROR)
|
||||
|
||||
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 com.google.android.material.composethemeadapter.MdcTheme
|
||||
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() {
|
||||
MdcTheme {
|
||||
TopBar(
|
||||
title = "A title"
|
||||
) { }
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun DrawerPreview() {
|
||||
MdcTheme {
|
||||
Drawer(currentRoute = "courses", menu = screens) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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.components.CourseDetailsScreen
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package de.sebse.fuplanner2.ui.courses
|
||||
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings.Global.getString
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.composethemeadapter.MdcTheme
|
||||
import de.sebse.fuplanner2.databinding.FragmentRefreshRecyclerBinding
|
||||
|
||||
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 {
|
||||
navController = findNavController()
|
||||
coursesViewModel = ViewModelProvider(this).get(CoursesViewModel::class.java)
|
||||
binding = FragmentRefreshRecyclerBinding.inflate(inflater, container, false)
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
MdcTheme {
|
||||
val courses = coursesViewModel.text.observeAsState(listOf())
|
||||
LazyColumn {
|
||||
items(courses.value) { course -> CourseItem(course) {
|
||||
course.uid?.let {
|
||||
navController.navigate(CoursesFragmentDirections.actionNavHomeToCourseDetails(it, course.title))
|
||||
}
|
||||
} }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,5 @@ import de.sebse.fuplanner2.database.AppDatabase
|
||||
import de.sebse.fuplanner2.database.Course
|
||||
|
||||
class CoursesViewModel : ViewModel() {
|
||||
val text: LiveData<List<Course>> = AppDatabase.getInstance().courseDao().getAll()
|
||||
}
|
||||
val list: LiveData<List<Course>> = AppDatabase.getInstance().courseDao().getAll()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package de.sebse.fuplanner2.ui.courses
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -17,6 +19,8 @@ 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.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
@@ -24,12 +28,56 @@ 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.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import com.google.android.material.composethemeadapter.MdcTheme
|
||||
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.database.Lecturer
|
||||
import de.sebse.fuplanner2.ui.details.CoursePreviewProvider
|
||||
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)
|
||||
}
|
||||
MdcTheme {
|
||||
GroupedCourseList(groups = groups) { course ->
|
||||
course.uid?.let {
|
||||
Log.d("WHERE TO GO", "${MenuItem.Courses.route}/$it")
|
||||
tools.navTo("${MenuItem.Courses.route}/$it")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun GroupedCourseList(groups: Map<String, List<Course>>, onclick: (course: Course) -> Unit) {
|
||||
LazyColumn {
|
||||
for ((key, value) in groups.entries) {
|
||||
stickyHeader(key = key) {
|
||||
Text(text = key)
|
||||
}
|
||||
items(value) { course ->
|
||||
CourseItem(course) { onclick(course) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CourseList(courses: List<Course>, onclick: () -> Unit) {
|
||||
LazyColumn {
|
||||
@@ -64,7 +112,8 @@ fun CourseItem(id: Long?, title: String, lecturers: String, type: String, onclic
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onclick)
|
||||
.padding(dimensionResource(R.dimen.card_view_margin))
|
||||
.padding(dimensionResource(R.dimen.card_view_margin)),
|
||||
elevation = dimensionResource(R.dimen.card_view_elevation)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -113,37 +162,9 @@ fun CourseItemHint(icon: ImageVector, @StringRes imageAltRes: Int, text: String)
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
fun CourseListPreview() {
|
||||
val course = Course(
|
||||
userId = 1,
|
||||
lastRefreshed = 1,
|
||||
lvNumber = hashSetOf("1000"),
|
||||
title = "Kurs mit tollem Namen",
|
||||
description = "Beschreibung",
|
||||
internalId = "165464635",
|
||||
isSummerSemester = false,
|
||||
year = 2020,
|
||||
lecturers = listOf(
|
||||
Lecturer(
|
||||
firstName = "Max",
|
||||
lastName = "Maurer",
|
||||
email = "some@mail.com",
|
||||
isResponsible = false
|
||||
),
|
||||
Lecturer(
|
||||
firstName = "Peter",
|
||||
lastName = "Engelbert",
|
||||
email = "coolio@example.com",
|
||||
isResponsible = false
|
||||
)
|
||||
),
|
||||
moduleType = 1236,
|
||||
type = "Type"
|
||||
)
|
||||
MdcTheme {
|
||||
CourseList(
|
||||
listOf(
|
||||
course, course, course
|
||||
)
|
||||
CoursePreviewProvider().values.take(3).toList()
|
||||
) { }
|
||||
}
|
||||
}
|
||||
@@ -151,14 +172,8 @@ fun CourseListPreview() {
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview
|
||||
@Composable
|
||||
fun CourseItemPreview() {
|
||||
fun CourseItemPreview(@PreviewParameter(CoursePreviewProvider::class, 1) course: Course) {
|
||||
MdcTheme {
|
||||
CourseItem(
|
||||
12411111111,
|
||||
"Höhere Algorithmik",
|
||||
"M. Berta, P. Parker",
|
||||
"Vorlesung"
|
||||
) {}
|
||||
CourseItem(course) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,61 +6,47 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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 com.google.android.material.composethemeadapter.MdcTheme
|
||||
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
|
||||
): View {
|
||||
navController = findNavController()
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
val course = detailsViewModel.course.observeAsState(null).value
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.title = args.title
|
||||
MdcTheme {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.description),
|
||||
style = MaterialTheme.typography.h5
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,22 +54,45 @@ class DetailsFragment : Fragment() {
|
||||
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.firstName, lecturer.lastName))
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.send_email, lecturer.firstName, lecturer.lastName)))
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, args.title)
|
||||
intent.putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
getString(R.string.email_preview, lecturer.firstName, lecturer.lastName)
|
||||
)
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
intent,
|
||||
getString(R.string.send_email, lecturer.firstName, lecturer.lastName)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun launchFragment(btnType: DetailsAdapter.ButtonTypes) {
|
||||
when (btnType) {
|
||||
DetailsAdapter.ButtonTypes.DESCRIPTION ->
|
||||
this.navController.navigate(DetailsFragmentDirections.actionCourseDetailsToDescriptionFragment(args.courseId, title))
|
||||
this.navController.navigate(
|
||||
DetailsFragmentDirections.actionCourseDetailsToDescriptionFragment(
|
||||
args.courseId,
|
||||
args.title
|
||||
)
|
||||
)
|
||||
DetailsAdapter.ButtonTypes.RESOURCES -> TODO()
|
||||
DetailsAdapter.ButtonTypes.GRADEBOOK -> TODO()
|
||||
DetailsAdapter.ButtonTypes.ANNOUNCEMENTS ->
|
||||
this.navController.navigate(DetailsFragmentDirections.actionCourseDetailsToCourseAnnouncements(args.courseId, title))
|
||||
this.navController.navigate(
|
||||
DetailsFragmentDirections.actionCourseDetailsToCourseAnnouncements(
|
||||
args.courseId,
|
||||
args.title
|
||||
)
|
||||
)
|
||||
DetailsAdapter.ButtonTypes.ASSIGNMENTS -> TODO()
|
||||
DetailsAdapter.ButtonTypes.EVENTS ->
|
||||
this.navController.navigate(DetailsFragmentDirections.actionCourseDetailsToCourseEvents(args.courseId, title))
|
||||
this.navController.navigate(
|
||||
DetailsFragmentDirections.actionCourseDetailsToCourseEvents(
|
||||
args.courseId,
|
||||
args.title
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package de.sebse.fuplanner2.ui.details.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.GridCells
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
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.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.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.android.material.composethemeadapter.MdcTheme
|
||||
import de.sebse.fuplanner2.R
|
||||
import de.sebse.fuplanner2.Tools
|
||||
import de.sebse.fuplanner2.database.Course
|
||||
import de.sebse.fuplanner2.ui.details.CoursePreviewProvider
|
||||
import de.sebse.fuplanner2.ui.details.DetailsViewModel
|
||||
import de.sebse.fuplanner2.ui.details.DetailsViewModelFactory
|
||||
import de.sebse.fuplanner2.ui.details.LecturerItem
|
||||
|
||||
|
||||
@Composable
|
||||
fun CourseDetailsScreen(tools: Tools, id: Long) {
|
||||
val coursesViewModel: DetailsViewModel = viewModel(factory = DetailsViewModelFactory(id))
|
||||
val state = coursesViewModel.course.observeAsState()
|
||||
val title = state.value?.title
|
||||
LaunchedEffect(title) {
|
||||
title?.let { tools.setTitle(it) }
|
||||
}
|
||||
CourseDetailsScreen(state.value, id)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CourseDetailsScreen(course: Course?, id: Long) {
|
||||
Column {
|
||||
QuickLinks(courseId = id)
|
||||
Text(
|
||||
text = stringResource(R.string.lecturers),
|
||||
style = MaterialTheme.typography.h5
|
||||
)
|
||||
LazyColumn {
|
||||
items(course?.lecturers ?: listOf()) {
|
||||
LecturerItem(lecturer = it, courseTitle = course?.title ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class QuickLinkProps(@StringRes val name: Int, val route: String)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun QuickLinks(courseId: Long) {
|
||||
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")
|
||||
)
|
||||
LazyVerticalGrid(
|
||||
cells = GridCells.Adaptive(150.dp),
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = 12.dp,
|
||||
vertical = 16.dp
|
||||
),
|
||||
) {
|
||||
items(list.size) { index ->
|
||||
Card(
|
||||
backgroundColor = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.padding(dimensionResource(R.dimen.card_view_margin))
|
||||
.fillMaxWidth(),
|
||||
elevation = dimensionResource(R.dimen.card_view_elevation),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(list[index].name),
|
||||
color = MaterialTheme.colors.onSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.h6,
|
||||
modifier = Modifier
|
||||
.padding(dimensionResource(R.dimen.card_view_padding))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CourseDetailsScreenPreview(@PreviewParameter(CoursePreviewProvider::class, 1) course: Course) {
|
||||
MdcTheme {
|
||||
CourseDetailsScreen(course, course.uid!!)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun QuickLinksPreview() {
|
||||
MdcTheme {
|
||||
QuickLinks(3)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package de.sebse.fuplanner2.ui.details
|
||||
|
||||
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.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.filled.Email
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
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 androidx.compose.ui.unit.dp
|
||||
import com.google.android.material.composethemeadapter.MdcTheme
|
||||
import de.sebse.fuplanner2.R
|
||||
import de.sebse.fuplanner2.database.Lecturer
|
||||
|
||||
@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) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dimensionResource(R.dimen.card_view_margin)),
|
||||
elevation = dimensionResource(R.dimen.card_view_elevation)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(true, onClick = click)
|
||||
.padding(dimensionResource(R.dimen.card_view_padding)),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
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
|
||||
.height(70.dp)
|
||||
.aspectRatio(1f, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LecturerItemPreview(@PreviewParameter(LecturerPreviewProvider::class, 2) lecturer: Lecturer) {
|
||||
MdcTheme {
|
||||
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,62 @@
|
||||
package de.sebse.fuplanner2.ui.details
|
||||
|
||||
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> {
|
||||
private val resp = faker.primitive.int(1, 2)
|
||||
override val values = (0..10).map {
|
||||
getLecturer(it < resp)
|
||||
}.asSequence()
|
||||
|
||||
private fun getLecturer(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> {
|
||||
var isSummer = false
|
||||
var year = 21
|
||||
override val values = (0..10).map {
|
||||
val res = getCourse()
|
||||
val reduce = faker.primitive.bool(.3f)
|
||||
year = if (reduce && isSummer) year-1 else year
|
||||
isSummer = if (reduce) !isSummer else isSummer
|
||||
res
|
||||
}.asSequence()
|
||||
|
||||
private fun getCourse(): 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
123
app/src/main/java/de/sebse/fuplanner2/utils/faking.kt
Normal file
123
app/src/main/java/de/sebse/fuplanner2/utils/faking.kt
Normal file
@@ -0,0 +1,123 @@
|
||||
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 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"
|
||||
}
|
||||
}
|
||||
|
||||
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 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()
|
||||
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 {
|
||||
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 {
|
||||
REMOVED, UPDATED, ADDED;
|
||||
|
||||
companion object {
|
||||
val values: List<CourseUpdateType> = values().toList()
|
||||
}
|
||||
@@ -32,13 +33,19 @@ object Notifications {
|
||||
|
||||
enum class CourseUpdateEntity {
|
||||
COURSE, ANNOUNCEMENT, ASSIGNMENT, GRADE, RESOURCE, EVENT;
|
||||
|
||||
companion object {
|
||||
val values: List<CourseUpdateEntity> = values().toList()
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -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(
|
||||
CourseUpdateType.REMOVED to updates.removed,
|
||||
CourseUpdateType.ADDED to updates.added,
|
||||
@@ -95,7 +106,13 @@ object Notifications {
|
||||
val notification = NotificationCompat.Builder(actCtx, COURSE_UPDATE_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_logo_mono)
|
||||
.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 ->
|
||||
unread.forEach { not ->
|
||||
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 added: ArrayList<T> = arrayListOf(),
|
||||
val updated: ArrayList<T> = arrayListOf(),
|
||||
@@ -137,12 +154,15 @@ data class UpdateResult<T: Updatable>(
|
||||
}
|
||||
|
||||
val values: List<T>
|
||||
get() {
|
||||
return this.added + this.updated + this.unmodified
|
||||
}
|
||||
get() {
|
||||
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.added.addAll(plus.added)
|
||||
source.updated.addAll(plus.updated)
|
||||
@@ -163,11 +183,13 @@ interface UpdatableCompanion {
|
||||
type: Notifications.CourseUpdateType,
|
||||
data: JsonObject
|
||||
): CharSequence?
|
||||
|
||||
fun adapterText(
|
||||
actCtx: Context,
|
||||
type: Notifications.CourseUpdateType,
|
||||
data: JsonObject
|
||||
): CharSequence?
|
||||
|
||||
fun adapterCallback(
|
||||
actCtx: Context,
|
||||
type: Notifications.CourseUpdateType,
|
||||
@@ -176,7 +198,7 @@ interface UpdatableCompanion {
|
||||
): () -> 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
|
||||
.map { it.getIdentifier() to Pair(it.getHash(), it) }
|
||||
.toMap()
|
||||
@@ -200,4 +222,3 @@ fun <T: Updatable> updateResultOf(old: List<T>, new: List<T>): UpdateResult<T> {
|
||||
}
|
||||
return UpdateResult(removed, added, updated, unmodified)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.util.Log
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.work.*
|
||||
import de.sebse.fuplanner2.R
|
||||
@@ -56,7 +57,7 @@ object console {
|
||||
object xml {
|
||||
fun decode(xml: String): String {
|
||||
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 {
|
||||
@Suppress("DEPRECATION")
|
||||
Html.fromHtml(xml).toString()
|
||||
@@ -76,7 +77,8 @@ object color {
|
||||
|
||||
// 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 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)
|
||||
@@ -93,7 +95,7 @@ object color {
|
||||
g: Double,
|
||||
b: Double
|
||||
): Int {
|
||||
return 0xFF000000.toInt() + (r*0xFF).roundToInt() * 0x10000 + (g*0xFF).roundToInt() * 0x100 + (b*0xFF).roundToInt()
|
||||
return 0xFF000000.toInt() + (r * 0xFF).roundToInt() * 0x10000 + (g * 0xFF).roundToInt() * 0x100 + (b * 0xFF).roundToInt()
|
||||
}
|
||||
|
||||
private fun hsvToRgb(
|
||||
@@ -118,24 +120,27 @@ object color {
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T: ListenableWorker> enqueueOneTimeWork(appCtx: Context, workBuilder: (OneTimeWorkRequest.Builder) -> OneTimeWorkRequest.Builder): LiveData<WorkInfo> {
|
||||
inline fun <reified T : ListenableWorker> enqueueOneTimeWork(
|
||||
appCtx: Context,
|
||||
workBuilder: (OneTimeWorkRequest.Builder) -> OneTimeWorkRequest.Builder
|
||||
): LiveData<WorkInfo> {
|
||||
val work = workBuilder(OneTimeWorkRequestBuilder<T>()).build()
|
||||
val workManager = WorkManager.getInstance(appCtx)
|
||||
workManager.enqueue(work)
|
||||
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() }
|
||||
}
|
||||
|
||||
fun hashCodeOf(vararg elements: Any?): Int {
|
||||
var code = 0
|
||||
elements.forEach { code = code*31 + (it?.hashCode() ?: 0) }
|
||||
elements.forEach { code = code * 31 + (it?.hashCode() ?: 0) }
|
||||
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) {
|
||||
@@ -147,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, 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.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.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 Long.timeAgoString(actCtx: Context): String {
|
||||
@@ -173,9 +184,21 @@ fun Long.timeAgoString(actCtx: Context): String {
|
||||
val diff = now - this
|
||||
return when {
|
||||
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 < 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)
|
||||
diff < 60 * MINUTE_MILLIS -> actCtx.getQuantityString(
|
||||
R.plurals.time_minutes_ago,
|
||||
(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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,4 +259,24 @@ fun String.dateStringToLong(format: String, locale: Locale): Long? {
|
||||
e.printStackTrace()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/linear_layout"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:showIn="@layout/activity_main"
|
||||
tools:context=".ui.courses.CoursesFragment">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:scrollbars="vertical"
|
||||
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"?>
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/swipe_refresh_layout"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:showIn="@layout/activity_main"
|
||||
tools:context=".ui.courses.CoursesFragment">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:scrollbars="vertical"
|
||||
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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
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>
|
||||
android:id="@+id/nav_graph">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_canteen"
|
||||
@@ -47,8 +25,7 @@
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/course_details"
|
||||
android:name="de.sebse.fuplanner2.ui.details.DetailsFragment"
|
||||
android:label="{title}">
|
||||
android:name="de.sebse.fuplanner2.ui.details.DetailsFragment">
|
||||
<action
|
||||
android:id="@+id/action_course_details_to_descriptionFragment"
|
||||
app:destination="@id/course_description"
|
||||
@@ -62,14 +39,14 @@
|
||||
app:enterAnim="@anim/slide_in_right"
|
||||
app:exitAnim="@anim/slide_out_left"
|
||||
app:popEnterAnim="@anim/slide_in_left"
|
||||
app:popExitAnim="@anim/slide_out_right" />
|
||||
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" />
|
||||
app:popExitAnim="@anim/slide_out_right" />
|
||||
<argument
|
||||
android:name="courseId"
|
||||
app:argType="long" />
|
||||
@@ -122,4 +99,4 @@
|
||||
app:destination="@id/course_details"
|
||||
app:popUpTo="@id/nav_courses" />
|
||||
</fragment>
|
||||
</navigation>
|
||||
</navigation>
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
<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="course_type">Course Type</string>
|
||||
<string name="description">Description</string>
|
||||
<string name="resources">Resources</string>
|
||||
<plurals name="not_course_update_text">
|
||||
<item quantity="one">One course message</item>
|
||||
<item quantity="other">%1$d course messages</item>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package de.sebse.fuplanner2
|
||||
|
||||
import de.sebse.fuplanner2.utils.getFaker
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
class FakerTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
fun lorem() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||
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
|
||||
// in the individual module build.gradle files
|
||||
|
||||
Reference in New Issue
Block a user