diff --git a/app/src/main/java/com/sapuseven/untis/activities/MainActivity.kt b/app/src/main/java/com/sapuseven/untis/activities/MainActivity.kt index 2cb69218e..c93ed465c 100644 --- a/app/src/main/java/com/sapuseven/untis/activities/MainActivity.kt +++ b/app/src/main/java/com/sapuseven/untis/activities/MainActivity.kt @@ -73,6 +73,7 @@ import com.sapuseven.untis.ui.functional.bottomInsets import com.sapuseven.untis.ui.functional.insetsPaddingValues import com.sapuseven.untis.ui.models.NavItemShortcut import com.sapuseven.untis.ui.preferences.convertRangeToPair +import com.sapuseven.untis.ui.preferences.decodeStoredTimetableValue import com.sapuseven.untis.ui.weekview.* import io.sentry.Breadcrumb import io.sentry.Sentry @@ -838,6 +839,7 @@ class NewMainAppState @OptIn(ExperimentalMaterial3Api::class) constructor( private suspend fun prepareItems( items: List ): List { + val hiddenSubjects = decodeStoredTimetableValue(preferences.timetableHiddenElements.getValue()).orEmpty() val newItems = mergeItems(items.mapNotNull { item -> if (item.periodData.isCancelled() && preferences.timetableHideCancelled.getValue()) return@mapNotNull null @@ -853,6 +855,9 @@ class NewMainAppState @OptIn(ExperimentalMaterial3Api::class) constructor( && item.periodData.element.backColor != UNTIS_DEFAULT_COLOR } } + if (item.periodData.subjects.isNotEmpty() && hiddenSubjects.containsAll(item.periodData.subjects)) { + return@mapNotNull null + } item }) colorItems(newItems) diff --git a/app/src/main/java/com/sapuseven/untis/activities/SettingsActivity.kt b/app/src/main/java/com/sapuseven/untis/activities/SettingsActivity.kt index 11a1ac06b..8fe75168f 100755 --- a/app/src/main/java/com/sapuseven/untis/activities/SettingsActivity.kt +++ b/app/src/main/java/com/sapuseven/untis/activities/SettingsActivity.kt @@ -45,6 +45,7 @@ import com.sapuseven.untis.BuildConfig import com.sapuseven.untis.R import com.sapuseven.untis.data.databases.entities.User import com.sapuseven.untis.helpers.SerializationUtils.getJSON +import com.sapuseven.untis.helpers.timetable.TimetableDatabaseInterface import com.sapuseven.untis.models.github.GithubUser import com.sapuseven.untis.preferences.* import com.sapuseven.untis.receivers.AutoMuteReceiver @@ -630,6 +631,20 @@ class SettingsActivity : BaseComposeActivity() { dataStore = dataStorePreferences.timetableRange ) + ElementPickerPreference( + title = { Text(stringResource(id = R.string.maindrawer_hide_subjects))}, + icon = { + Icon( + painter = painterResource(id = R.drawable.all_hide_subjects), + contentDescription = null + ) + }, + type = TimetableDatabaseInterface.Type.SUBJECT, + multiSelect = true, + dataStore = dataStorePreferences.timetableHiddenElements, + timetableDatabaseInterface = timetableDatabaseInterface, + ) + /*SwitchPreference( title = { Text(stringResource(R.string.preference_timetable_range_index_reset)) }, dependency = dataStorePreferences.timetableRange, diff --git a/app/src/main/java/com/sapuseven/untis/preferences/DataStorePreferences.kt b/app/src/main/java/com/sapuseven/untis/preferences/DataStorePreferences.kt index 372062227..045724909 100644 --- a/app/src/main/java/com/sapuseven/untis/preferences/DataStorePreferences.kt +++ b/app/src/main/java/com/sapuseven/untis/preferences/DataStorePreferences.kt @@ -244,7 +244,12 @@ val BaseComposeActivity.dataStorePreferences: DataStorePreferences currentUserId(), "infocenter_absences_timerange", defaultValue = "current_schoolyear" - ) + ), + timetableHiddenElements = this.stringDataStore( + currentUserId(), + "timetable_hidden_elements", + defaultValue = "" + ), ) } @@ -300,5 +305,6 @@ class DataStorePreferences( val schoolBackground: UntisPreferenceDataStore>, val infocenterAbsencesOnlyUnexcused: UntisPreferenceDataStore, val infocenterAbsencesSortReverse: UntisPreferenceDataStore, - val infocenterAbsencesTimeRange: UntisPreferenceDataStore + val infocenterAbsencesTimeRange: UntisPreferenceDataStore, + val timetableHiddenElements: UntisPreferenceDataStore, ) diff --git a/app/src/main/java/com/sapuseven/untis/ui/dialogs/ElementPickerDialog.kt b/app/src/main/java/com/sapuseven/untis/ui/dialogs/ElementPickerDialog.kt index d3bc31e90..c1bf4e280 100644 --- a/app/src/main/java/com/sapuseven/untis/ui/dialogs/ElementPickerDialog.kt +++ b/app/src/main/java/com/sapuseven/untis/ui/dialogs/ElementPickerDialog.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import com.google.accompanist.flowlayout.MainAxisAlignment import com.sapuseven.untis.R import com.sapuseven.untis.helpers.timetable.TimetableDatabaseInterface import com.sapuseven.untis.models.untis.timetable.PeriodElement @@ -48,7 +49,8 @@ fun ElementPickerDialogFullscreen( onDismiss: (success: Boolean) -> Unit = {}, onSelect: (selectedItem: PeriodElement?) -> Unit = {}, onMultiSelect: (selectedItems: List) -> Unit = {}, - additionalActions: (@Composable () -> Unit) = {} + additionalActions: (@Composable () -> Unit) = {}, + selectedElements: List? = null, ) { var selectedType by remember { mutableStateOf(initialType) } var showSearch by remember { mutableStateOf(false) } @@ -57,7 +59,9 @@ fun ElementPickerDialogFullscreen( val items = remember(selectedType) { mutableStateMapOf().apply { timetableDatabaseInterface.getElements(selectedType) - .associateWith { false } + .associateWith { + selectedElements?.contains(it) ?: false + } .also { putAll(it) } @@ -191,24 +195,30 @@ fun ElementPickerDialogFullscreen( /** * A minimal dialog version of the element picker. - * Missing features currently are: search, toolbar actions, multi select (due to missing confirm button), close button. + * Missing features currently are: toolbar actions, close button. */ +@OptIn(ExperimentalLayoutApi::class) @Composable fun ElementPickerDialog( title: (@Composable () -> Unit)?, timetableDatabaseInterface: TimetableDatabaseInterface, initialType: TimetableDatabaseInterface.Type? = null, + multiSelect: Boolean, hideTypeSelection: Boolean = false, hideTypeSelectionPersonal: Boolean = false, onDismiss: (success: Boolean) -> Unit = {}, - onSelect: (selectedItem: PeriodElement?) -> Unit = {} + onSelect: (selectedItem: PeriodElement?) -> Unit = {}, + onMultiSelect: (selectedItems: List) -> Unit, + initialSelection: List? = null, ) { var selectedType by remember { mutableStateOf(initialType) } val items = remember(selectedType) { mutableStateMapOf().apply { timetableDatabaseInterface.getElements(selectedType) - .associateWith { false } + .associateWith { + initialSelection?.contains(it) ?: false + } .also { putAll(it) } @@ -235,12 +245,48 @@ fun ElementPickerDialog( } } + if (multiSelect) { + val interactionSource = remember { MutableInteractionSource() } + val selectAll: ((Boolean) -> Unit) = { selectState -> + items.forEach { (item, _) -> items[item] = selectState } + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 24.dp) + ) { + Checkbox( + checked = !items.values.contains(false), + onCheckedChange = selectAll, + interactionSource = interactionSource + ) + + Text( + text = stringResource(id = R.string.elementpicker_select_all), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .clickable( + interactionSource = interactionSource, + indication = null, + role = Role.Checkbox + ) { + selectAll(items.values.contains(false)) + } + .weight(1f) + ) + } + + Divider() + } + ElementPickerElements( timetableDatabaseInterface = timetableDatabaseInterface, selectedType = selectedType, onDismiss = onDismiss, onSelect = onSelect, items = items, + multiSelect = multiSelect, modifier = Modifier .fillMaxWidth() .weight(1f) @@ -255,6 +301,27 @@ fun ElementPickerDialog( onDismiss = onDismiss, onSelect = onSelect ) + + if (multiSelect) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 24.dp) + .padding(horizontal = 24.dp), + ) { + TextButton(onClick = { + onDismiss(false) + }) { + Text(text = stringResource(id = R.string.all_cancel)) + } + TextButton(onClick = { + onMultiSelect(items.filter { it.value }.keys.toList()) + onDismiss(true) + }) { + Text(text = stringResource(id = R.string.all_ok)) + } + } + } } } } @@ -276,7 +343,7 @@ fun ElementPickerElements( contentAlignment = Alignment.Center, modifier = modifier ) { - selectedType?.let { + selectedType?.let { type -> LazyVerticalGrid( columns = GridCells.Adaptive(if (multiSelect) 128.dp else 96.dp), modifier = Modifier.fillMaxHeight(), @@ -288,7 +355,11 @@ fun ElementPickerElements( object { val element = it val name = timetableDatabaseInterface.getShortName(it) - val enabled = timetableDatabaseInterface.isAllowed(it) + val enabled = if (type != TimetableDatabaseInterface.Type.SUBJECT) { + timetableDatabaseInterface.isAllowed(it) + } else { + true + } } } .filter { it.name.contains(filter, true) } @@ -303,7 +374,10 @@ fun ElementPickerElements( if (multiSelect) Checkbox( checked = items[item.element] ?: false, - onCheckedChange = { items[item.element] = it }, + onCheckedChange = { + onSelect(item.element) + items[item.element] = it + }, interactionSource = interactionSource, enabled = item.enabled ) @@ -370,10 +444,12 @@ fun ElementPickerTypeSelection( contentDescription = null ) }, - label = { AbbreviatedText( - text = stringResource(id = R.string.all_personal), - abbreviatedText = stringResource(id = R.string.all_personal_abbr) - ) }, + label = { + AbbreviatedText( + text = stringResource(id = R.string.all_personal), + abbreviatedText = stringResource(id = R.string.all_personal_abbr) + ) + }, selected = false, onClick = { onSelect(null) diff --git a/app/src/main/java/com/sapuseven/untis/ui/preferences/ElementPickerPreference.kt b/app/src/main/java/com/sapuseven/untis/ui/preferences/ElementPickerPreference.kt index 887d8b520..e3c26b3b7 100644 --- a/app/src/main/java/com/sapuseven/untis/ui/preferences/ElementPickerPreference.kt +++ b/app/src/main/java/com/sapuseven/untis/ui/preferences/ElementPickerPreference.kt @@ -2,6 +2,7 @@ package com.sapuseven.untis.ui.preferences import androidx.compose.material3.Text import androidx.compose.runtime.* +import androidx.compose.ui.text.style.TextOverflow import com.sapuseven.untis.helpers.SerializationUtils import com.sapuseven.untis.helpers.timetable.TimetableDatabaseInterface import com.sapuseven.untis.models.untis.timetable.PeriodElement @@ -18,7 +19,9 @@ fun ElementPickerPreference( dependency: UntisPreferenceDataStore<*>? = null, dataStore: UntisPreferenceDataStore, timetableDatabaseInterface: TimetableDatabaseInterface, - highlight: Boolean = false + type: TimetableDatabaseInterface.Type? = null, + multiSelect: Boolean = false, + highlight: Boolean = false, ) { val value = remember { mutableStateOf(dataStore.defaultValue) } var showDialog by remember { mutableStateOf(false) } @@ -26,15 +29,22 @@ fun ElementPickerPreference( val scope = rememberCoroutineScope() @Composable - fun generateSummary(element: PeriodElement): String { - return timetableDatabaseInterface.getShortName(element) + fun generateSummary(elements: List): String = elements.joinToString { element -> + timetableDatabaseInterface.getShortName(element) + } + + @Composable + fun summary(value: String) { + decodeStoredTimetableValue(value)?.let { + if (it.isNotEmpty()) { + Text(generateSummary(it), overflow = TextOverflow.Ellipsis, maxLines = 1) + } + } } Preference( title = title, - summary = decodeStoredTimetableValue(value.value)?.let { - { Text(generateSummary(it)) } - }, + summary = { summary(value = value.value) }, icon = icon, value = value, dependency = dependency, @@ -53,18 +63,36 @@ fun ElementPickerPreference( showDialog = false }, onSelect = { element -> - scope.launch { dataStore.saveValue(element?.let { encodeStoredTimetableValue(it) } ?: "") } - showDialog = false + if (!multiSelect) { + scope.launch { + dataStore.saveValue(element?.let { encodeStoredTimetableValue(listOf(it)) } ?: "") + } + showDialog = false + } + }, + multiSelect = multiSelect, + onMultiSelect = { elements -> + scope.launch { dataStore.saveValue(encodeStoredTimetableValue(elements)) } }, - initialType = decodeStoredTimetableValue(value.value)?.let { TimetableDatabaseInterface.Type.valueOf(it.type) } + initialSelection = decodeStoredTimetableValue(value.value), + hideTypeSelection = type != null, + initialType = type ?: + decodeStoredTimetableValue(value.value) + ?.firstOrNull() + ?.let { TimetableDatabaseInterface.Type.valueOf(it.type)}, ) } -fun encodeStoredTimetableValue(value: PeriodElement): String = - SerializationUtils.getJSON().encodeToString(value) +fun encodeStoredTimetableValue(values: List): String = + SerializationUtils.getJSON().encodeToString(values) -fun decodeStoredTimetableValue(value: String): PeriodElement? = try { - SerializationUtils.getJSON().decodeFromString(value) +fun decodeStoredTimetableValue(values: String): List? = try { + SerializationUtils.getJSON().decodeFromString(values) } catch (e: Throwable) { - null + // Backwards-compatibility when the value isn't stored as a list yet + try { + listOf(SerializationUtils.getJSON().decodeFromString(values)) + } catch (e: Throwable) { + null + } } diff --git a/app/src/main/java/com/sapuseven/untis/workers/AutoMuteSetupWorker.kt b/app/src/main/java/com/sapuseven/untis/workers/AutoMuteSetupWorker.kt index a0fe12486..9dd3daf12 100644 --- a/app/src/main/java/com/sapuseven/untis/workers/AutoMuteSetupWorker.kt +++ b/app/src/main/java/com/sapuseven/untis/workers/AutoMuteSetupWorker.kt @@ -16,11 +16,13 @@ import com.sapuseven.untis.data.databases.UserDatabase import com.sapuseven.untis.data.databases.entities.User import com.sapuseven.untis.helpers.config.booleanDataStore import com.sapuseven.untis.helpers.config.intDataStore +import com.sapuseven.untis.helpers.config.stringDataStore import com.sapuseven.untis.helpers.timetable.TimetableDatabaseInterface import com.sapuseven.untis.receivers.AutoMuteReceiver import com.sapuseven.untis.receivers.AutoMuteReceiver.Companion.EXTRA_BOOLEAN_MUTE import com.sapuseven.untis.receivers.AutoMuteReceiver.Companion.EXTRA_INT_ID import com.sapuseven.untis.receivers.AutoMuteReceiver.Companion.EXTRA_LONG_USER_ID +import com.sapuseven.untis.ui.preferences.decodeStoredTimetableValue import com.sapuseven.untis.workers.DailyWorker.Companion.WORKER_DATA_USER_ID import org.joda.time.LocalDateTime @@ -81,6 +83,13 @@ class AutoMuteSetupWorker(context: Context, params: WorkerParameters) : "preference_automute_minimum_break_length" ).getValue() + val hiddenSubjects = decodeStoredTimetableValue( + applicationContext.stringDataStore( + user.id, + "timetable_hidden_elements" + ).getValue() + ).orEmpty() + timetable.items.merged().sortedBy { it.startDateTime }.zipWithNext().withLast() .forEach { it.first?.let { item -> @@ -94,6 +103,9 @@ class AutoMuteSetupWorker(context: Context, params: WorkerParameters) : if (item.periodData.isCancelled() && !automuteCancelledLessons) return@forEach // lesson is cancelled + if (item.periodData.subjects.isNotEmpty() && hiddenSubjects.containsAll(item.periodData.subjects)) + return@forEach // lesson is hidden + val muteIntent = Intent(applicationContext, AutoMuteReceiver::class.java) .putExtra(EXTRA_INT_ID, id) diff --git a/app/src/main/java/com/sapuseven/untis/workers/NotificationSetupWorker.kt b/app/src/main/java/com/sapuseven/untis/workers/NotificationSetupWorker.kt index 173beb0dc..ea985a54e 100644 --- a/app/src/main/java/com/sapuseven/untis/workers/NotificationSetupWorker.kt +++ b/app/src/main/java/com/sapuseven/untis/workers/NotificationSetupWorker.kt @@ -23,6 +23,7 @@ import com.sapuseven.untis.data.timetable.TimegridItem import com.sapuseven.untis.helpers.DateTimeUtils import com.sapuseven.untis.helpers.config.booleanDataStore import com.sapuseven.untis.helpers.config.intDataStore +import com.sapuseven.untis.helpers.config.stringDataStore import com.sapuseven.untis.helpers.timetable.TimetableDatabaseInterface import com.sapuseven.untis.receivers.NotificationReceiver import com.sapuseven.untis.receivers.NotificationReceiver.Companion.EXTRA_BOOLEAN_CLEAR @@ -39,6 +40,7 @@ import com.sapuseven.untis.receivers.NotificationReceiver.Companion.EXTRA_STRING import com.sapuseven.untis.receivers.NotificationReceiver.Companion.EXTRA_STRING_NEXT_SUBJECT_LONG import com.sapuseven.untis.receivers.NotificationReceiver.Companion.EXTRA_STRING_NEXT_TEACHER import com.sapuseven.untis.receivers.NotificationReceiver.Companion.EXTRA_STRING_NEXT_TEACHER_LONG +import com.sapuseven.untis.ui.preferences.decodeStoredTimetableValue import com.sapuseven.untis.workers.DailyWorker.Companion.WORKER_DATA_USER_ID import org.joda.time.DateTime import org.joda.time.LocalDateTime @@ -115,8 +117,21 @@ class NotificationSetupWorker(context: Context, params: WorkerParameters) : "preference_notifications_in_multiple" ).getValue() - val preparedItems = timetable.items.filter { !it.periodData.isCancelled() } - .sortedBy { it.startDateTime }.merged().zipWithNext() + val hiddenSubjects = decodeStoredTimetableValue( + applicationContext.stringDataStore( + user.id, + "timetable_hidden_elements" + ).getValue() + ).orEmpty() + + val preparedItems = timetable.items + .filterNot { it.periodData.isCancelled() } + .filterNot { item -> + item.periodData.subjects.isNotEmpty() && hiddenSubjects.containsAll(item.periodData.subjects) + } + .sortedBy { it.startDateTime } + .merged() + .zipWithNext() if (preparedItems.isEmpty()) { Log.d(LOG_TAG, "No notifications to schedule") diff --git a/app/src/main/java/com/sapuseven/untis/workers/TimetableDependantWorker.kt b/app/src/main/java/com/sapuseven/untis/workers/TimetableDependantWorker.kt index 2e5d57b29..113016206 100644 --- a/app/src/main/java/com/sapuseven/untis/workers/TimetableDependantWorker.kt +++ b/app/src/main/java/com/sapuseven/untis/workers/TimetableDependantWorker.kt @@ -40,7 +40,7 @@ abstract class TimetableDependantWorker( "preference_timetable_personal_timetable", defaultValue = "" ).getValue() - ) + )?.firstOrNull() val elemId = customPersonalTimetable?.id ?: user.userData.elemId val elemType = customPersonalTimetable?.type ?: user.userData.elemType ?: "" diff --git a/app/src/main/res/drawable/all_hide_subjects.xml b/app/src/main/res/drawable/all_hide_subjects.xml new file mode 100644 index 000000000..5e0ff5160 --- /dev/null +++ b/app/src/main/res/drawable/all_hide_subjects.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 86a3d35bb..56f1b8525 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,6 +57,7 @@ Search for rooms… Search for teachers… Select a Timetable + Select all Unknown error Wrong username or key! The specified school doesn\'t exist! @@ -387,6 +388,8 @@ Show last 30 days Show last 90 days Current school year + Subjects to hide + Subject selection Open date picker Select Time Start time