Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8284ad6
feat: add course table block and demo
Dao-you Feb 28, 2026
d7fa4da
fix: course block adjustment to prevent from overflow
Dao-you Mar 3, 2026
635575b
fix: use mock data in provider instead of standalong
Dao-you Mar 3, 2026
eb4afd5
feat: add a summary typedef
Dao-you Mar 3, 2026
49c6aac
feat: split hasWeekendCourse to sat and sun
Dao-you Mar 4, 2026
1ee3a70
feat: add classroom name per CourseTableBlockObject
Dao-you Mar 4, 2026
89216bb
fix: clean unused code
Dao-you Mar 4, 2026
5bbf5f6
feat: add hasNCourse in CourseTableSummaryObject
Dao-you Mar 5, 2026
05e801f
feat: update classroom display format
Dao-you Mar 3, 2026
461d91c
feat: adjust course block text style
Dao-you Mar 3, 2026
f5f5f02
feat: add mock data for recurring courses
Dao-you Mar 4, 2026
5cd628f
feat: add weekday labels
Dao-you Mar 4, 2026
9d93fe7
chore: keep DayOfWeek enum unformatted
Dao-you Mar 4, 2026
f366145
fix: update mock data to match data types
Dao-you Mar 5, 2026
089dae8
feat: add course grid background and header
Dao-you Mar 5, 2026
7cf6df1
fix: optimize app bar behavior
Dao-you Mar 5, 2026
79b08ef
feat: align course blocks to the grid
Dao-you Mar 5, 2026
524fabc
fix: prevent form overflow in course table
Dao-you Mar 5, 2026
e4f5b74
feat: expand mock course table scenarios
Dao-you Mar 5, 2026
3504d3e
fix: prevent text overflow in course table blocks
Dao-you Mar 5, 2026
05ae7b0
chore: simplify course table block object fields
Dao-you Mar 5, 2026
fd7140d
feat: refine course table block styling
Dao-you Mar 5, 2026
5a57070
feat: add course cell animations
Dao-you Mar 6, 2026
10f0566
feat: add course table block skeleton and optimize widget preview
Dao-you Mar 7, 2026
fcb1550
feat: make width dynamic based on weekday count
Dao-you Mar 7, 2026
63fbca5
feat: add fake loading effect and make course table grid stateful
Dao-you Mar 7, 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
2 changes: 1 addition & 1 deletion lib/components/widget_preview_frame.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class WidgetPreviewFrame extends StatelessWidget {
home: Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(4.0),
child: Center(child: child),
),
),
Expand Down
30 changes: 16 additions & 14 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,22 @@ Future<void> main() async {
ErrorType.unknown => t.errors.occurred,
};

showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(errorTitle),
// TODO: Remove technical details from user-facing error messages
content: Text(error.toString()),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(t.general.ok),
),
],
),
);
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(errorTitle),
// TODO: Remove technical details from user-facing error messages
content: Text(error.toString()),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(t.general.ok),
),
],
),
);
});
}

// Pass all uncaught "fatal" errors from the framework to Crashlytics
Expand Down
20 changes: 12 additions & 8 deletions lib/models/course.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
// dart format off
/// Day of the week for class schedules.
enum DayOfWeek {
sunday,
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
// TODO: turn weekday label to i18n string
sunday('日'),
monday('一'),
tuesday('二'),
wednesday('三'),
thursday('四'),
friday('五'),
saturday('六');

final String label;
const DayOfWeek(this.label);
}

// dart format off
/// Class period within a day, following NTUT's schedule structure.
///
/// NTUT uses periods 1-4, N (noon), 5-9, and A-D:
Expand Down
85 changes: 85 additions & 0 deletions lib/repositories/course_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,95 @@

import 'package:riverpod/riverpod.dart';
import 'package:tattoo/database/database.dart';
import 'package:tattoo/models/course.dart';
import 'package:tattoo/services/course_service.dart';
import 'package:tattoo/services/i_school_plus_service.dart';
import 'package:tattoo/services/portal_service.dart';

/// Temporary UI contract for one course entry in the course table.
///
/// Field names and types align with current database schema as much as possible
/// so repository implementation can migrate without API changes.
typedef CourseTableInfoObject = ({
/// [CourseOfferings.number].
String number,

/// [Courses.nameZh].
String? courseNameZh,

/// [Teachers.nameZh] of this offering.
///
/// A course offering can have multiple teachers.
List<String> teacherNamesZh,

/// [Courses.credits].
double credits,

/// [Courses.hours].
int hours,

/// [Classrooms.nameZh] of this offering.
///
/// A course offering can use multiple classrooms.
List<String> classroomNamesZh,

/// Raw schedule format from [Schedules] table.
///
/// Each entry is one `(dayOfWeek, period)` slot.
List<({DayOfWeek dayOfWeek, Period period})> schedule,

/// [Classes.nameZh] of this offering.
///
/// A course offering can target multiple classes.
List<String> classNamesZh,
});

/// Temporary UI contract for one renderable time block in the course table.
typedef CourseTableBlockObject = ({
/// [CourseOfferings.number].
String courseNumber,

/// [Courses.nameZh].
String? courseNameZh,

/// Classroom name for this block.
String classroomNameZh,

/// Weekday of this block.
DayOfWeek dayOfWeek,

/// Inclusive start slot of this block.
Period startSection,

/// Inclusive end slot of this block.
Period endSection,
});

typedef CourseTableSummaryObject = ({
// The semester this course table belongs to.
Semester semester,

/// Course blocks to render in the table.
List<CourseTableBlockObject> courses,

/// Whether the table has courses in the morning (before 12:00).
bool hasAmCourse,
bool hasNCourse,
bool hasPmCourse,
bool hasNightCourse,
Period earliestStartSection,
Period latestEndSection,
bool hasWeekdayCourse,
bool hasSatCourse,
bool hasSunCourse,

/// Total credits of all courses in the table.
double totalCredits,

/// Total hours of all courses in the table.
int totalHours,
});

/// Provides the [CourseRepository] instance.
final courseRepositoryProvider = Provider<CourseRepository>((ref) {
return CourseRepository(
Expand Down
175 changes: 175 additions & 0 deletions lib/screens/main/course_table/course_table_block.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import 'package:flutter/material.dart';
import 'package:flutter/widget_previews.dart';
import 'package:tattoo/components/app_skeleton.dart';
import 'package:tattoo/components/widget_preview_frame.dart';
import 'package:tattoo/models/course.dart';
import 'package:tattoo/repositories/course_repository.dart';
import 'package:auto_size_text/auto_size_text.dart';

class CourseTableBlock extends StatelessWidget {
final CourseTableBlockObject courseBlock;
final Color blockColor;

const CourseTableBlock({
required this.courseBlock,
required this.blockColor,
super.key,
});

@override
Widget build(BuildContext context) {
final containerColor = HSLColor.fromColor(
blockColor,
).withLightness(0.95).withSaturation(0.3).toColor();
final borderColor = HSLColor.fromColor(
blockColor,
).withLightness(0.3).withSaturation(0.8).toColor();
final borderStyle = Border.all(
color: borderColor,
width: 1,
);
final theme = Theme.of(context);

return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
child: Container(
decoration: BoxDecoration(
color: containerColor,
borderRadius: BorderRadius.circular(8),
border: borderStyle,
),
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2),
child: SizedBox(
height: 64,
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AutoSizeText(
courseBlock.courseNameZh ?? courseBlock.courseNumber,
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 12,
fontWeight: FontWeight.w700,
),
maxLines: 2,
minFontSize: 10,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
AutoSizeText(
courseBlock.classroomNameZh,
style: theme.textTheme.bodySmall?.copyWith(
fontSize: 8,
fontWeight: FontWeight.w400,
),
maxLines: 1,
minFontSize: 6,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}

class CourseTableBlockSkeleton extends StatelessWidget {
const CourseTableBlockSkeleton({super.key});

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final baseColor = colorScheme.surfaceContainerHighest;
final borderColor = colorScheme.outlineVariant;

return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: borderColor, width: 1),
),
clipBehavior: Clip.antiAlias,
child: Skeletonizer(
effect: PulseEffect(
from: baseColor,
to: borderColor,
duration: const Duration(milliseconds: 800),
),
child: Skeleton.leaf(
child: Container(color: baseColor),
),
),
);
}
}

@Preview(
name: 'Named Course',
group: 'Course Table',
size: Size(220, 150),
)
Widget courseTableBlockNamedPreview() {
return WidgetPreviewFrame(
child: Row(
spacing: 4,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 50,
height: 68,
child: CourseTableBlock(
courseBlock: _previewNamedCourseTableBlock,
blockColor: Colors.blue,
),
),
SizedBox(
width: 50,
height: 136,
child: CourseTableBlock(
courseBlock: _previewNamedCourseTableBlock,
blockColor: Colors.red,
),
),
],
),
);
}

@Preview(
name: 'CourseTableBlockSkeleton',
group: 'Course Table',
size: Size(220, 150),
)
Widget courseTableBlockSkeletonPreview() {
return WidgetPreviewFrame(
child: Row(
spacing: 4,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: 50,
height: 68,
child: CourseTableBlockSkeleton(),
),
const SizedBox(
width: 50,
height: 136,
child: CourseTableBlockSkeleton(),
),
],
),
);
}

final CourseTableBlockObject _previewNamedCourseTableBlock = (
courseNumber: 'CSIE3001',
courseNameZh: '微處理機及自動控制應用實務',
classroomNameZh: '六教305',
dayOfWeek: DayOfWeek.monday,
startSection: Period.third,
endSection: Period.fourth,
);
Loading
Loading