Skip to content

Commit ed41890

Browse files
committed
fix: 修復多項 UI 和功能問題
- 修復全形半形不統一,以及課表文字超出範圍 - 修復選擇不同學期課表沒有刷新的問題 - 修復 i 學院公告附件下載 UI 未及時更新 - 修復 i 學院同步公告線程鎖未正常運作 - 修復課程公告附件 URL 不是最新的問題 - 調整同步公告間隔設定
1 parent f22b806 commit ed41890

File tree

5 files changed

+290
-104
lines changed

5 files changed

+290
-104
lines changed

lib/src/connectors/ischool_plus_connector.dart

Lines changed: 105 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ import '../models/ischool_plus/announcement.dart';
99
import '../models/ischool_plus/announcement_detail.dart';
1010
import '../models/ischool_plus/course_file.dart';
1111

12+
/// 鎖請求項目
13+
class _LockRequest {
14+
final Completer<void> completer;
15+
final bool highPriority;
16+
final String courseId; // 用於調試
17+
18+
_LockRequest({
19+
required this.completer,
20+
required this.highPriority,
21+
required this.courseId,
22+
});
23+
}
24+
1225
/// i學院連接器 - 參考 TAT 實作
1326
class ISchoolPlusConnector {
1427
static const String _baseUrl = 'https://istudy.ntut.edu.tw/';
@@ -17,8 +30,8 @@ class ISchoolPlusConnector {
1730
final Dio _dio;
1831

1932
// 用於防止並發請求導致課程選擇混亂的互斥鎖
20-
Completer<void>? _currentLock; // 當前正在執行的鎖
21-
final List<Completer<void>> _waitingQueue = []; // 等待隊列
33+
final List<_LockRequest> _lockQueue = []; // 鎖請求隊列(等待執行的請求)
34+
bool _isLocked = false; // 是否有請求正在執行(簡單的布爾標記)
2235

2336
ISchoolPlusConnector({required Dio dio}) : _dio = dio {
2437
// 不覆蓋傳入的 Dio 配置,保持共享的設置
@@ -201,48 +214,89 @@ class ISchoolPlusConnector {
201214
}
202215

203216
/// 獲取鎖(支持優先級)
204-
Future<void> _acquireLock({bool highPriority = false}) async {
205-
// 如果有當前正在執行的鎖,先等它完成
206-
if (_currentLock != null) {
207-
final completer = Completer<void>();
208-
209-
if (highPriority) {
210-
// 高優先級(手動操作)插入到等待隊列前面
211-
_waitingQueue.insert(0, completer);
212-
} else {
213-
// 低優先級(背景同步)加到等待隊列末尾
214-
_waitingQueue.add(completer);
217+
Future<void> _acquireLock(String courseId, {bool highPriority = false}) async {
218+
final completer = Completer<void>();
219+
final request = _LockRequest(
220+
completer: completer,
221+
highPriority: highPriority,
222+
courseId: courseId,
223+
);
224+
225+
final queueSize = _lockQueue.length;
226+
227+
// 將請求加入隊列
228+
if (highPriority && (_isLocked || _lockQueue.isNotEmpty)) {
229+
// 高優先級:插入到第一個低優先級請求之前
230+
int insertIndex = 0;
231+
while (insertIndex < _lockQueue.length && _lockQueue[insertIndex].highPriority) {
232+
insertIndex++;
215233
}
216-
217-
// 等待輪到自己
218-
await completer.future;
234+
_lockQueue.insert(insertIndex, request);
235+
print('[ISchoolPlus] 高優先級請求 $courseId 插隊到位置 $insertIndex (隊列: ${_lockQueue.length}, 鎖定: $_isLocked, 原隊列: $queueSize)');
236+
} else {
237+
// 低優先級:加到隊列末尾
238+
_lockQueue.add(request);
239+
print('[ISchoolPlus] 低優先級請求 $courseId 加入隊列 (隊列: ${_lockQueue.length}, 鎖定: $_isLocked, 原隊列: $queueSize)');
219240
}
241+
242+
// 嘗試處理隊列
243+
_processQueue();
244+
245+
// 等待輪到自己
246+
await completer.future;
220247

221-
// 創建自己的鎖
222-
_currentLock = Completer<void>();
248+
// completer 完成時,_isLocked 已經在 _processQueue 中設置為 true 了
249+
print('[ISchoolPlus] 請求 $courseId 獲得鎖並開始執行');
223250
}
224-
251+
252+
/// 處理鎖隊列
253+
void _processQueue() {
254+
// 如果正在處理或隊列為空,則不處理
255+
if (_isLocked || _lockQueue.isEmpty) {
256+
return;
257+
}
258+
259+
// 移除並獲取隊列中的第一個請求
260+
final first = _lockQueue.removeAt(0);
261+
262+
// 先設置鎖定狀態(在 complete 之前!)
263+
_isLocked = true;
264+
print('[ISchoolPlus] 準備處理請求 ${first.courseId} (隊列剩餘: ${_lockQueue.length})');
265+
266+
// 完成請求(讓它開始執行)
267+
first.completer.complete();
268+
}
269+
225270
/// 釋放鎖
226271
void _releaseLock() {
227-
// 完成當前鎖
228-
_currentLock?.complete();
229-
_currentLock = null;
272+
if (!_isLocked) {
273+
print('[ISchoolPlus] 警告:嘗試釋放鎖但當前未鎖定');
274+
return;
275+
}
276+
277+
print('[ISchoolPlus] 釋放鎖 (隊列剩餘: ${_lockQueue.length})');
230278

231-
// 處理等待隊列中的下一個請求
232-
if (_waitingQueue.isNotEmpty) {
233-
final next = _waitingQueue.removeAt(0);
234-
next.complete();
279+
// 先解鎖(在處理下一個請求之前)
280+
_isLocked = false;
281+
282+
// 處理下一個請求
283+
if (_lockQueue.isNotEmpty) {
284+
_processQueue();
235285
}
236286
}
237287

238288
/// 取得課程公告列表
239289
/// [highPriority] 是否為高優先級請求(手動操作)
240290
Future<List<ISchoolPlusAnnouncement>> getCourseAnnouncements(
241291
String courseId, {bool highPriority = false}) async {
292+
print('[ISchoolPlus] 請求公告列表: $courseId (高優先級: $highPriority)');
293+
242294
// 獲取鎖,高優先級請求會插隊
243-
await _acquireLock(highPriority: highPriority);
295+
await _acquireLock(courseId, highPriority: highPriority);
244296

245297
try {
298+
print('[ISchoolPlus] 開始處理公告列表: $courseId');
299+
246300
// 先選擇課程
247301
if (!await _selectCourse(courseId)) {
248302
throw Exception('無法選擇課程');
@@ -349,9 +403,24 @@ class ISchoolPlusConnector {
349403
}
350404

351405
/// 取得公告詳細內容
406+
/// [courseId] 課程 ID (6位數,必須提供)
407+
/// [highPriority] 是否為高優先級請求(手動操作)
352408
Future<ISchoolPlusAnnouncementDetail?> getAnnouncementDetail(
353-
ISchoolPlusAnnouncement announcement) async {
409+
ISchoolPlusAnnouncement announcement, {required String courseId, bool highPriority = false}) async {
410+
print('[ISchoolPlus] 請求公告詳情: $courseId (高優先級: $highPriority)');
411+
412+
// 獲取鎖,高優先級請求會插隊
413+
await _acquireLock(courseId, highPriority: highPriority);
414+
354415
try {
416+
print('[ISchoolPlus] 開始處理公告詳情: $courseId');
417+
418+
// 先選擇課程
419+
if (!await _selectCourse(courseId)) {
420+
print('[ISchoolPlus] 無法選擇課程: $courseId');
421+
return null;
422+
}
423+
355424
final data = {
356425
'token': announcement.token ?? '',
357426
'cid': announcement.cid ?? '',
@@ -404,16 +473,23 @@ class ISchoolPlusConnector {
404473
dev.log('[ISchoolPlus] Get announcement detail error: $e',
405474
stackTrace: stackTrace);
406475
return null;
476+
} finally {
477+
// 釋放鎖
478+
_releaseLock();
407479
}
408480
}
409481

410482
/// 取得課程檔案列表
411483
/// [highPriority] 是否為高優先級請求(手動操作)
412484
Future<List<ISchoolPlusCourseFile>> getCourseFiles(String courseId, {bool highPriority = false}) async {
485+
print('[ISchoolPlus] 請求檔案列表: $courseId (高優先級: $highPriority)');
486+
413487
// 獲取鎖,高優先級請求會插隊
414-
await _acquireLock(highPriority: highPriority);
488+
await _acquireLock(courseId, highPriority: highPriority);
415489

416490
try {
491+
print('[ISchoolPlus] 開始處理檔案列表: $courseId');
492+
417493
// 先選擇課程
418494
if (!await _selectCourse(courseId)) {
419495
return [];

lib/src/pages/course_table_page.dart

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:convert';
12
import 'package:flutter/material.dart';
23
import 'package:provider/provider.dart';
34
import 'package:hive_flutter/hive_flutter.dart';
@@ -604,7 +605,8 @@ class _CourseTablePageState extends State<CourseTablePage> {
604605
// 保存用戶的選擇
605606
_saveSelectedSemester(year, semester);
606607

607-
_loadCourseTable();
608+
// 強制刷新課表
609+
_loadCourseTable(forceRefresh: true);
608610
}
609611

610612
/// 保存用戶選擇的學期
@@ -638,6 +640,56 @@ class _CourseTablePageState extends State<CourseTablePage> {
638640
// 判斷是否為無課號課程(如班會課、導師時間等)
639641
final hasNoCourseId = courseId.isEmpty || courseId.startsWith('NO_ID_');
640642

643+
// 解析上課時間
644+
String timeInfo = '';
645+
try {
646+
final scheduleJson = course['schedule'] as String?;
647+
if (scheduleJson != null && scheduleJson.isNotEmpty) {
648+
final schedule = json.decode(scheduleJson) as Map<String, dynamic>;
649+
final List<String> timeParts = [];
650+
651+
// 節次時間對應表
652+
const Map<String, String> sectionTimeMap = {
653+
'1': '08:10-09:00',
654+
'2': '09:10-10:00',
655+
'3': '10:10-11:00',
656+
'4': '11:10-12:00',
657+
'N': '12:10-13:00',
658+
'5': '13:10-14:00',
659+
'6': '14:10-15:00',
660+
'7': '15:10-16:00',
661+
'8': '16:10-17:00',
662+
'9': '17:10-18:00',
663+
'A': '18:30-19:20',
664+
'B': '19:25-20:15',
665+
'C': '20:20-21:10',
666+
'D': '21:15-22:05',
667+
};
668+
669+
schedule.forEach((day, sections) {
670+
if (sections == null || sections.toString().trim().isEmpty) return;
671+
672+
final sectionList = sections.toString().trim().split(' ');
673+
if (sectionList.isEmpty) return;
674+
675+
// 取得第一節和最後一節的時間
676+
final firstSection = sectionList.first;
677+
final lastSection = sectionList.last;
678+
679+
final startTime = sectionTimeMap[firstSection]?.split('-')[0] ?? '';
680+
final endTime = sectionTimeMap[lastSection]?.split('-')[1] ?? '';
681+
682+
if (startTime.isNotEmpty && endTime.isNotEmpty) {
683+
timeParts.add('$day $startTime-$endTime');
684+
}
685+
});
686+
687+
timeInfo = timeParts.join('、');
688+
}
689+
} catch (e) {
690+
debugPrint('[CourseTable] 解析課程時間失敗: $e');
691+
}
692+
641693
showDialog(
642694
context: context,
643695
builder: (context) => AlertDialog(
@@ -648,12 +700,18 @@ class _CourseTablePageState extends State<CourseTablePage> {
648700
children: [
649701
// 只有有課號的課程才顯示課號
650702
if (!hasNoCourseId)
651-
Text('課號:$courseId'),
652-
Text('學分:${course['credits']} / 時數:${course['hours']}'),
703+
Text('課號$courseId'),
704+
Text('學分${course['credits']} / 時數${course['hours']}'),
653705
if (course['instructor']?.isNotEmpty == true)
654706
Text('教師:${course['instructor']}'),
655707
if (course['classroom']?.isNotEmpty == true)
656-
Text('教室:${course['classroom']}'),
708+
Text(
709+
'教室:${course['classroom']}',
710+
maxLines: 1,
711+
overflow: TextOverflow.ellipsis,
712+
),
713+
if (timeInfo.isNotEmpty)
714+
Text('時間:$timeInfo'),
657715
if (course['required']?.isNotEmpty == true)
658716
Text('修別:${course['required']}'),
659717
],

0 commit comments

Comments
 (0)