From f05b39736c08d230d198a95600021d711be5eba4 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Mon, 6 Jan 2020 00:11:03 +0100 Subject: [PATCH] [Dialogs/GenerateBlockTimetable] Add new dialog. --- .../timetable/GenerateBlockTimetableDialog.kt | 355 ++++++++++++++++++ .../modules/timetable/v2/TimetableFragment.kt | 19 +- .../edziennik/utils/models/Date.java | 4 + .../dialog_generate_block_timetable.xml | 62 +++ app/src/main/res/values/strings.xml | 3 + 5 files changed, 426 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/GenerateBlockTimetableDialog.kt create mode 100644 app/src/main/res/layout/dialog_generate_block_timetable.xml diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/GenerateBlockTimetableDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/GenerateBlockTimetableDialog.kt new file mode 100644 index 00000000..978030b5 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/GenerateBlockTimetableDialog.kt @@ -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>() + for (i in weekStart.weekDay..weekEnd.weekDay) { + weekDays.add(mutableListOf()) + } + + val allLessons = withContext(Dispatchers.Default) { + app.db.timetableDao().getBetweenDatesNow(weekStart, weekEnd) + } + val lessonRanges = mutableMapOf() + + var maxWeekDay = 5 + var minTime: Time? = null + var maxTime: Time? = null + + val lessons: List = 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() + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetableFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetableFragment.kt index 37d9e77c..63e6614d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetableFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetableFragment.kt @@ -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 -> diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java index 4e332863..a6c38bcb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java @@ -45,6 +45,10 @@ public class Date implements Comparable { 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))); } diff --git a/app/src/main/res/layout/dialog_generate_block_timetable.xml b/app/src/main/res/layout/dialog_generate_block_timetable.xml new file mode 100644 index 00000000..3f97733b --- /dev/null +++ b/app/src/main/res/layout/dialog_generate_block_timetable.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ff039368..ef4fb77d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1148,4 +1148,7 @@ Ten odbiorca został już wybrany To konto nie ma żadnych uczniów 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. + Pokaż nazwę profilu + Do wydruku (czarno-białe) + Na wybrany tydzień