Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d752d63
feat: Implement custom class management functionality
hazuki-keatsu Mar 20, 2026
7d3cfee
fix: Repair notification service initialization error when app start …
hazuki-keatsu Mar 20, 2026
661adb6
feat: Add delete functionality to date selector component
hazuki-keatsu Mar 20, 2026
2ec619a
feat: Integrate CustomCourseData into CourseReminder
hazuki-keatsu Mar 20, 2026
d65964d
feat: add i18n for date picker and adjust the layout of class add window
hazuki-keatsu Mar 20, 2026
9d928bb
refactor: refactor the way of generating CourseID to avoid conflicts
hazuki-keatsu Apr 8, 2026
934c51a
fix: update the id generator to the new one
hazuki-keatsu Apr 17, 2026
f3f7739
fix: avoid that the start time is equal to the end and change snackBa…
hazuki-keatsu Apr 17, 2026
5bcd78c
Merge branch 'main' into feat/improveAddingCourse
hazuki-keatsu Apr 28, 2026
f1a5965
refactor: refactor custom class handling and remove user-defined clas…
hazuki-keatsu May 1, 2026
fb0cde9
Merge branch 'main' into feat/improveAddingCourse
hazuki-keatsu May 1, 2026
61c5cc6
fix: align Reminder logics and modify log content
hazuki-keatsu May 1, 2026
a18b7dc
refactor: style fine-tuning for DateSelectorFree
hazuki-keatsu May 1, 2026
4046bf7
Merge branch 'main' into feat/improveAddingCourse
hazuki-keatsu May 3, 2026
7c67974
refactor: extract file I/O into CustomClassRepository
hazuki-keatsu May 3, 2026
f324b0c
fix: clear residual code
hazuki-keatsu May 8, 2026
6975ecc
fix: align the icon of class_add_window
hazuki-keatsu May 9, 2026
61ba75a
feat: add more time ranges in custom_class_card
hazuki-keatsu May 9, 2026
cd4280f
Merge branch 'main' into feat/improveAddingCourse
hazuki-keatsu May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import java.time.LocalDateTime

object ClassTableConstants {
const val CLASS_FILE_NAME = "ClassTable.json"
const val USER_CLASS_FILE_NAME = "UserClass.json"
const val EXAM_FILE_NAME = "exam.json"
const val PHYSICS_EXPERIMENT_FILE_NAME = "PhysicsExperiment.json"
const val OTHER_EXPERIMENT_FILE_NAME = "OtherExperiment.json"
const val CUSTOM_CLASS_FILE_NAME = "CustomClassesV2.json"
Comment thread
hazuki-keatsu marked this conversation as resolved.

// In SharedPreferencesPlugin, SHARED_PREFERENCES_NAME is private.
// Be attention to the changes of SharedPreferencesPlugin.SHARED_PREFERENCES_NAME.
Expand Down Expand Up @@ -80,18 +80,6 @@ data class TimeLineItem(
}
}

@Serializable
data class UserDefinedClassData(
val userDefinedDetail: List<ClassDetail>,
val timeArrangement: List<TimeArrangement>,
) {
companion object {
val EMPTY = UserDefinedClassData(
emptyList(), emptyList()
)
}
}

@OptIn(ExperimentalSerializationApi::class)
@Serializable
@JsonIgnoreUnknownKeys
Expand All @@ -100,25 +88,24 @@ data class ClassTableData(
val semesterCode: String,
val termStartDay: String,
val classDetail: List<ClassDetail>,
val userDefinedDetail: List<ClassDetail>,
val timeArrangement: List<TimeArrangement>,
// ClassChanges has been omitted here since calculated in time main app.
// NotArrangedClassDetail has been omitted here since useless.
) {
companion object {
val EMPTY = ClassTableData(
0, "", "2024-01-01",
emptyList(), emptyList(), emptyList(),
emptyList(), emptyList(),
)
}

// Should never go wrong.
fun getClassName(arrangement: TimeArrangement): String = when (arrangement.source) {
Source.SCHOOL -> classDetail[arrangement.index].name
Source.USER -> userDefinedDetail[arrangement.index].name
Source.EXAM -> "Unknown Exam"
Source.EXPERIMENT -> "Unknown Experiment"
Source.EMPTY -> "Unknown Empty"
Source.USER -> "Unknown Custom Class"
Comment thread
hazuki-keatsu marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -212,3 +199,24 @@ val ExperimentData.timeRanges: List<Pair<LocalDateTime, LocalDateTime>>
}
}

@Serializable
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

记得 Kotlin 要想解析 Dart 的 Record 对象要这么搞?需要核实下面的代码。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这块的映射应该是没问题的,虽然我不太懂kotlin代码,但是这个映射应该是没问题的:p

data class CustomClassTimeRange(
val id: String,
@SerialName("start_time")
@Serializable(with = LocalDateTimeSerializer::class)
val startTime: LocalDateTime,
@SerialName("end_time")
@Serializable(with = LocalDateTimeSerializer::class)
val endTime: LocalDateTime,
)

@Serializable
data class CustomClass(
val id: String,
val name: String,
val teacher: String? = null,
val classroom: String? = null,
@SerialName("time_ranges")
val timeRanges: List<CustomClassTimeRange>,
)

Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ object ClassTableDataHolder {
var schoolClassJsonData: Result<String> = Result.success("")
@Synchronized set

@JvmStatic
var userDefinedClassJsonData: Result<String?> = Result.success(null)
@Synchronized set

@JvmStatic
var examJsonData: Result<String> = Result.success("")
@Synchronized set
Expand All @@ -36,6 +32,10 @@ object ClassTableDataHolder {
var otherExperimentJsonData: Result<String?> = Result.success(null)
@Synchronized set

@JvmStatic
var customClassJsonData: Result<String?> = Result.success(null)
@Synchronized set

@JvmStatic
var weekSwift: Long = 0
@Synchronized set
Expand All @@ -62,8 +62,8 @@ object ClassTableDataHolder {
"Exam JSON loaded, isSuccess: ${examJsonData.isSuccess}, " + "length: ${examJsonData.getOrNull()?.length ?: 0}"
)

userDefinedClassJsonData = loadFileContent(
context, ClassTableConstants.USER_CLASS_FILE_NAME
physicsExperimentJsonData = loadFileContent(
context, ClassTableConstants.PHYSICS_EXPERIMENT_FILE_NAME
).fold(onSuccess = { Result.success(it) }, onFailure = {
if (it is FileNotFoundException) {
Result.success(null)
Expand All @@ -73,11 +73,11 @@ object ClassTableDataHolder {
})
Log.i(
TAG,
"User Class JSON loaded, isSuccess: ${userDefinedClassJsonData.isSuccess} " + "length: ${userDefinedClassJsonData.getOrNull()?.length ?: 0}"
"Physics Experiment JSON loaded, isSuccess: ${physicsExperimentJsonData.isSuccess} " + "length: ${physicsExperimentJsonData.getOrNull()?.length ?: 0}"
)

physicsExperimentJsonData = loadFileContent(
context, ClassTableConstants.PHYSICS_EXPERIMENT_FILE_NAME
otherExperimentJsonData = loadFileContent(
context, ClassTableConstants.OTHER_EXPERIMENT_FILE_NAME
).fold(onSuccess = { Result.success(it) }, onFailure = {
if (it is FileNotFoundException) {
Result.success(null)
Expand All @@ -87,11 +87,11 @@ object ClassTableDataHolder {
})
Log.i(
TAG,
"Physics Experiment JSON loaded, isSuccess: ${physicsExperimentJsonData.isSuccess} " + "length: ${physicsExperimentJsonData.getOrNull()?.length ?: 0}"
"Other Experiment JSON loaded, isSuccess: ${otherExperimentJsonData.isSuccess} " + "length: ${otherExperimentJsonData.getOrNull()?.length ?: 0}"
)

otherExperimentJsonData = loadFileContent(
context, ClassTableConstants.OTHER_EXPERIMENT_FILE_NAME
customClassJsonData = loadFileContent(
Comment thread
hazuki-keatsu marked this conversation as resolved.
context, ClassTableConstants.CUSTOM_CLASS_FILE_NAME
).fold(onSuccess = { Result.success(it) }, onFailure = {
if (it is FileNotFoundException) {
Result.success(null)
Expand All @@ -101,7 +101,7 @@ object ClassTableDataHolder {
})
Log.i(
TAG,
"Other Experiment JSON loaded, isSuccess: ${otherExperimentJsonData.isSuccess} " + "length: ${otherExperimentJsonData.getOrNull()?.length ?: 0}"
"Custom Class JSON loaded, isSuccess: ${customClassJsonData.isSuccess} " + "length: ${customClassJsonData.getOrNull()?.length ?: 0}"
)

Log.i(TAG, "Finished loading data from files.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import io.github.benderblog.traintime_pda.R
import io.github.benderblog.traintime_pda.model.ClassTableConstants
import io.github.benderblog.traintime_pda.model.ClassTableData
import io.github.benderblog.traintime_pda.model.ClassTableWidgetLoadState
import io.github.benderblog.traintime_pda.model.CustomClass
import io.github.benderblog.traintime_pda.model.ExamData
import io.github.benderblog.traintime_pda.model.ExperimentData
import io.github.benderblog.traintime_pda.model.Source
import io.github.benderblog.traintime_pda.model.TimeLineItem
import io.github.benderblog.traintime_pda.model.UserDefinedClassData
import io.github.benderblog.traintime_pda.model.endTime
import io.github.benderblog.traintime_pda.model.startTime
import io.github.benderblog.traintime_pda.model.timeRanges
Expand All @@ -34,6 +34,7 @@ class ClassTableWidgetDataProvider {
private lateinit var classTableData: ClassTableData
private lateinit var examData: ExamData
private var experimentData: List<ExperimentData> = emptyList()
private var customClassData: List<CustomClass> = emptyList()

private var widgetState = ClassTableWidgetLoadState.LOADING
private var errorMessage: String? = null
Expand Down Expand Up @@ -74,6 +75,7 @@ class ClassTableWidgetDataProvider {
loadOneDayClass()
loadOneDayExam()
loadOneDayExperiment()
loadOneDayCustomClass()
timeLineItem.sortBy { it.startTime }
}
} catch (e: Exception) {
Expand Down Expand Up @@ -125,31 +127,6 @@ class ClassTableWidgetDataProvider {
"schoolClassTableData loaded, " + "semester code: ${schoolClassTableData.semesterCode}, " + "begin time: ${schoolClassTableData.termStartDay}, " + "semester length: ${schoolClassTableData.semesterLength}, " + "class detail length: ${schoolClassTableData.classDetail.size}, " + "time arrangement length: ${schoolClassTableData.timeArrangement.size}"
)

val userDefinedClassData = ClassTableDataHolder.userDefinedClassJsonData.getOrElse {
Log.e("$tag[loadBasicConfig]", "Failed to load userDefinedClassJsonData", it)
widgetState = ClassTableWidgetLoadState.ERROR_COURSE_USER_DEFINED
errorMessage = it.localizedMessage ?: it.message
?: context.getString(R.string.widget_classtable_unknown_error)
return
}?.takeIf {
Log.i("$tag[loadBasicConfig]", "userDefinedClassJsonData is not blank: ${it.isNotBlank()}")
it.isNotBlank()
}?.let {
try {
lenientJson.decodeFromString<UserDefinedClassData>(it)
} catch (e: Exception) {
Log.e("$tag[loadBasicConfig]", "Failed to parse userDefinedClassJsonData", e)
widgetState = ClassTableWidgetLoadState.ERROR_COURSE_USER_DEFINED
errorMessage = e.localizedMessage ?: e.message
?: context.getString(R.string.widget_classtable_unknown_error)
return
}
} ?: UserDefinedClassData.EMPTY
Log.i(
"$tag[loadBasicConfig]",
"userDefinedClassJsonData loaded, " + "userDefinedDetail length: ${userDefinedClassData.userDefinedDetail.size}, " + "time arrangement length: ${userDefinedClassData.timeArrangement.size}"
)

examData = ClassTableDataHolder.examJsonData.getOrElse {
Log.e("$tag[loadBasicConfig]", "Failed to load examJsonData", it)
widgetState = ClassTableWidgetLoadState.ERROR_EXAM
Expand Down Expand Up @@ -215,17 +192,35 @@ class ClassTableWidgetDataProvider {

experimentData = physicsExperimentData + otherExperimentData

// merge class table with user added class
classTableData = schoolClassTableData.copy(
userDefinedDetail = userDefinedClassData.userDefinedDetail,
timeArrangement = schoolClassTableData.timeArrangement + userDefinedClassData.timeArrangement,
)
Log.i("$tag[loadBasicConfig]", "Class table merged.")
customClassData = ClassTableDataHolder.customClassJsonData.getOrElse {
Log.e("$tag[loadBasicConfig]", "Failed to load customClassJsonData", it)
widgetState = ClassTableWidgetLoadState.ERROR_COURSE_USER_DEFINED
errorMessage = it.localizedMessage ?: it.message
?: context.getString(R.string.widget_classtable_unknown_error)
return
}?.takeIf {
Log.i("$tag[loadBasicConfig]", "customClassJsonData is not blank: ${it.isNotBlank()}")
it.isNotBlank()
}?.let {
try {
lenientJson.decodeFromString<List<CustomClass>>(it)
} catch (e: Exception) {
Log.e("$tag[loadBasicConfig]", "Failed to parse customClassJsonData", e)
widgetState = ClassTableWidgetLoadState.ERROR_COURSE_USER_DEFINED
errorMessage = e.localizedMessage ?: e.message
?: context.getString(R.string.widget_classtable_unknown_error)
return
}
} ?: emptyList()
Log.i("$tag[loadBasicConfig]", "customClassData loaded, data length: ${customClassData.size}")

classTableData = schoolClassTableData
Log.i("$tag[loadBasicConfig]", "Class table loaded.")

// calculate day index of today
val termStartDayStr = classTableData.termStartDay
if (termStartDayStr.isBlank()) {
if (classTableData == ClassTableData.EMPTY && userDefinedClassData == UserDefinedClassData.EMPTY) {
if (classTableData == ClassTableData.EMPTY) {
Comment thread
hazuki-keatsu marked this conversation as resolved.
Log.w("$tag[loadBasicConfig]", "Term start day is blank and no class data loaded.")
} else {
Log.e("$tag[loadBasicConfig]", "Term start day is blank, cannot calculate week/day index!")
Expand Down Expand Up @@ -396,4 +391,38 @@ class ClassTableWidgetDataProvider {
}
}
}
}

Comment thread
hazuki-keatsu marked this conversation as resolved.
private fun loadOneDayCustomClass() {
val curYear: Int = currentTime.year
val curMonth: Int = currentTime.monthValue
val curDay: Int = currentTime.dayOfMonth
Log.i(
"$tag[loadOneDayCustomClass]",
"curTime: $curYear-$curMonth-$curDay",
)
for ((index, cc) in customClassData.withIndex()) {
for (tr in cc.timeRanges) {
if (tr.startTime.year == curYear &&
tr.startTime.monthValue == curMonth &&
tr.startTime.dayOfMonth == curDay
) {
Log.i(
"$tag[loadOneDayCustomClass]",
"Adding custom class $cc at date ${tr.startTime}",
)
timeLineItem.add(
TimeLineItem(
type = Source.USER,
name = cc.name,
teacher = cc.teacher ?: "未知教师",
place = cc.classroom ?: "未安排教室",
startTime = tr.startTime,
endTime = tr.endTime,
colorIndex = index,
)
)
}
}
}
}
}
28 changes: 28 additions & 0 deletions assets/flutter_i18n/en_US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,21 @@ weekday:
saturday: "Sat."
sunday: "Sun."

# Months
month:
january: "Jan."
february: "Feb."
march: "Mar."
april: "Apr."
may: "May"
june: "Jun."
july: "Jul."
august: "Aug."
september: "Sept."
october: "Oct."
november: "Nov."
december: "Dec."

# 考勤查询
class_attendance:
title: "Attendance Query"
Expand Down Expand Up @@ -275,15 +290,28 @@ classtable:
input_start_time_hint: "Time start"
input_end_time_hint: "Time end"
wheel_choose_hint: "Period {index}"
choose_at_least_one: "Please choose at least one time for class"
repeat_weekly: "Repeat Weekly"
free_time: "Free Time"
date_selector_free:
rule: "Time must be between 8:30 and 21:25."
rule_2: "The end time must be later than the start time."
class_start_time: "Start time"
class_end_time: "End time"
edit_class_time: "Edit the class time"
choose_class_time: "Choose a class time"
course_detail_card:
class_number_string: "Class {number}"
unknown_teacher: "Unknown teacher"
unknown_place: "Unknown classroom"
class_period: "period {start} to {stop}"
edit: "Edit"
delete: "Delete"
delete_single: "Delete this one"
delete_all: "Delete all"
delete_title: "Are you sure to delete this class information?"
delete_content: "Everything will be excuted."
delete_content_single: "Only the information within this time range of the class will be removed."
output_to_system:
success: Successfully output to the system calendar.
failure: Problem occurred while outputing to the system calendar.
Expand Down
Loading