diff --git a/app/build.gradle b/app/build.gradle index db1ca7f..69807ce 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,10 +32,12 @@ android { buildFeatures { viewBinding true + compose true } - dataBinding { - enabled = true + composeOptions { + kotlinCompilerVersion kotlin_version + kotlinCompilerExtensionVersion compose_version } // To inline the bytecode built with JVM target 1.8 into @@ -54,7 +56,6 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' @@ -77,4 +78,14 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + // Compose + implementation "androidx.compose.runtime:runtime:$compose_version" + implementation "androidx.compose.runtime:runtime-livedata:$compose_version" + implementation "androidx.compose.ui:ui:$compose_version" + implementation "androidx.compose.ui:ui-tooling:$compose_version" + implementation "androidx.compose.foundation:foundation:$compose_version" + implementation "androidx.compose.foundation:foundation-layout:$compose_version" + implementation "androidx.compose.material:material:$compose_version" + implementation "com.google.android.material:compose-theme-adapter:$compose_version" } diff --git a/app/src/main/java/de/sebse/fuplanner2/database/Lecturer.kt b/app/src/main/java/de/sebse/fuplanner2/database/Lecturer.kt index a4b24b5..cd71011 100644 --- a/app/src/main/java/de/sebse/fuplanner2/database/Lecturer.kt +++ b/app/src/main/java/de/sebse/fuplanner2/database/Lecturer.kt @@ -5,7 +5,7 @@ import com.beust.klaxon.KlaxonException data class Lecturer ( - val fistName: String, + val firstName: String, val lastName: String, val email: String, val isResponsible: Boolean diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesAdapter.kt b/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesAdapter.kt deleted file mode 100644 index 828a0e6..0000000 --- a/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesAdapter.kt +++ /dev/null @@ -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() { - - private val positionalData: ArrayList = arrayListOf() - var dataset: List = 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>(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(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 -} diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesFragment.kt b/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesFragment.kt index 0c72a45..fe13607 100644 --- a/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesFragment.kt +++ b/app/src/main/java/de/sebse/fuplanner2/ui/courses/CoursesFragment.kt @@ -1,25 +1,20 @@ 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.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 com.google.android.material.composethemeadapter.MdcTheme 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() { @@ -32,42 +27,23 @@ class CoursesFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - val viewManager = LinearLayoutManager(context) - val viewAdapter = CoursesAdapter(this::onClick) + ): View { + navController = findNavController() 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 + 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)) + } + } } + } + } } - binding.swipeRefreshLayout.setOnRefreshListener { - val work = OneTimeWorkRequestBuilder() - .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)) } } } diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/courses/components.kt b/app/src/main/java/de/sebse/fuplanner2/ui/courses/components.kt new file mode 100644 index 0000000..a0fd4ff --- /dev/null +++ b/app/src/main/java/de/sebse/fuplanner2/ui/courses/components.kt @@ -0,0 +1,154 @@ +package de.sebse.fuplanner2.ui.courses + +import android.content.Context +import android.content.res.Configuration +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.* +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.ui.Modifier +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 com.google.android.material.composethemeadapter.MdcTheme +import de.sebse.fuplanner2.R +import de.sebse.fuplanner2.database.Course +import de.sebse.fuplanner2.database.Lecturer +import de.sebse.fuplanner2.utils.color.getColor + +@Composable +fun CourseList(courses: List, 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) { + Card( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onclick) + .padding(dimensionResource(R.dimen.card_view_margin)) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(dimensionResource(R.dimen.card_view_padding)) + ) { + Text( + text = title, + color = Color(getColor(LocalContext.current, id ?: 0, highContrast = true)), + 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() { + 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 + )) { } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview +@Composable +fun CourseItemPreview() { + MdcTheme { + CourseItem( + 12411111111, + "Höhere Algorithmik", + "M. Berta, P. Parker", + "Vorlesung" + ) {} + } +} + diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsAdapter.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsAdapter.kt index c78dcae..f2026b9 100644 --- a/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsAdapter.kt +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsAdapter.kt @@ -61,7 +61,7 @@ class DetailsAdapter(private val onQuickLink: (ButtonTypes) -> Unit, private val holder.btnResources.setOnClickListener { this.onQuickLink(ButtonTypes.RESOURCES) } } is MailHolder -> cast(positionalData[position])?.let { type -> - holder.title.text = res.getString(R.string.full_name, type.fistName, type.lastName) + holder.title.text = res.getString(R.string.full_name, type.firstName, type.lastName) holder.subLeft.text = type.email holder.itemView.setOnClickListener { this.onMailTo(type) } } diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsFragment.kt b/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsFragment.kt index 4f097ee..d9aae85 100644 --- a/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsFragment.kt +++ b/app/src/main/java/de/sebse/fuplanner2/ui/details/DetailsFragment.kt @@ -69,8 +69,8 @@ class DetailsFragment : Fragment() { 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))) + 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) { diff --git a/app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleFragment.kt b/app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleFragment.kt index 1160167..68b3a16 100644 --- a/app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleFragment.kt +++ b/app/src/main/java/de/sebse/fuplanner2/ui/schedule/ScheduleFragment.kt @@ -1,24 +1,21 @@ package de.sebse.fuplanner2.ui.schedule import android.content.Context -import android.content.res.Configuration import android.os.Bundle import android.text.SpannableStringBuilder import android.view.* -import androidx.annotation.ColorInt import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import com.alamkanak.weekview.WeekView -import com.alamkanak.weekview.WeekViewDisplayable import com.alamkanak.weekview.WeekViewEntity -import com.alamkanak.weekview.WeekViewEvent import de.sebse.fuplanner2.R import de.sebse.fuplanner2.database.AppDatabase import de.sebse.fuplanner2.database.Course import de.sebse.fuplanner2.database.Event 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.toTimeString import kotlinx.coroutines.Dispatchers @@ -26,8 +23,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.* -import kotlin.math.roundToInt -import kotlin.random.Random class ScheduleFragment : Fragment() { @@ -164,56 +159,6 @@ data class ContextEvent(val actCtx: Context, val event: Event) { .setStyle(style) .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 { - 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( diff --git a/app/src/main/java/de/sebse/fuplanner2/utils/utils.kt b/app/src/main/java/de/sebse/fuplanner2/utils/utils.kt index 2486cad..618dc1b 100644 --- a/app/src/main/java/de/sebse/fuplanner2/utils/utils.kt +++ b/app/src/main/java/de/sebse/fuplanner2/utils/utils.kt @@ -5,11 +5,13 @@ package de.sebse.fuplanner2.utils import android.annotation.SuppressLint import android.annotation.TargetApi import android.content.Context +import android.content.res.Configuration import android.os.Build import android.text.Html import android.text.Spanned import android.text.format.DateFormat import android.util.Log +import androidx.annotation.ColorInt import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.lifecycle.LiveData @@ -20,6 +22,8 @@ import kotlinx.coroutines.runBlocking import java.text.ParseException import java.text.SimpleDateFormat import java.util.* +import kotlin.math.roundToInt +import kotlin.random.Random object console { @@ -60,6 +64,60 @@ object xml { } } +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 { + 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 enqueueOneTimeWork(appCtx: Context, workBuilder: (OneTimeWorkRequest.Builder) -> OneTimeWorkRequest.Builder): LiveData { val work = workBuilder(OneTimeWorkRequestBuilder()).build() val workManager = WorkManager.getInstance(appCtx) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7cd19dc..77bc890 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,6 +59,7 @@ Location:
%1$s
]]>
Time:
%1$s - %2$s]]>
Course:
%1$s
]]>
+ Course Type One course message %1$d course messages diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 82238fb..2f2e821 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ -