Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 41 additions & 0 deletions lib/models/forum_models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,30 @@ class ForumPost {
bool isLikedBy(String? userID) =>
userID != null && userID.isNotEmpty && likes.contains(userID);

ForumPost copyWithLikes(String userID, {required bool liked}) {
final newLikes = List<String>.from(likes);
if (liked) {
if (!newLikes.contains(userID)) newLikes.add(userID);
} else {
newLikes.remove(userID);
}
return ForumPost(
id: id,
post: post,
school: school,
title: title,
content: content,
anonymous: anonymous,
pin: pin,
tags: tags,
images: images,
likes: newLikes,
user: user,
created: created,
updated: updated,
);
}

factory ForumPost.fromJson(Map<String, dynamic> json) {
final user = json['user'];
return ForumPost(
Expand Down Expand Up @@ -145,6 +169,23 @@ class ForumMessage {
bool isLikedBy(String? userID) =>
userID != null && userID.isNotEmpty && likes.contains(userID);

ForumMessage copyWithLikes(String userID, {required bool liked}) {
final newLikes = List<String>.from(likes);
if (liked) {
if (!newLikes.contains(userID)) newLikes.add(userID);
} else {
newLikes.remove(userID);
}
return ForumMessage(
id: id,
content: content,
anonymous: anonymous,
user: user,
created: created,
likes: newLikes,
);
}

factory ForumMessage.fromJson(Map<String, dynamic> json) {
final user = json['user'];
return ForumMessage(
Expand Down
38 changes: 12 additions & 26 deletions lib/screens/attendance_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import 'package:shared_preferences/shared_preferences.dart';

import '../models/models.dart';
import '../services/api_service.dart';
import 'unsupported_screen.dart';
import '../widgets/async_content_builder.dart';
import '../widgets/empty_tile.dart';

class AttendanceScreen extends StatefulWidget {
const AttendanceScreen({super.key});
Expand Down Expand Up @@ -168,12 +169,14 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
}

Widget _buildBody() {
if (_isLoading) return const Center(child: CircularProgressIndicator());
if (_unsupported) {
return const UnsupportedScreen(title: '此功能不支援', message: '目前選擇的學校尚未支援此功能');
}
if (_error != null) {
return UnsupportedScreen(title: '載入失敗', message: _error!, onRetry: _loadData);
if (_isLoading || _unsupported || _error != null) {
return AsyncContentBuilder(
isLoading: _isLoading,
isUnsupported: _unsupported,
error: _error,
onRetry: _loadData,
child: const SizedBox.shrink(),
);
}

final stats = _filteredStatistics;
Expand Down Expand Up @@ -205,7 +208,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
Text('各科缺曠統計', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
if (_subjectAbsences.isEmpty)
const _EmptyTile(message: '無缺曠記錄')
const EmptyTile(message: '無缺曠記錄')
else
..._subjectAbsences
.where((e) => e.total > 0)
Expand All @@ -217,7 +220,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> {
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
if (grouped.isEmpty)
_EmptyTile(message: _searchText.isEmpty ? '無缺曠記錄' : '找不到符合的記錄')
EmptyTile(message: _searchText.isEmpty ? '無缺曠記錄' : '找不到符合的記錄')
else
...grouped.map((g) => _AbsenceDayRow(
date: g.date,
Expand Down Expand Up @@ -527,21 +530,4 @@ class _Badge extends StatelessWidget {
}
}

class _EmptyTile extends StatelessWidget {
final String message;
const _EmptyTile({required this.message});

@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(children: [
const Icon(Icons.info_outline, color: Colors.grey),
const SizedBox(width: 12),
Text(message),
]),
),
);
}
}
14 changes: 3 additions & 11 deletions lib/screens/curriculum_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../services/api_service.dart';
import '../services/cache_service.dart';
import '../services/notification_token_service.dart';
import '../services/school_config_manager.dart';
import '../widgets/timetable_cells.dart';
import 'unsupported_screen.dart';

class CurriculumScreen extends StatefulWidget {
Expand Down Expand Up @@ -556,8 +557,8 @@ class _CurriculumScreenState extends State<CurriculumScreen> {
children: [
// Header
TableRow(children: [
_headerCell('節次'),
..._weekdays.map((d) => _headerCell('週$d')),
const TimetableHeaderCell(text: '節次'),
..._weekdays.map((d) => TimetableHeaderCell(text: '週$d')),
]),
// Rows
...periods.map((period) => TableRow(children: [
Expand All @@ -582,15 +583,6 @@ class _CurriculumScreenState extends State<CurriculumScreen> {
);
}

Widget _headerCell(String text) {
return Container(
padding: const EdgeInsets.all(8),
color: Colors.grey[200],
alignment: Alignment.center,
child: Text(text, style: const TextStyle(fontSize: 12)),
);
}

Widget _periodCell(String period) {
final pt = _effectivePeriodTime(period);
final isManual = _manualPeriodTimes.containsKey(period);
Expand Down
67 changes: 12 additions & 55 deletions lib/screens/exam_score_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import 'package:provider/provider.dart';

import '../models/models.dart';
import '../services/api_service.dart';
import '../widgets/async_content_builder.dart';
import '../widgets/empty_tile.dart';
import '../widgets/score_label.dart';
import 'unsupported_screen.dart';

class ExamScoreScreen extends StatefulWidget {
Expand Down Expand Up @@ -66,20 +69,13 @@ class _ExamScoreScreenState extends State<ExamScoreScreen> {
}

Widget _buildBody() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_unsupported) {
return const UnsupportedScreen(
title: '此功能不支援',
message: '目前選擇的學校尚未支援此功能',
);
}
if (_error != null) {
return UnsupportedScreen(
title: '載入失敗',
message: _error!,
if (_isLoading || _unsupported || _error != null) {
return AsyncContentBuilder(
isLoading: _isLoading,
isUnsupported: _unsupported,
error: _error,
onRetry: _loadMenu,
child: const SizedBox.shrink(),
);
}
if (_menu.isEmpty) {
Expand Down Expand Up @@ -195,7 +191,7 @@ class _ExamScoreDetailScreenState extends State<ExamScoreDetailScreen> {
Text('科目成績', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
if (_data.subjects.isEmpty)
const _EmptyTile(message: '無成績資料')
const EmptyTile(message: '無成績資料')
else
..._data.subjects.map((s) => _ExamSubjectCard(score: s)),
],
Expand All @@ -222,8 +218,8 @@ class _ExamSubjectCard extends StatelessWidget {
const SizedBox(height: 6),
Row(
children: [
Expanded(child: _ScoreLabel(label: '個人', value: score.personalScore)),
Expanded(child: _ScoreLabel(label: '班均', value: score.classAverage)),
Expanded(child: ScoreLabel(label: '個人', value: score.personalScore)),
Expanded(child: ScoreLabel(label: '班均', value: score.classAverage)),
],
),
],
Expand All @@ -233,43 +229,4 @@ class _ExamSubjectCard extends StatelessWidget {
}
}

class _ScoreLabel extends StatelessWidget {
final String label;
final String value;

const _ScoreLabel({required this.label, required this.value});

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: Theme.of(context).textTheme.labelSmall),
const SizedBox(height: 2),
Text(value, style: Theme.of(context).textTheme.titleSmall),
],
);
}
}

class _EmptyTile extends StatelessWidget {
final String message;

const _EmptyTile({required this.message});

@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
const Icon(Icons.info_outline, color: Colors.grey),
const SizedBox(width: 12),
Text(message),
],
),
),
);
}
}
25 changes: 4 additions & 21 deletions lib/screens/following_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '../models/models.dart';
import '../services/api_service.dart';
import '../services/cache_service.dart';
import '../services/vocpass_auth_service.dart';
import '../widgets/timetable_cells.dart';
import 'unsupported_screen.dart';

// MARK: - 追蹤名單
Expand Down Expand Up @@ -257,13 +258,13 @@ class _SharedCurriculumScreenState extends State<SharedCurriculumScreen> {
final tableRows = <TableRow>[];

tableRows.add(TableRow(children: [
_headerCell('節次'),
..._weekdays.map((day) => _headerCell('週$day')),
const TimetableHeaderCell(text: '節次'),
..._weekdays.map((day) => TimetableHeaderCell(text: '週$day')),
]));

for (final period in periods) {
tableRows.add(TableRow(children: [
_periodCell(period),
TimetablePeriodCell(text: period),
..._weekdays.map((day) => _subjectCell(_subjectAt(day, period))),
]));
}
Expand All @@ -275,24 +276,6 @@ class _SharedCurriculumScreenState extends State<SharedCurriculumScreen> {
);
}

Widget _headerCell(String text) {
return Container(
padding: const EdgeInsets.all(8),
color: Colors.grey[200],
alignment: Alignment.center,
child: Text(text, style: const TextStyle(fontSize: 12)),
);
}

Widget _periodCell(String text) {
return Container(
padding: const EdgeInsets.all(8),
color: Colors.grey[100],
alignment: Alignment.center,
child: Text(text, style: const TextStyle(fontSize: 12)),
);
}

Widget _subjectCell(String subject) {
return Container(
padding: const EdgeInsets.all(6),
Expand Down
50 changes: 4 additions & 46 deletions lib/screens/forum/forum_post_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ class _ForumPostDetailScreenState extends State<ForumPostDetailScreen> {
final userID = _currentUserID;
if (userID == null) return;
final liked = _post.isLikedBy(userID);
setState(() => _post = _withPostLike(_post, userID, !liked));
setState(() => _post = _post.copyWithLikes(userID, liked: !liked));
try {
await ForumService.instance
.setPostLike(postID: _post.likeTargetID, liked: !liked);
} catch (e) {
if (!mounted) return;
setState(() => _post = _withPostLike(_post, userID, liked));
setState(() => _post = _post.copyWithLikes(userID, liked: liked));
_showError(e);
}
}
Expand All @@ -114,13 +114,13 @@ class _ForumPostDetailScreenState extends State<ForumPostDetailScreen> {
final liked = message.isLikedBy(userID);
final index = _messages.indexWhere((m) => m.id == message.id);
if (index < 0) return;
setState(() => _messages[index] = _withMessageLike(message, userID, !liked));
setState(() => _messages[index] = message.copyWithLikes(userID, liked: !liked));
try {
await ForumService.instance
.setMessageLike(messageID: message.id, liked: !liked);
} catch (e) {
if (!mounted) return;
setState(() => _messages[index] = _withMessageLike(message, userID, liked));
setState(() => _messages[index] = message.copyWithLikes(userID, liked: liked));
_showError(e);
}
}
Expand Down Expand Up @@ -527,48 +527,6 @@ class _ForumPostDetailScreenState extends State<ForumPostDetailScreen> {
);
}

// MARK: - 樂觀更新輔助

ForumPost _withPostLike(ForumPost post, String userID, bool liked) {
final likes = List<String>.from(post.likes);
if (liked) {
if (!likes.contains(userID)) likes.add(userID);
} else {
likes.remove(userID);
}
return ForumPost(
id: post.id,
post: post.post,
school: post.school,
title: post.title,
content: post.content,
anonymous: post.anonymous,
pin: post.pin,
tags: post.tags,
images: post.images,
likes: likes,
user: post.user,
created: post.created,
updated: post.updated,
);
}

ForumMessage _withMessageLike(ForumMessage message, String userID, bool liked) {
final likes = List<String>.from(message.likes);
if (liked) {
if (!likes.contains(userID)) likes.add(userID);
} else {
likes.remove(userID);
}
return ForumMessage(
id: message.id,
content: message.content,
anonymous: message.anonymous,
user: message.user,
created: message.created,
likes: likes,
);
}
}

class _LikeButton extends StatelessWidget {
Expand Down
Loading