Skip to content
28 changes: 28 additions & 0 deletions assets/flutter_i18n/en_US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,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 @@ -233,15 +248,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
28 changes: 28 additions & 0 deletions assets/flutter_i18n/zh_CN.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ weekday:
saturday: "周六"
sunday: "周日"

# 月份
month:
january: "一月"
february: "二月"
march: "三月"
april: "四月"
may: "五月"
june: "六月"
july: "七月"
august: "八月"
september: "九月"
october: "十月"
november: "十一月"
december: "十二月"

# 考勤查询
class_attendance:
title: "考勤查询"
Expand Down Expand Up @@ -224,6 +239,16 @@ classtable:
input_start_time_hint: "上课时间"
input_end_time_hint: "下课时间"
wheel_choose_hint: "第 {index} 节"
choose_at_least_one: "请至少选择一个上课日期和时间"
repeat_weekly: "按周重复"
free_time: "自定义日期"
date_selector_free:
rule: "时间必须在 08:30-21:25 之间"
rule_2: "下课时间必须晚于上课时间"
class_start_time: "上课时间"
class_end_time: "下课时间"
edit_class_time: "编辑课程时间"
choose_class_time: "选择课程时间"
course_detail_card:
class_number_string: "{number} 班"
unknown_teacher: "老师未定"
Expand All @@ -232,7 +257,10 @@ classtable:
edit: "编辑"
delete: "删除"
delete_title: "是否删除课程信息?"
delete_single: "删除本次"
delete_all: "删除全部"
delete_content: "所有关于这个课的信息都会被删除,课表上关于这门课的信息将不复存在!"
delete_content_single: "关于这个课的信息只有这个时间段都会被删除,其他的时间段会被保留。"
output_to_system:
success: "成功导出到系统日历"
failure: "导出到系统日历过程中发生了问题:P"
Expand Down
26 changes: 26 additions & 0 deletions assets/flutter_i18n/zh_TW.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ weekday:
friday: 週五
saturday: 週六
sunday: 週日
month:
january: 一月
february: 二月
march: 三月
april: 四月
may: 五月
june: 六月
july: 七月
august: 八月
september: 九月
october: 十月
november: 十一月
december: 十二月
class_attendance:
title: 考勤查詢
detail_title: 簽到信息 - {courseName}
Expand Down Expand Up @@ -186,6 +199,16 @@ classtable:
input_start_time_hint: 上課時間
input_end_time_hint: 下課時間
wheel_choose_hint: 第 {index} 節
choose_at_least_one: 請至少選擇一個上課日期和時間
repeat_weekly: 按周重複
free_time: 自定義日期
date_selector_free:
rule: 時間必須在 08:30-21:25 之間
rule_2: 下課時間必須晚於上課時間
class_start_time: 上課時間
class_end_time: 下課時間
edit_class_time: 編輯課程時間
choose_class_time: 選擇課程時間
course_detail_card:
class_number_string: '{number} 班'
unknown_teacher: 老師未定
Expand All @@ -194,7 +217,10 @@ classtable:
edit: 編輯
delete: 刪除
delete_title: 是否刪除課程信息?
delete_single: 刪除本次
delete_all: 刪除全部
delete_content: 所有關於這個課的信息都會被刪除,課表上關於這門課的信息將不復存在!
delete_content_single: 關於這個課的信息只有這個時間段都會被刪除,其他的時間段會被保留。
output_to_system:
success: 成功導出到系統日曆
failure: 導出到系統日曆過程中發生了問題:P
Expand Down
198 changes: 198 additions & 0 deletions lib/controller/custom_class_controller.dart
Comment thread
hazuki-keatsu marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Copyright 2026 Hazuki Keatsu.
// Copyright 2026 Traintime PDA authors.
// SPDX-License-Identifier: MPL-2.0

import 'dart:convert';
import 'dart:io';

import 'package:signals/signals.dart';
import 'package:watermeter/model/pda_service/custom_class.dart';
import 'package:watermeter/repository/network_session.dart';

enum CustomClassState { fetching, fetched, error, none }

class CustomClassOccurrence {
final CustomClass customClass;
final CustomClassTimeRange timeRange;

const CustomClassOccurrence({
required this.customClass,
required this.timeRange,
});
}

class CustomClassController {
static final CustomClassController i = CustomClassController._();

static const String _customClassFileName = 'CustomClassesV2.json';
static const String _customClassIdPrefix = 'cc';
static const String _timeRangeIdPrefix = 'tr';

late File customClassFile;

CustomClassController._() {
customClassFile = File('${supportPath.path}/$_customClassFileName');
_load();
}

int _idSequence = 0;

String _nextId(String prefix) {
final int now = DateTime.now().microsecondsSinceEpoch;
_idSequence = (_idSequence + 1) & 0xFFFFF;
return '$prefix-${now.toRadixString(36)}-${_idSequence.toRadixString(36)}';
}

String generateCustomClassId() => _nextId(_customClassIdPrefix);

String generateTimeRangeId() => _nextId(_timeRangeIdPrefix);

DateTime _dateOnly(DateTime value) =>
DateTime(value.year, value.month, value.day);

final errorSignal = signal<String?>(null);
final stateSignal = signal<CustomClassState>(CustomClassState.none);
final customClassesSignal = signal<List<CustomClass>>([]);

String? get error => errorSignal.value;

CustomClassState get state => stateSignal.value;

List<CustomClass> get customClasses => customClassesSignal.value;

void _load() {
stateSignal.value = CustomClassState.fetching;
errorSignal.value = null;
try {
if (!customClassFile.existsSync()) {
customClassFile.writeAsStringSync('[]');
}
final dynamic decoded = jsonDecode(customClassFile.readAsStringSync());
customClassesSignal.value = (decoded as List<dynamic>)
.map((e) => CustomClass.fromJson(e as Map<String, dynamic>))
.toList();
stateSignal.value = CustomClassState.fetched;
} catch (e) {
stateSignal.value = CustomClassState.error;
errorSignal.value = e.toString();
customClassesSignal.value = [];
}
}

bool _save(List<CustomClass> nextClasses) {
try {
customClassFile.writeAsStringSync(
jsonEncode(nextClasses.map((e) => e.toJson()).toList()),
);
customClassesSignal.value = nextClasses;
errorSignal.value = null;
stateSignal.value = CustomClassState.fetched;
return true;
} catch (e) {
stateSignal.value = CustomClassState.error;
errorSignal.value = 'Failed to save custom classes: $e';
return false;
}
}

int _indexOfCustomClassById(String customClassId) => customClasses.indexWhere(
(customClass) => customClass.id == customClassId,
);

/// 添加新的自定义课程
Future<void> addCustomClass(CustomClass customClass) async {
final List<CustomClass> updated = List<CustomClass>.from(customClasses)
..add(customClass);
_save(updated);
}

/// 编辑已有的自定义课程
Future<void> editCustomClassById(
String customClassId,
CustomClass customClass,
) async {
final int index = _indexOfCustomClassById(customClassId);
if (index < 0) return;
final List<CustomClass> updated = List<CustomClass>.from(customClasses);
updated[index] = customClass;
_save(updated);
}

/// 删除已有的自定义课程
Future<void> deleteCustomClassById(String customClassId) async {
final int index = _indexOfCustomClassById(customClassId);
if (index < 0) return;
final List<CustomClass> updated = List<CustomClass>.from(customClasses)
..removeAt(index);
_save(updated);
}

/// 从已有的自定义课程中一处某个时间段
Future<void> deleteCustomClassTimeRange({
required String customClassId,
required String timeRangeId,
}) async {
final int customIndex = _indexOfCustomClassById(customClassId);
if (customIndex < 0) return;

final CustomClass customClass = customClasses[customIndex];
final int timeRangeIndex = customClass.timeRanges.indexWhere(
(timeRange) => timeRange.id == timeRangeId,
);
if (timeRangeIndex < 0) return;

final List<CustomClassTimeRange> updatedRanges =
List<CustomClassTimeRange>.from(customClass.timeRanges)
..removeAt(timeRangeIndex);

if (updatedRanges.isEmpty) {
final List<CustomClass> updated = List<CustomClass>.from(customClasses)
..removeAt(customIndex);
_save(updated);
return;
}

final CustomClass updatedClass = CustomClass(
id: customClass.id,
name: customClass.name,
teacher: customClass.teacher,
classroom: customClass.classroom,
timeRanges: updatedRanges,
);
final List<CustomClass> updated = List<CustomClass>.from(customClasses);
updated[customIndex] = updatedClass;
_save(updated);
}

/// 通过周索引、日索引和学期开始日期来找到有日程的那天
List<CustomClassOccurrence> getOccurrenceOfDay({
required int weekIndex,
required int dayIndex,
required DateTime semesterStartDay,
}) {
final List<CustomClassOccurrence> occurrences = [];

for (final customClass in customClasses) {
for (final timeRange in customClass.timeRanges) {
final int diffDays = _dateOnly(
timeRange.startTime,
).difference(_dateOnly(semesterStartDay)).inDays;
if (diffDays < 0) continue;

final int targetWeek = diffDays ~/ 7;
final int targetDay = diffDays % 7 + 1;

if (targetWeek == weekIndex && targetDay == dayIndex) {
occurrences.add(
CustomClassOccurrence(
customClass: customClass,
timeRange: timeRange,
),
);
}
}
}

return occurrences;
}
}
27 changes: 15 additions & 12 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,20 @@ void main() async {
);

// Initialize notification services
try {
await NotificationServiceRegistrar().initializeAllServices();
if (Platform.isAndroid || Platform.isIOS) {
try {
await NotificationServiceRegistrar().initializeAllServices();

// Handle app launch from notification
WidgetsBinding.instance.addPostFrameCallback((_) async {
final services = NotificationServiceRegistrar().getAllServices();
await Future.wait(
services.map((service) => service.handleAppLaunchFromNotification()),
);
});
} catch (e) {
log.error('Failed to initialize notification services', e);
// Handle app launch from notification
WidgetsBinding.instance.addPostFrameCallback((_) async {
final services = NotificationServiceRegistrar().getAllServices();
await Future.wait(
services.map((service) => service.handleAppLaunchFromNotification()),
);
});
} catch (e) {
log.error('Failed to initialize notification services', e);
}
}
}

Expand Down Expand Up @@ -117,7 +119,8 @@ class _MyAppState extends State<MyApp> {

WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) return;
final screenWidth = PlatformDispatcher.instance.views.first.physicalSize.width /
final screenWidth =
PlatformDispatcher.instance.views.first.physicalSize.width /
PlatformDispatcher.instance.views.first.devicePixelRatio;
log.info("Screen width: $screenWidth.");
if (screenWidth < 480) {
Expand Down
Loading