Added FormattedDate.kt to produce formatted strings more easily; TaskOverviewAdapter.kt now produces expandable items

This commit is contained in:
denizk0461
2023-05-23 12:39:43 +02:00
parent ce0667300d
commit e52a055cf1
17 changed files with 266 additions and 53 deletions
+2 -2
View File
@@ -7,11 +7,11 @@
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\kduez\.android\avd\Pixel_6_A13_.avd" />
<value value="C:\Users\kduez\.android\avd\Nexus_4_API_24.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-05-22T13:36:19.819030Z" />
<timeTargetWasSelectedWithDropDown value="2023-05-23T10:00:38.080775800Z" />
</component>
</project>
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 21,
"identityHash": "4ac175b58eab4b387bd1ceb98f39a913",
"identityHash": "202a385ce4f79f957d41c8e4a2b8ab22",
"entities": [
{
"tableName": "studip_events",
@@ -268,7 +268,7 @@
},
{
"tableName": "event_tasks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`taskId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` INTEGER NOT NULL, `dueDate` INTEGER NOT NULL, `title` TEXT NOT NULL, `notes` TEXT NOT NULL, `room` TEXT NOT NULL, FOREIGN KEY(`eventId`) REFERENCES `studip_events`(`eventId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`taskId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` INTEGER NOT NULL, `dueDate` INTEGER NOT NULL, `notifyDate` INTEGER NOT NULL, `title` TEXT NOT NULL, `notes` TEXT NOT NULL, `room` TEXT NOT NULL, FOREIGN KEY(`eventId`) REFERENCES `studip_events`(`eventId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "taskId",
@@ -288,6 +288,12 @@
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notifyDate",
"columnName": "notifyDate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
@@ -332,7 +338,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4ac175b58eab4b387bd1ceb98f39a913')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '202a385ce4f79f957d41c8e4a2b8ab22')"
]
}
}
@@ -1,16 +1,18 @@
package com.denizk0461.weserplaner.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.denizk0461.weserplaner.BuildConfig
import androidx.transition.TransitionManager
import com.denizk0461.weserplaner.R
import com.denizk0461.weserplaner.data.AppDiffUtilCallback
import com.denizk0461.weserplaner.data.isInThePast
import com.denizk0461.weserplaner.databinding.ItemTaskBinding
import com.denizk0461.weserplaner.model.EventTask
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import com.denizk0461.weserplaner.model.FormattedDate
class TaskOverviewAdapter(
private val onClickListener: OnClickListener,
@@ -46,16 +48,65 @@ class TaskOverviewAdapter(
// Retrieve item for current position
val currentItem = tasks[position]
// Determine whether this item's view is expanded
var isExpanded = false
holder.binding.layoutExpandedContainer.visibility = View.GONE
// Context for easier reference
val context = holder.binding.root.context
// view set-up
SimpleDateFormat("yyyy-MM-dd, HH:mm:ss.SSS", Locale.GERMANY)
.format(Date(BuildConfig.BUILD_TIME_MILLIS))
// Set title text
holder.binding.textTitle.text = currentItem.title
// Set due date text to formatted date, TODO let user pick the date format
val dueDate = FormattedDate(currentItem.dueDate)
holder.binding.textDueDate.text = dueDate.localisedString(context)
if (currentItem.notifyDate == -1L) {
holder.binding.layoutDueDate.visibility = View.GONE
holder.binding.iconNotifyHeader.setImageResource(0)
} else {
val notificationDrawable = AppCompatResources.getDrawable(
context,
R.drawable.notifications,
)
holder.binding.layoutDueDate.visibility = View.VISIBLE
holder.binding.iconNotifyHeader.setImageDrawable(notificationDrawable)
holder.binding.iconNotifyDate.setImageDrawable(notificationDrawable)
// TODO let user pick date format
val notifyDate = FormattedDate(currentItem.notifyDate)
holder.binding.textNotifyDate.text = context.getString(if (notifyDate.date.isInThePast()) {
R.string.task_item_notification_active_past
} else {
R.string.task_item_notification_active_future
}, notifyDate.dateString, notifyDate.timeString)
}
// Set up single click listener
holder.binding.linearLayout.setOnClickListener {
onClickListener.onClick(currentItem)
// Prepare layout animation
TransitionManager.beginDelayedTransition(holder.binding.root.parent as ViewGroup)
// Set visibility of further items
holder.binding.layoutExpandedContainer.visibility = if (isExpanded) {
View.GONE
} else {
View.VISIBLE
}
// Set value of isExpanded depending on whether the view is expanded
isExpanded = !isExpanded
}
// Set up long press listener
@@ -89,14 +140,6 @@ class TaskOverviewAdapter(
*/
interface OnClickListener {
/**
* Executed when an item has been clicked.
*
* @param task item that has been clicked
*/
fun onClick(task: EventTask)
/**
* Executed when an item has been long-pressed.
*
@@ -17,9 +17,8 @@ import com.denizk0461.weserplaner.exception.ParcelNotFoundException
import com.denizk0461.weserplaner.values.TextSheetContentId
import com.denizk0461.weserplaner.sheet.TextSheet
import com.google.android.material.snackbar.Snackbar
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import kotlin.jvm.Throws
/*
@@ -39,6 +38,25 @@ fun String.parseToMinutes(): Int = try {
0
}
/**
* Retrieves the current date using a Calendar instance.
*
* @return date object containing the current time
*/
fun getCurrentTime(): Date = Calendar.getInstance().run {
Date((get(Calendar.MINUTE) + get(Calendar.HOUR_OF_DAY) * 60L))
}
/**
* Determines whether a given date lies in the past.
*
* @receiver date to check
* @param against date to check against, defaults to current date
* @return true if it is in the past, false if it is in the future or at this moment
*/
fun Date.isInThePast(against: Date = getCurrentTime()): Boolean =
this.time < against.time
/**
* Retrieves a specified colour customised to the currently applied theme.
*
@@ -151,19 +169,6 @@ fun SwipeRefreshLayout.setRainbowProgressCircle() {
)
}
/**
* Formats a date object to a string in the ISO 8601 format. Example:
* 2023-05-21, 14:32:59.067
*
* @receiver date object to convert to string
* @param useHighPrecision determines whether seconds and milliseconds are shown
* @return date formatted as ISO 8601 string
*/
fun Date.formatToIso8601String(useHighPrecision: Boolean = false): String = SimpleDateFormat(
"yyyy-MM-dd, HH:mm${if (useHighPrecision) ":ss.SSS" else ""}",
Locale.GERMANY,
).format(this)
/**
* Timeslots for which a conversion to an academic quarter is applicable.
*/
@@ -76,6 +76,7 @@ interface AppDAO {
"SELECT event_tasks.taskId, " +
"event_tasks.eventId, " +
"event_tasks.dueDate, " +
"event_tasks.notifyDate, " +
"event_tasks.title AS taskTitle, " +
"studip_events.title AS eventTitle, " +
"studip_events.lecturer, " +
@@ -96,6 +97,7 @@ interface AppDAO {
"SELECT event_tasks.taskId, " +
"event_tasks.eventId, " +
"event_tasks.dueDate, " +
"event_tasks.notifyDate, " +
"event_tasks.title AS taskTitle, " +
"studip_events.title AS eventTitle, " +
"studip_events.lecturer, " +
@@ -115,6 +117,7 @@ interface AppDAO {
"SELECT event_tasks.taskId, " +
"event_tasks.eventId, " +
"event_tasks.dueDate, " +
"event_tasks.notifyDate, " +
"event_tasks.title AS taskTitle, " +
"studip_events.title AS eventTitle, " +
"studip_events.lecturer, " +
@@ -16,11 +16,11 @@ import com.denizk0461.weserplaner.BuildConfig
import com.denizk0461.weserplaner.R
import com.denizk0461.weserplaner.activity.FetcherActivity
import com.denizk0461.weserplaner.activity.ImageActivity
import com.denizk0461.weserplaner.data.formatToIso8601String
import com.denizk0461.weserplaner.data.getTextSheet
import com.denizk0461.weserplaner.data.showSnackBar
import com.denizk0461.weserplaner.data.showToast
import com.denizk0461.weserplaner.databinding.FragmentSettingsBinding
import com.denizk0461.weserplaner.model.FormattedDate
import com.denizk0461.weserplaner.sheet.AllergenConfigSheet
import com.denizk0461.weserplaner.viewmodel.SettingsViewModel
import java.nio.charset.StandardCharsets
@@ -318,7 +318,7 @@ class SettingsFragment : AppFragment<FragmentSettingsBinding>() {
// Set build version code and time text
@SuppressLint("SetTextI18n")
binding.buildTimeText.text = "v${BuildConfig.VERSION_CODE} | ${
Date(BuildConfig.BUILD_TIME_MILLIS).formatToIso8601String(useHighPrecision = true)
FormattedDate(BuildConfig.BUILD_TIME_MILLIS).commaSeparatedString(context)
}"
// Set up switch for showing beta screens
@@ -91,10 +91,6 @@ class TaskOverviewFragment : AppFragment<FragmentTaskOverviewBinding>(),
}
}
override fun onClick(task: EventTask) {
// TODO
}
override fun onLongClick(task: EventTask): Boolean {
// TODO
return false
@@ -10,6 +10,8 @@ import androidx.room.PrimaryKey
* @param taskId primary key for the entity
* @param eventId foreign key referencing the event this task is assigned to
* @param dueDate time and date at which the task is due
* @param notifyDate time and date at which the user wants to be notified about this task; -1 if
* the user doesn't want to be notified
* @param title title of the task
* @param notes user-added notes for the task
* @param room room the task may take place in
@@ -28,7 +30,8 @@ import androidx.room.PrimaryKey
data class EventTask(
@PrimaryKey(autoGenerate = true) val taskId: Int = 0,
val eventId: Int,
val dueDate: Int,
val dueDate: Long,
val notifyDate: Long,
val title: String,
val notes: String,
val room: String,
@@ -7,6 +7,8 @@ package com.denizk0461.weserplaner.model
* @param taskId primary key for the entity
* @param eventId foreign key referencing the event this task is assigned to
* @param dueDate time and date at which the task is due
* @param notifyDate time and date at which the user wants to be notified about this task; -1 if
* the user doesn't want to be notified
* @param eventTitle title of the event
* @param taskTitle title of the task
* @param lecturer lecturer(s) organising the event
@@ -17,6 +19,7 @@ data class EventTaskExtended(
val taskId: Int,
val eventId: Int,
val dueDate: Int,
val notifyDate: Int,
val eventTitle: String,
val taskTitle: String,
val lecturer: String,
@@ -0,0 +1,49 @@
package com.denizk0461.weserplaner.model
import android.content.Context
import com.denizk0461.weserplaner.R
import com.denizk0461.weserplaner.values.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Wrapper for Java's date object that exposes pre-formatted date and time strings.
*
* @param date date object to wrap
* @param format format the date should be presented in; see [com.denizk0461.weserplaner.values.DateFormat]
*/
class FormattedDate(val date: Date, format: DateFormat = DateFormat.ISO8601) {
constructor(time: Long, format: DateFormat = DateFormat.ISO8601) : this(Date(time), format)
var dateString: String = SimpleDateFormat(format.dateFormat, Locale.GERMANY).format(date)
var timeString: String = SimpleDateFormat(format.timeFormat, Locale.GERMANY).format(date)
/**
* Produces a string containing date and time of the object, separated by a comma. Example:
* 24.05.2023, 8:45
*
* @param context used to retrieve the string resource
* @return formatted string
*/
fun commaSeparatedString(context: Context): String = context.getString(
R.string.formatted_date_string_comma,
dateString,
timeString,
)
/**
* Produces a string containing date and time of the object, localised to the user's locale. Example:
* 24.05.2023 at 8:45 (English)
* 24.05.2023 um 8:45 (German)
*
* @param context used to retrieve the string resource
* @return formatted string
*/
fun localisedString(context: Context): String = context.getString(
R.string.formatted_date_string_localised,
dateString,
timeString,
)
}
@@ -0,0 +1,12 @@
package com.denizk0461.weserplaner.values
/**
* Determines the format a date should be put into.
*/
enum class DateFormat(val dateFormat: String, val timeFormat: String) {
EUROPEAN("dd.MM.yyyy", "H:mm"), // 24.05.2023, 8:45
ISO8601("yyyy-MM-dd", "HH:mm"), // 2023-05-24, 08:45
ISO8601_PRECISE("yyyy-MM-dd", "HH:mm:ss.SSS"), // 2023-05-24, 08:45:11.518
FUCKED_UP("MM/dd/yy", "h:mm a"), // 05/24/23, 8:45 AM
;
}
@@ -18,8 +18,8 @@ class TaskOverviewViewModel(application: Application) : AppViewModel(application
// fun getTasks(order: TaskOrder): LiveData<List<EventTask>> = repo.getTasks(order)
fun getTasks(order: TaskOrder) = listOf( // dummy data
EventTask(0, 0, 1684758756, "title1", "user notes", "GW2 B-idk"),
EventTask(1, 0, 1684798756, "title2", "user notes", "GW3 C-idk"),
EventTask(2, 0, 1689758756, "title3", "user notes", "GW4 D-idk"),
EventTask(0, 0, 1684758756L, -1L, "title1", "user notes", "GW2 B-idk"),
EventTask(1, 0, 1684798756L, -1L, "title2", "user notes", "GW3 C-idk"),
EventTask(2, 0, 1689758756L, 1689958756L, "title3", "user notes", "GW4 D-idk"),
)
}
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,18.69L7.84,6.14 5.27,3.49 4,4.76l2.8,2.8v0.01c-0.52,0.99 -0.8,2.16 -0.8,3.42v5l-2,2v1h13.73l2,2L21,19.72l-1,-1.03zM12,22c1.11,0 2,-0.89 2,-2h-4c0,1.11 0.89,2 2,2zM18,14.68L18,11c0,-3.08 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68c-0.15,0.03 -0.29,0.08 -0.42,0.12 -0.1,0.03 -0.2,0.07 -0.3,0.11h-0.01c-0.01,0 -0.01,0 -0.02,0.01 -0.23,0.09 -0.46,0.2 -0.68,0.31 0,0 -0.01,0 -0.01,0.01L18,14.68z"/>
</vector>
+72 -2
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="8dp"
@@ -17,12 +18,81 @@
android:id="@+id/linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:clickable="true"
android:focusable="true"
android:padding="16dp">
android:padding="16dp"
android:background="@drawable/selectable_item_background"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="22sp"
app:fontFamily="@font/lato_bolditalic"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/icon_notify_header"
android:layout_marginEnd="8dp"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon_notify_header"
android:layout_width="20dp"
android:layout_height="20dp"
android:scaleType="centerInside"
android:tint="?attr/colorText"
android:importantForAccessibility="no"
app:layout_constraintTop_toTopOf="@id/text_title"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/text_due_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/layout_expanded_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.divider.MaterialDivider
android:id="@+id/divider"
style="@style/DividerTheme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="4dp"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/layout_due_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon_notify_date"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="8dp"
android:scaleType="centerInside"
android:tint="?attr/colorText"
android:importantForAccessibility="no"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/text_notify_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
+6
View File
@@ -90,6 +90,12 @@
<string name="task_order_menu_date_created">Erstellungszeit</string>
<string name="task_order_menu_date_due">Abgabezeit</string>
<string name="formatted_date_string_localised">%s um %s</string>
<string name="task_item_notification_none">Du wirst nicht benachrichtigt.</string>
<string name="task_item_notification_active_future">Du wirst am %s um %s benachrichtigt.</string>
<string name="task_item_notification_active_past">Du wurdest am %s um %s benachrichtigt.</string>
<string name="task_overview_snack_unimplemented">Aufgaben sind noch nicht implementiert!</string>
<string name="task_overview_fab_add_task">Aufgabe hinzufügen</string>
+7
View File
@@ -91,6 +91,13 @@
<string name="task_order_menu_date_created">Date created</string>
<string name="task_order_menu_date_due">Date due</string>
<string name="formatted_date_string_comma" translatable="false">%s, %s</string>
<string name="formatted_date_string_localised">%s at %s</string>
<string name="task_item_notification_none">You won\'t be notified</string>
<string name="task_item_notification_active_future">You will be notified on %s at %s.</string>
<string name="task_item_notification_active_past">You were notified on %s at %s.</string>
<string name="task_overview_snack_unimplemented">Tasks are not yet implemented!</string>
<string name="task_overview_fab_add_task">Add task</string>