[Dialogs/GenerateBlockTimetable] Add new dialog.

This commit is contained in:
Kacper Ziubryniewicz 2020-01-06 00:11:03 +01:00
parent 31a293c5c0
commit f05b39736c
5 changed files with 426 additions and 17 deletions

View File

@ -0,0 +1,355 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-1-5
*/
package pl.szczodrzynski.edziennik.ui.dialogs.timetable
import android.content.Intent
import android.graphics.*
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.util.Log
import android.view.View.MeasureSpec
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.FileProvider
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.data.db.modules.timetable.LessonFull
import pl.szczodrzynski.edziennik.databinding.DialogGenerateBlockTimetableBinding
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
import java.io.File
import java.io.FileOutputStream
import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt
class GenerateBlockTimetableDialog(
val activity: AppCompatActivity,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
const val TAG = "GenerateBlockTimetableDialog"
private const val WIDTH_CONSTANT = 70
private const val WIDTH_WEEKDAY = 285
private const val WIDTH_SPACING = 15
private const val HEIGHT_CONSTANT = 60
private const val HEIGHT_MINUTE = 3
private const val HEIGHT_FOOTER = 40
}
private val heightProfileName by lazy { if (showProfileName) 100 else 0 }
private val app by lazy { activity.application as App }
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private lateinit var dialog: AlertDialog
private lateinit var b: DialogGenerateBlockTimetableBinding
private var showProfileName: Boolean = false
private var noColors: Boolean = false
init { run {
if (activity.isFinishing)
return@run
job = Job()
onShowListener?.invoke(TAG)
val weekCurrentStart = Week.getWeekStart()
val weekCurrentEnd = Week.getWeekEnd()
val weekNextStart = weekCurrentEnd.clone().stepForward(0, 0, 1)
val weekNextEnd = weekNextStart.clone().stepForward(0, 0, 6)
b = DialogGenerateBlockTimetableBinding.inflate(activity.layoutInflater)
b.showProfileNameItem.onClick { b.showProfileNameCheckbox.trigger() }
b.showProfileNameCheckbox.setOnCheckedChangeListener { _, isChecked -> showProfileName = isChecked }
b.noColorsItem.onClick { b.noColorsCheckbox.trigger() }
b.noColorsCheckbox.setOnCheckedChangeListener { _, isChecked -> noColors = isChecked }
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.timetable_generate_range)
.setItems(arrayOf(
activity.getString(R.string.timetable_generate_current_week_format, weekCurrentStart.formattedStringShort, weekCurrentEnd.formattedStringShort)
.asColoredSpannable(android.R.attr.textColorPrimary.resolveAttr(activity)),
activity.getString(R.string.timetable_generate_next_week_format, weekNextStart.formattedStringShort, weekNextEnd.formattedStringShort)
.asColoredSpannable(android.R.attr.textColorPrimary.resolveAttr(activity)),
activity.getString(R.string.timetable_generate_selected_week)
.asColoredSpannable(android.R.attr.textColorPrimary.resolveAttr(activity))
)) { dialog, which ->
dialog.dismiss()
when (which) {
0 -> generateBlockTimetable(weekCurrentStart, weekCurrentEnd)
1 -> generateBlockTimetable(weekNextStart, weekNextEnd)
2 -> selectDate()
}
}
.setView(b.root)
.setNeutralButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
.setOnDismissListener { onDismissListener?.invoke(TAG) }
.show()
}}
private fun selectDate() {
MaterialDatePicker.Builder
.datePicker()
.setSelection(Date.getToday().inMillis)
.build()
.apply {
addOnPositiveButtonClickListener { dateInMillis ->
dismiss()
val selectedDate = Date.fromMillis(dateInMillis)
generateBlockTimetable(selectedDate.weekStart, selectedDate.weekEnd)
}
}
.show(activity.supportFragmentManager, "MaterialDatePicker")
}
private fun generateBlockTimetable(weekStart: Date, weekEnd: Date) { launch {
val progressDialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.timetable_generate_progress_title)
.setMessage(R.string.timetable_generate_progress_text)
.show()
val weekDays = mutableListOf<MutableList<Lesson>>()
for (i in weekStart.weekDay..weekEnd.weekDay) {
weekDays.add(mutableListOf())
}
val allLessons = withContext(Dispatchers.Default) {
app.db.timetableDao().getBetweenDatesNow(weekStart, weekEnd)
}
val lessonRanges = mutableMapOf<Int, Int>()
var maxWeekDay = 5
var minTime: Time? = null
var maxTime: Time? = null
val lessons: List<LessonFull> = allLessons.mapNotNull { lesson ->
if (lesson.profileId != app.profile.id || lesson.type == Lesson.TYPE_NO_LESSONS
|| lesson.date == null || lesson.startTime == null || lesson.endTime == null)
return@mapNotNull null
if (lesson.date!!.weekDay > maxWeekDay)
maxWeekDay = lesson.date!!.weekDay
lessonRanges[lesson.startTime!!.value] = lesson.endTime!!.value
weekDays[lesson.date!!.weekDay].add(lesson)
if (minTime == null || lesson.startTime!! < minTime!!) {
minTime = lesson.startTime!!.clone()
}
if (maxTime == null || lesson.endTime!! > maxTime!!) {
maxTime = lesson.endTime!!.clone()
}
return@mapNotNull lesson
}
if (minTime == null) {
progressDialog.dismiss()
// TODO: Toast
return@launch
}
val diff = Time.diff(maxTime, minTime)
val imageWidth = WIDTH_CONSTANT + maxWeekDay * (WIDTH_WEEKDAY + WIDTH_SPACING) - WIDTH_SPACING
val imageHeight = heightProfileName + HEIGHT_CONSTANT + diff.inMinutes * HEIGHT_MINUTE + HEIGHT_FOOTER
val bitmap = Bitmap.createBitmap(imageWidth + 20, imageHeight + 30, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
if (noColors) canvas.drawARGB(255, 255, 255, 255)
else canvas.drawARGB(255, 225, 225, 225)
val paint = Paint().apply {
isAntiAlias = true
isFilterBitmap = true
isDither = true
}
lessons.forEach { lesson ->
val lessonLength = Time.diff(lesson.endTime, lesson.startTime)
val firstOffset = Time.diff(lesson.startTime, minTime)
val lessonWeekDay = lesson.date!!.weekDay
val left = WIDTH_CONSTANT + lessonWeekDay * (WIDTH_WEEKDAY + WIDTH_SPACING)
val top = heightProfileName + HEIGHT_CONSTANT + firstOffset.inMinutes * HEIGHT_MINUTE
val blockWidth = WIDTH_WEEKDAY
val blockHeight = lessonLength.inMinutes * HEIGHT_MINUTE
val viewWidth = 380.dp
val viewHeight = lessonLength.inMinutes * 4.dp
val layout = activity.layoutInflater.inflate(R.layout.row_timetable_block_item, null) as LinearLayout
val item: LinearLayout = layout.findViewById(R.id.timetableItemLayout)
val card: CardView = layout.findViewById(R.id.timetableItemCard)
val subjectName: TextView = layout.findViewById(R.id.timetableItemSubjectName)
val classroomName: TextView = layout.findViewById(R.id.timetableItemClassroomName)
val teacherName: TextView = layout.findViewById(R.id.timetableItemTeacherName)
val teamName: TextView = layout.findViewById(R.id.timetableItemTeamName)
if (noColors) {
card.setCardBackgroundColor(Color.WHITE)
card.cardElevation = 0f
item.setBackgroundResource(R.drawable.bg_rounded_16dp_outline)
subjectName.setTextColor(Color.BLACK)
classroomName.setTextColor(0xffaaaaaa.toInt())
teacherName.setTextColor(0xffaaaaaa.toInt())
teamName.setTextColor(0xffaaaaaa.toInt())
}
subjectName.text = lesson.subjectName ?: ""
classroomName.text = lesson.classroom ?: ""
teacherName.text = lesson.teacherName ?: ""
teamName.text = lesson.teamName ?: ""
when (lesson.type) {
Lesson.TYPE_NORMAL -> {}
Lesson.TYPE_CANCELLED, Lesson.TYPE_SHIFTED_SOURCE -> {
card.setCardBackgroundColor(Color.BLACK)
subjectName.setTextColor(Color.WHITE)
subjectName.text = lesson.subjectName?.asStrikethroughSpannable() ?: ""
}
else -> {
card.setCardBackgroundColor(0xff234158.toInt())
subjectName.setTextColor(Color.WHITE)
subjectName.setTypeface(null, Typeface.BOLD_ITALIC)
}
}
layout.isDrawingCacheEnabled = true
layout.measure(MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY))
layout.layout(0, 0, layout.measuredWidth, layout.measuredHeight)
layout.buildDrawingCache(true)
val itemBitmap = layout.drawingCache
canvas.drawBitmap(itemBitmap, null, Rect(left, top, left + blockWidth, top + blockHeight), paint)
}
val textPaint = Paint().apply {
setARGB(255, 0, 0, 0)
textAlign = Paint.Align.CENTER
textSize = 30f
isAntiAlias = true
isFilterBitmap = true
isDither = true
}
for (w in 0..maxWeekDay) {
val x = WIDTH_CONSTANT + w * WIDTH_WEEKDAY + w * WIDTH_SPACING
canvas.drawText(Week.getFullDayName(w), x + (WIDTH_WEEKDAY / 2f), heightProfileName + HEIGHT_CONSTANT / 2 + 10f, textPaint)
}
if (showProfileName) {
textPaint.textSize = 50f
canvas.drawText("${app.profile.name} - plan lekcji, ${weekStart.formattedStringShort} - ${weekEnd.formattedStringShort}", (imageWidth + 20) / 2f, 80f, textPaint)
}
textPaint.apply {
setARGB(128, 0, 0, 0)
textAlign = Paint.Align.RIGHT
textSize = 26f
typeface = Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
}
val footerTextPaintCenter = ((textPaint.descent() + textPaint.ascent()) / 2).roundToInt()
canvas.drawText("Wygenerowano w aplikacji Szkolny.eu", imageWidth - 10f, imageHeight - footerTextPaintCenter - 10f, textPaint)
textPaint.apply {
setARGB(255, 127, 127, 127)
textAlign = Paint.Align.CENTER
textSize = 16f
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
}
val textPaintCenter = ((textPaint.descent() + textPaint.ascent()) / 2).roundToInt()
val linePaint = Paint().apply {
setARGB(255, 100, 100, 100)
style = Paint.Style.STROKE
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
isAntiAlias = true
isFilterBitmap = true
isDither = true
}
val minTimeInt = ((minTime!!.value / 10000) * 60) + ((minTime!!.value / 100) % 100)
lessonRanges.forEach { (startTime, endTime) ->
listOf(startTime, endTime).forEach { value ->
val hour = value / 10000
val minute = (value / 100) % 100
val time = Time(hour, minute, 0)
val firstOffset = time.inMinutes - minTimeInt // offset in minutes
val top = (heightProfileName + HEIGHT_CONSTANT + firstOffset * HEIGHT_MINUTE).toFloat()
canvas.drawText(time.stringHM, WIDTH_CONSTANT / 2f, top - textPaintCenter, textPaint)
canvas.drawLine(WIDTH_CONSTANT.toFloat(), top, imageWidth.toFloat(), top, linePaint)
}
}
val today = Date.getToday().stringY_m_d
val now = Time.getNow().stringH_M_S
val outputDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu").apply { mkdirs() }
val outputFile = File(outputDir, "plan_lekcji_${app.profile.name}_${today}_${now}.png")
try {
val fos = FileOutputStream(outputFile)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.close()
} catch (e: Exception) {
Log.e("SAVE_IMAGE", e.message, e)
return@launch
}
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(activity, app.packageName + ".provider", outputFile)
} else {
Uri.parse("file://" + outputFile.absolutePath)
}
progressDialog.dismiss()
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.timetable_generate_success_title)
.setMessage(R.string.timetable_generate_success_text)
.setPositiveButton(R.string.share) { dialog, _ ->
dialog.dismiss()
val intent = Intent(Intent.ACTION_SEND)
intent.setDataAndType(null, "image/*")
intent.putExtra(Intent.EXTRA_STREAM, uri)
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.share_intent)))
}
.setNegativeButton(R.string.open) { dialog, _ ->
dialog.dismiss()
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, "image/*")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
activity.startActivity(intent)
}
.setNeutralButton(R.string.do_nothing) { dialog, _ -> dialog.dismiss() }
.show()
}}
}

View File

@ -26,6 +26,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding
import pl.szczodrzynski.edziennik.observeOnce
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.dialogs.timetable.GenerateBlockTimetableDialog
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
@ -196,7 +197,7 @@ class TimetableFragment : Fragment(), CoroutineScope {
.withIcon(Icon2.cmd_table_large)
.withOnClickListener(View.OnClickListener {
activity.bottomSheet.close()
//showBlockTimetableDialog()
GenerateBlockTimetableDialog(activity)
}),
BottomSheetSeparatorItem(true),
BottomSheetPrimaryItem(true)
@ -217,22 +218,6 @@ class TimetableFragment : Fragment(), CoroutineScope {
})
}}
/*private fun showBlockTimetableDialog() {
val weekCurrentStart = Week.getWeekStart()
val weekCurrentEnd = Week.getWeekEnd()
val weekNextStart = weekCurrentEnd.clone().stepForward(0, 0, 1)
val weekNextEnd = weekNextStart.clone().stepForward(0, 0, 6)
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.timetable_generate_range)
.setMultiChoiceItems(arrayOf(
getString(R.string.timetable_generate_current_week_format, weekCurrentStart.formattedStringShort, weekCurrentEnd.formattedStringShort),
getString(R.string.timetable_generate_next_week_format, weekNextStart.formattedStringShort, weekNextEnd.formattedStringShort)
), BooleanArray(2)) { dialog, which, isChecked ->
}
}*/
private fun markLessonsAsSeen() = pageSelection?.let { date ->
app.db.timetableDao().getForDate(App.profileId, date).observeOnce(this@TimetableFragment, Observer { lessons ->
lessons.forEach { lesson ->

View File

@ -45,6 +45,10 @@ public class Date implements Comparable<Date> {
return clone().stepForward(0, 0, -getWeekDay());
}
public Date getWeekEnd() {
return clone().stepForward(0, 0, 6-getWeekDay());
}
public static Date fromYmd(String dateTime) {
return new Date(Integer.parseInt(dateTime.substring(0, 4)), Integer.parseInt(dateTime.substring(4, 6)), Integer.parseInt(dateTime.substring(6, 8)));
}

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) Kacper Ziubryniewicz 2020-1-5
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/showProfileNameItem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal">
<CheckBox
android:id="@+id/showProfileNameCheckbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/timetable_generate_show_profile_name" />
</LinearLayout>
<LinearLayout
android:id="@+id/noColorsItem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal">
<CheckBox
android:id="@+id/noColorsCheckbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/timetable_generate_no_colors" />
</LinearLayout>
</LinearLayout>
</layout>

View File

@ -1148,4 +1148,7 @@
<string name="messages_compose_recipient_exists">Ten odbiorca został już wybrany</string>
<string name="login_account_no_students">To konto nie ma żadnych uczniów</string>
<string name="login_account_no_students_text">Do tego konta nie ma przypisanego żadnego ucznia, dlatego zalogowanie nie jest możliwe.\n\nPrzypisz ucznia na stronie swojego e-dziennika lub zaloguj się kontem, które ma przypisanego ucznia.</string>
<string name="timetable_generate_show_profile_name">Pokaż nazwę profilu</string>
<string name="timetable_generate_no_colors">Do wydruku (czarno-białe)</string>
<string name="timetable_generate_selected_week">Na wybrany tydzień</string>
</resources>