diff --git a/lib/app.dart b/lib/app.dart index da48d7a..f270c3f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -25,6 +25,7 @@ class Academia extends StatelessWidget { providers: [ BlocProvider(create: (_) => AuthCubit()), BlocProvider(create: (_) => ProfileCubit()), + BlocProvider(create: (_) => CourseCubit()), ], child: DynamicColorBuilder( builder: (lightscheme, darkscheme) => MaterialApp.router( diff --git a/lib/database/database.dart b/lib/database/database.dart index a0c858c..a63eed1 100644 --- a/lib/database/database.dart +++ b/lib/database/database.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:academia/features/auth/models/user.dart'; import 'package:academia/features/auth/models/user_credentials.dart'; import 'package:academia/features/auth/models/user_profile.dart'; +import 'package:academia/features/courses/models/course.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:drift_flutter/drift_flutter.dart'; @@ -43,7 +44,12 @@ Future _getDatabaseDirectory() async { } } -@DriftDatabase(tables: [User, UserProfile, UserCredential]) +@DriftDatabase(tables: [ + User, + UserProfile, + UserCredential, + Course, +]) class AppDatabase extends _$AppDatabase { // After generating code, this class needs to define a schemaVersion getter // and a constructor telling drift where the database should be stored. diff --git a/lib/database/database.g.dart b/lib/database/database.g.dart index c566a91..dd699cb 100644 --- a/lib/database/database.g.dart +++ b/lib/database/database.g.dart @@ -1496,18 +1496,524 @@ class UserCredentialCompanion extends UpdateCompanion { } } +class $CourseTable extends Course with TableInfo<$CourseTable, CourseData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CourseTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _unitMeta = const VerificationMeta('unit'); + @override + late final GeneratedColumn unit = GeneratedColumn( + 'unit', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _userMeta = const VerificationMeta('user'); + @override + late final GeneratedColumn user = GeneratedColumn( + 'user', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES user (id)')); + static const VerificationMeta _sectionMeta = + const VerificationMeta('section'); + @override + late final GeneratedColumn section = GeneratedColumn( + 'section', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _weekDayMeta = + const VerificationMeta('weekDay'); + @override + late final GeneratedColumn weekDay = GeneratedColumn( + 'week_day', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _campusMeta = const VerificationMeta('campus'); + @override + late final GeneratedColumn campus = GeneratedColumn( + 'campus', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _roomMeta = const VerificationMeta('room'); + @override + late final GeneratedColumn room = GeneratedColumn( + 'room', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _lecturerMeta = + const VerificationMeta('lecturer'); + @override + late final GeneratedColumn lecturer = GeneratedColumn( + 'lecturer', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _periodMeta = const VerificationMeta('period'); + @override + late final GeneratedColumn period = GeneratedColumn( + 'period', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _colorMeta = const VerificationMeta('color'); + @override + late final GeneratedColumn color = GeneratedColumn( + 'color', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: Constant(DateTime.now())); + @override + List get $columns => [ + unit, + user, + section, + weekDay, + campus, + room, + lecturer, + period, + color, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'course'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('unit')) { + context.handle( + _unitMeta, unit.isAcceptableOrUnknown(data['unit']!, _unitMeta)); + } else if (isInserting) { + context.missing(_unitMeta); + } + if (data.containsKey('user')) { + context.handle( + _userMeta, user.isAcceptableOrUnknown(data['user']!, _userMeta)); + } + if (data.containsKey('section')) { + context.handle(_sectionMeta, + section.isAcceptableOrUnknown(data['section']!, _sectionMeta)); + } else if (isInserting) { + context.missing(_sectionMeta); + } + if (data.containsKey('week_day')) { + context.handle(_weekDayMeta, + weekDay.isAcceptableOrUnknown(data['week_day']!, _weekDayMeta)); + } else if (isInserting) { + context.missing(_weekDayMeta); + } + if (data.containsKey('campus')) { + context.handle(_campusMeta, + campus.isAcceptableOrUnknown(data['campus']!, _campusMeta)); + } else if (isInserting) { + context.missing(_campusMeta); + } + if (data.containsKey('room')) { + context.handle( + _roomMeta, room.isAcceptableOrUnknown(data['room']!, _roomMeta)); + } else if (isInserting) { + context.missing(_roomMeta); + } + if (data.containsKey('lecturer')) { + context.handle(_lecturerMeta, + lecturer.isAcceptableOrUnknown(data['lecturer']!, _lecturerMeta)); + } else if (isInserting) { + context.missing(_lecturerMeta); + } + if (data.containsKey('period')) { + context.handle(_periodMeta, + period.isAcceptableOrUnknown(data['period']!, _periodMeta)); + } else if (isInserting) { + context.missing(_periodMeta); + } + if (data.containsKey('color')) { + context.handle( + _colorMeta, color.isAcceptableOrUnknown(data['color']!, _colorMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {unit}; + @override + CourseData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CourseData( + unit: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}unit'])!, + user: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user']), + section: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}section'])!, + weekDay: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}week_day'])!, + campus: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}campus'])!, + room: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}room'])!, + lecturer: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}lecturer'])!, + period: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}period'])!, + color: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}color']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at']), + ); + } + + @override + $CourseTable createAlias(String alias) { + return $CourseTable(attachedDatabase, alias); + } +} + +class CourseData extends DataClass implements Insertable { + final String unit; + final String? user; + final String section; + final String weekDay; + final String campus; + final String room; + final String lecturer; + final String period; + final int? color; + final DateTime? createdAt; + const CourseData( + {required this.unit, + this.user, + required this.section, + required this.weekDay, + required this.campus, + required this.room, + required this.lecturer, + required this.period, + this.color, + this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['unit'] = Variable(unit); + if (!nullToAbsent || user != null) { + map['user'] = Variable(user); + } + map['section'] = Variable(section); + map['week_day'] = Variable(weekDay); + map['campus'] = Variable(campus); + map['room'] = Variable(room); + map['lecturer'] = Variable(lecturer); + map['period'] = Variable(period); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + return map; + } + + CourseCompanion toCompanion(bool nullToAbsent) { + return CourseCompanion( + unit: Value(unit), + user: user == null && nullToAbsent ? const Value.absent() : Value(user), + section: Value(section), + weekDay: Value(weekDay), + campus: Value(campus), + room: Value(room), + lecturer: Value(lecturer), + period: Value(period), + color: + color == null && nullToAbsent ? const Value.absent() : Value(color), + createdAt: createdAt == null && nullToAbsent + ? const Value.absent() + : Value(createdAt), + ); + } + + factory CourseData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CourseData( + unit: serializer.fromJson(json['unit']), + user: serializer.fromJson(json['user']), + section: serializer.fromJson(json['section']), + weekDay: serializer.fromJson(json['day_of_the_week']), + campus: serializer.fromJson(json['campus']), + room: serializer.fromJson(json['room']), + lecturer: serializer.fromJson(json['lecturer']), + period: serializer.fromJson(json['period']), + color: serializer.fromJson(json['color']), + createdAt: serializer.fromJson(json['created_at']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'unit': serializer.toJson(unit), + 'user': serializer.toJson(user), + 'section': serializer.toJson(section), + 'day_of_the_week': serializer.toJson(weekDay), + 'campus': serializer.toJson(campus), + 'room': serializer.toJson(room), + 'lecturer': serializer.toJson(lecturer), + 'period': serializer.toJson(period), + 'color': serializer.toJson(color), + 'created_at': serializer.toJson(createdAt), + }; + } + + CourseData copyWith( + {String? unit, + Value user = const Value.absent(), + String? section, + String? weekDay, + String? campus, + String? room, + String? lecturer, + String? period, + Value color = const Value.absent(), + Value createdAt = const Value.absent()}) => + CourseData( + unit: unit ?? this.unit, + user: user.present ? user.value : this.user, + section: section ?? this.section, + weekDay: weekDay ?? this.weekDay, + campus: campus ?? this.campus, + room: room ?? this.room, + lecturer: lecturer ?? this.lecturer, + period: period ?? this.period, + color: color.present ? color.value : this.color, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + ); + CourseData copyWithCompanion(CourseCompanion data) { + return CourseData( + unit: data.unit.present ? data.unit.value : this.unit, + user: data.user.present ? data.user.value : this.user, + section: data.section.present ? data.section.value : this.section, + weekDay: data.weekDay.present ? data.weekDay.value : this.weekDay, + campus: data.campus.present ? data.campus.value : this.campus, + room: data.room.present ? data.room.value : this.room, + lecturer: data.lecturer.present ? data.lecturer.value : this.lecturer, + period: data.period.present ? data.period.value : this.period, + color: data.color.present ? data.color.value : this.color, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('CourseData(') + ..write('unit: $unit, ') + ..write('user: $user, ') + ..write('section: $section, ') + ..write('weekDay: $weekDay, ') + ..write('campus: $campus, ') + ..write('room: $room, ') + ..write('lecturer: $lecturer, ') + ..write('period: $period, ') + ..write('color: $color, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(unit, user, section, weekDay, campus, room, + lecturer, period, color, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CourseData && + other.unit == this.unit && + other.user == this.user && + other.section == this.section && + other.weekDay == this.weekDay && + other.campus == this.campus && + other.room == this.room && + other.lecturer == this.lecturer && + other.period == this.period && + other.color == this.color && + other.createdAt == this.createdAt); +} + +class CourseCompanion extends UpdateCompanion { + final Value unit; + final Value user; + final Value section; + final Value weekDay; + final Value campus; + final Value room; + final Value lecturer; + final Value period; + final Value color; + final Value createdAt; + final Value rowid; + const CourseCompanion({ + this.unit = const Value.absent(), + this.user = const Value.absent(), + this.section = const Value.absent(), + this.weekDay = const Value.absent(), + this.campus = const Value.absent(), + this.room = const Value.absent(), + this.lecturer = const Value.absent(), + this.period = const Value.absent(), + this.color = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + CourseCompanion.insert({ + required String unit, + this.user = const Value.absent(), + required String section, + required String weekDay, + required String campus, + required String room, + required String lecturer, + required String period, + this.color = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : unit = Value(unit), + section = Value(section), + weekDay = Value(weekDay), + campus = Value(campus), + room = Value(room), + lecturer = Value(lecturer), + period = Value(period); + static Insertable custom({ + Expression? unit, + Expression? user, + Expression? section, + Expression? weekDay, + Expression? campus, + Expression? room, + Expression? lecturer, + Expression? period, + Expression? color, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (unit != null) 'unit': unit, + if (user != null) 'user': user, + if (section != null) 'section': section, + if (weekDay != null) 'week_day': weekDay, + if (campus != null) 'campus': campus, + if (room != null) 'room': room, + if (lecturer != null) 'lecturer': lecturer, + if (period != null) 'period': period, + if (color != null) 'color': color, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + CourseCompanion copyWith( + {Value? unit, + Value? user, + Value? section, + Value? weekDay, + Value? campus, + Value? room, + Value? lecturer, + Value? period, + Value? color, + Value? createdAt, + Value? rowid}) { + return CourseCompanion( + unit: unit ?? this.unit, + user: user ?? this.user, + section: section ?? this.section, + weekDay: weekDay ?? this.weekDay, + campus: campus ?? this.campus, + room: room ?? this.room, + lecturer: lecturer ?? this.lecturer, + period: period ?? this.period, + color: color ?? this.color, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (unit.present) { + map['unit'] = Variable(unit.value); + } + if (user.present) { + map['user'] = Variable(user.value); + } + if (section.present) { + map['section'] = Variable(section.value); + } + if (weekDay.present) { + map['week_day'] = Variable(weekDay.value); + } + if (campus.present) { + map['campus'] = Variable(campus.value); + } + if (room.present) { + map['room'] = Variable(room.value); + } + if (lecturer.present) { + map['lecturer'] = Variable(lecturer.value); + } + if (period.present) { + map['period'] = Variable(period.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CourseCompanion(') + ..write('unit: $unit, ') + ..write('user: $user, ') + ..write('section: $section, ') + ..write('weekDay: $weekDay, ') + ..write('campus: $campus, ') + ..write('room: $room, ') + ..write('lecturer: $lecturer, ') + ..write('period: $period, ') + ..write('color: $color, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); $AppDatabaseManager get managers => $AppDatabaseManager(this); late final $UserTable user = $UserTable(this); late final $UserProfileTable userProfile = $UserProfileTable(this); late final $UserCredentialTable userCredential = $UserCredentialTable(this); + late final $CourseTable course = $CourseTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override List get allSchemaEntities => - [user, userProfile, userCredential]; + [user, userProfile, userCredential, course]; @override DriftDatabaseOptions get options => const DriftDatabaseOptions(storeDateTimeAsText: true); @@ -1559,6 +2065,20 @@ final class $$UserTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache)); } + + static MultiTypedResultKey<$CourseTable, List> _courseRefsTable( + _$AppDatabase db) => + MultiTypedResultKey.fromTable(db.course, + aliasName: $_aliasNameGenerator(db.user.id, db.course.user)); + + $$CourseTableProcessedTableManager get courseRefs { + final manager = $$CourseTableTableManager($_db, $_db.course) + .filter((f) => f.user.id($_item.id)); + + final cache = $_typedResult.readTableOrNull(_courseRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } } class $$UserTableFilterComposer extends Composer<_$AppDatabase, $UserTable> { @@ -1622,6 +2142,27 @@ class $$UserTableFilterComposer extends Composer<_$AppDatabase, $UserTable> { )); return f(composer); } + + Expression courseRefs( + Expression Function($$CourseTableFilterComposer f) f) { + final $$CourseTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.course, + getReferencedColumn: (t) => t.user, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$CourseTableFilterComposer( + $db: $db, + $table: $db.course, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$UserTableOrderingComposer extends Composer<_$AppDatabase, $UserTable> { @@ -1728,6 +2269,27 @@ class $$UserTableAnnotationComposer )); return f(composer); } + + Expression courseRefs( + Expression Function($$CourseTableAnnotationComposer a) f) { + final $$CourseTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.course, + getReferencedColumn: (t) => t.user, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$CourseTableAnnotationComposer( + $db: $db, + $table: $db.course, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$UserTableTableManager extends RootTableManager< @@ -1741,7 +2303,7 @@ class $$UserTableTableManager extends RootTableManager< $$UserTableUpdateCompanionBuilder, (UserData, $$UserTableReferences), UserData, - PrefetchHooks Function({bool userProfileRefs})> { + PrefetchHooks Function({bool userProfileRefs, bool courseRefs})> { $$UserTableTableManager(_$AppDatabase db, $UserTable table) : super(TableManagerState( db: db, @@ -1812,10 +2374,14 @@ class $$UserTableTableManager extends RootTableManager< .map((e) => (e.readTable(table), $$UserTableReferences(db, table, e))) .toList(), - prefetchHooksCallback: ({userProfileRefs = false}) { + prefetchHooksCallback: ( + {userProfileRefs = false, courseRefs = false}) { return PrefetchHooks( db: db, - explicitlyWatchedTables: [if (userProfileRefs) db.userProfile], + explicitlyWatchedTables: [ + if (userProfileRefs) db.userProfile, + if (courseRefs) db.course + ], addJoins: null, getPrefetchedDataCallback: (items) async { return [ @@ -1830,6 +2396,17 @@ class $$UserTableTableManager extends RootTableManager< referencedItemsForCurrentItem: (item, referencedItems) => referencedItems.where((e) => e.userId == item.id), + typedResults: items), + if (courseRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$UserTableReferences._courseRefsTable(db), + managerFromTypedResult: (p0) => + $$UserTableReferences(db, table, p0).courseRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => + referencedItems.where((e) => e.user == item.id), typedResults: items) ]; }, @@ -1849,7 +2426,7 @@ typedef $$UserTableProcessedTableManager = ProcessedTableManager< $$UserTableUpdateCompanionBuilder, (UserData, $$UserTableReferences), UserData, - PrefetchHooks Function({bool userProfileRefs})>; + PrefetchHooks Function({bool userProfileRefs, bool courseRefs})>; typedef $$UserProfileTableCreateCompanionBuilder = UserProfileCompanion Function({ required String userId, @@ -2657,6 +3234,348 @@ typedef $$UserCredentialTableProcessedTableManager = ProcessedTableManager< (UserCredentialData, $$UserCredentialTableReferences), UserCredentialData, PrefetchHooks Function({bool userId, bool username, bool email})>; +typedef $$CourseTableCreateCompanionBuilder = CourseCompanion Function({ + required String unit, + Value user, + required String section, + required String weekDay, + required String campus, + required String room, + required String lecturer, + required String period, + Value color, + Value createdAt, + Value rowid, +}); +typedef $$CourseTableUpdateCompanionBuilder = CourseCompanion Function({ + Value unit, + Value user, + Value section, + Value weekDay, + Value campus, + Value room, + Value lecturer, + Value period, + Value color, + Value createdAt, + Value rowid, +}); + +final class $$CourseTableReferences + extends BaseReferences<_$AppDatabase, $CourseTable, CourseData> { + $$CourseTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $UserTable _userTable(_$AppDatabase db) => + db.user.createAlias($_aliasNameGenerator(db.course.user, db.user.id)); + + $$UserTableProcessedTableManager? get user { + if ($_item.user == null) return null; + final manager = $$UserTableTableManager($_db, $_db.user) + .filter((f) => f.id($_item.user!)); + final item = $_typedResult.readTableOrNull(_userTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$CourseTableFilterComposer + extends Composer<_$AppDatabase, $CourseTable> { + $$CourseTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get unit => $composableBuilder( + column: $table.unit, builder: (column) => ColumnFilters(column)); + + ColumnFilters get section => $composableBuilder( + column: $table.section, builder: (column) => ColumnFilters(column)); + + ColumnFilters get weekDay => $composableBuilder( + column: $table.weekDay, builder: (column) => ColumnFilters(column)); + + ColumnFilters get campus => $composableBuilder( + column: $table.campus, builder: (column) => ColumnFilters(column)); + + ColumnFilters get room => $composableBuilder( + column: $table.room, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lecturer => $composableBuilder( + column: $table.lecturer, builder: (column) => ColumnFilters(column)); + + ColumnFilters get period => $composableBuilder( + column: $table.period, builder: (column) => ColumnFilters(column)); + + ColumnFilters get color => $composableBuilder( + column: $table.color, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$UserTableFilterComposer get user { + final $$UserTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.user, + referencedTable: $db.user, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$UserTableFilterComposer( + $db: $db, + $table: $db.user, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$CourseTableOrderingComposer + extends Composer<_$AppDatabase, $CourseTable> { + $$CourseTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get unit => $composableBuilder( + column: $table.unit, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get section => $composableBuilder( + column: $table.section, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get weekDay => $composableBuilder( + column: $table.weekDay, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get campus => $composableBuilder( + column: $table.campus, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get room => $composableBuilder( + column: $table.room, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lecturer => $composableBuilder( + column: $table.lecturer, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get period => $composableBuilder( + column: $table.period, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get color => $composableBuilder( + column: $table.color, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$UserTableOrderingComposer get user { + final $$UserTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.user, + referencedTable: $db.user, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$UserTableOrderingComposer( + $db: $db, + $table: $db.user, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$CourseTableAnnotationComposer + extends Composer<_$AppDatabase, $CourseTable> { + $$CourseTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get unit => + $composableBuilder(column: $table.unit, builder: (column) => column); + + GeneratedColumn get section => + $composableBuilder(column: $table.section, builder: (column) => column); + + GeneratedColumn get weekDay => + $composableBuilder(column: $table.weekDay, builder: (column) => column); + + GeneratedColumn get campus => + $composableBuilder(column: $table.campus, builder: (column) => column); + + GeneratedColumn get room => + $composableBuilder(column: $table.room, builder: (column) => column); + + GeneratedColumn get lecturer => + $composableBuilder(column: $table.lecturer, builder: (column) => column); + + GeneratedColumn get period => + $composableBuilder(column: $table.period, builder: (column) => column); + + GeneratedColumn get color => + $composableBuilder(column: $table.color, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$UserTableAnnotationComposer get user { + final $$UserTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.user, + referencedTable: $db.user, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$UserTableAnnotationComposer( + $db: $db, + $table: $db.user, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$CourseTableTableManager extends RootTableManager< + _$AppDatabase, + $CourseTable, + CourseData, + $$CourseTableFilterComposer, + $$CourseTableOrderingComposer, + $$CourseTableAnnotationComposer, + $$CourseTableCreateCompanionBuilder, + $$CourseTableUpdateCompanionBuilder, + (CourseData, $$CourseTableReferences), + CourseData, + PrefetchHooks Function({bool user})> { + $$CourseTableTableManager(_$AppDatabase db, $CourseTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$CourseTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$CourseTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$CourseTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value unit = const Value.absent(), + Value user = const Value.absent(), + Value section = const Value.absent(), + Value weekDay = const Value.absent(), + Value campus = const Value.absent(), + Value room = const Value.absent(), + Value lecturer = const Value.absent(), + Value period = const Value.absent(), + Value color = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + CourseCompanion( + unit: unit, + user: user, + section: section, + weekDay: weekDay, + campus: campus, + room: room, + lecturer: lecturer, + period: period, + color: color, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String unit, + Value user = const Value.absent(), + required String section, + required String weekDay, + required String campus, + required String room, + required String lecturer, + required String period, + Value color = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + CourseCompanion.insert( + unit: unit, + user: user, + section: section, + weekDay: weekDay, + campus: campus, + room: room, + lecturer: lecturer, + period: period, + color: color, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$CourseTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ({user = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (user) { + state = state.withJoin( + currentTable: table, + currentColumn: table.user, + referencedTable: $$CourseTableReferences._userTable(db), + referencedColumn: $$CourseTableReferences._userTable(db).id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$CourseTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $CourseTable, + CourseData, + $$CourseTableFilterComposer, + $$CourseTableOrderingComposer, + $$CourseTableAnnotationComposer, + $$CourseTableCreateCompanionBuilder, + $$CourseTableUpdateCompanionBuilder, + (CourseData, $$CourseTableReferences), + CourseData, + PrefetchHooks Function({bool user})>; class $AppDatabaseManager { final _$AppDatabase _db; @@ -2666,4 +3585,6 @@ class $AppDatabaseManager { $$UserProfileTableTableManager(_db, _db.userProfile); $$UserCredentialTableTableManager get userCredential => $$UserCredentialTableTableManager(_db, _db.userCredential); + $$CourseTableTableManager get course => + $$CourseTableTableManager(_db, _db.course); } diff --git a/lib/features/auth/repository/user_remote_repository.dart b/lib/features/auth/repository/user_remote_repository.dart index 6b072c3..59a6821 100644 --- a/lib/features/auth/repository/user_remote_repository.dart +++ b/lib/features/auth/repository/user_remote_repository.dart @@ -45,7 +45,6 @@ final class UserRemoteRepository with DioErrorHandler { } on DioException catch (de) { return handleDioError(de); } catch (e) { - rethrow; return left("Something went terribly wrong please try that later"); } } diff --git a/lib/features/auth/repository/user_repository.dart b/lib/features/auth/repository/user_repository.dart index df203b2..d974027 100644 --- a/lib/features/auth/repository/user_repository.dart +++ b/lib/features/auth/repository/user_repository.dart @@ -46,16 +46,17 @@ final class UserRepository { Future> authenticateRemotely( UserCredentialData credentials) async { // Register a magnet singleton instance - // GetIt.instance.registerSingletonIfAbsent( - // () => Magnet(credentials.admno, credentials.password), - // instanceName: "magnet", - // ); // TODO: (erick) enable auth with magnet + GetIt.instance.registerSingletonIfAbsent( + () => Magnet(credentials.admno, credentials.password), + instanceName: "magnet", + ); + // authenticate with magnet - const magnetResult = Right(Object()); - // await (GetIt.instance.get(instanceName: "magnet").login()); - // + final magnetResult = + await (GetIt.instance.get(instanceName: "magnet").login()); + // Right(Object()); return magnetResult.fold((error) { return left(error.toString()); }, (session) async { @@ -88,7 +89,9 @@ final class UserRepository { if (localResult.isRight()) { final profile = (localResult as Right).value; if (profile == null) { - return await refreshUserProfile(user); + return left( + "Failed to fetch your profile from cache please connect to the internet and refresh", + ); } return right((localResult as Right).value); } diff --git a/lib/features/auth/views/login_page.dart b/lib/features/auth/views/login_page.dart index 78eb3f8..1046520 100644 --- a/lib/features/auth/views/login_page.dart +++ b/lib/features/auth/views/login_page.dart @@ -1,6 +1,5 @@ import 'package:academia/database/database.dart'; -import 'package:academia/features/auth/cubit/auth_cubit.dart'; -import 'package:academia/features/auth/cubit/auth_states.dart'; +import 'package:academia/features/features.dart'; import 'package:academia/utils/router/router.dart'; import 'package:academia/utils/validator/validator.dart'; import 'package:flutter/material.dart'; @@ -179,6 +178,7 @@ class _LoginPageState extends State { ); }, (r) { HapticFeedback.heavyImpact(); + context.pop(); GoRouter.of(context).pushReplacementNamed( AcademiaRouter.home, ); diff --git a/lib/features/auth/views/user_selection_page.dart b/lib/features/auth/views/user_selection_page.dart index d8a0713..6c6866e 100644 --- a/lib/features/auth/views/user_selection_page.dart +++ b/lib/features/auth/views/user_selection_page.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:icons_plus/icons_plus.dart'; -import 'package:skeletonizer/skeletonizer.dart'; import 'package:sliver_tools/sliver_tools.dart'; class UserSelectionPage extends StatefulWidget { @@ -20,25 +19,6 @@ class UserSelectionPage extends StatefulWidget { class _UserSelectionPageState extends State { late AuthCubit authCubit = BlocProvider.of(context); - /// Shows a dialog with [title] and [content] - void _showMessageDialog(String title, String content) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(title), - content: Text(content), - actions: [ - TextButton( - onPressed: () { - context.pop(); - }, - child: const Text("Ok"), - ), - ], - ), - ); - } - @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/features/auth/views/widgets/user_selection_tile.dart b/lib/features/auth/views/widgets/user_selection_tile.dart index a46abda..a5f793d 100644 --- a/lib/features/auth/views/widgets/user_selection_tile.dart +++ b/lib/features/auth/views/widgets/user_selection_tile.dart @@ -1,6 +1,7 @@ import 'package:academia/database/database.dart'; import 'package:academia/features/auth/cubit/auth_cubit.dart'; import 'package:academia/features/auth/cubit/auth_states.dart'; +import 'package:academia/features/home/views/layout.dart'; import 'package:academia/utils/router/router.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; @@ -22,7 +23,7 @@ class UserSelectionTile extends StatefulWidget { } class _UserSelectionTileState extends State { - late AuthCubit _authCubit = BlocProvider.of(context); + late AuthCubit authCubit = BlocProvider.of(context); UserProfileData? profile; UserCredentialData? creds; @@ -46,11 +47,11 @@ class _UserSelectionTileState extends State { } Future _fetchUserProfile(UserData user) async { - return await _authCubit.fetchUserProfile(user); + return await authCubit.fetchUserProfile(user); } Future _fetchUserCredentials(UserData user) async { - final result = await _authCubit.fetchUserCredsFromCache(user); + final result = await authCubit.fetchUserCredsFromCache(user); return result.fold((l) { _showMessageDialog("Error", l); return null; @@ -73,15 +74,17 @@ class _UserSelectionTileState extends State { enabled: snapshot.connectionState != ConnectionState.done, child: ListTile( onTap: () async { - final result = await _authCubit.authenticate(creds!); + final result = await authCubit.authenticate(creds!); result.fold((l) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(l), )); }, (r) { if (!context.mounted) return; - GoRouter.of(context) - .pushReplacementNamed(AcademiaRouter.home); + context.pop(); + GoRouter.of(context).pushReplacementNamed( + AcademiaRouter.home, + ); }); }, title: Text("@${widget.user.username}"), diff --git a/lib/features/courses/courses.dart b/lib/features/courses/courses.dart new file mode 100644 index 0000000..08a758e --- /dev/null +++ b/lib/features/courses/courses.dart @@ -0,0 +1,3 @@ +export 'views/courses_page.dart'; +export 'cubit/course_cubit.dart'; +export 'cubit/course_state.dart'; diff --git a/lib/features/courses/cubit/course_cubit.dart b/lib/features/courses/cubit/course_cubit.dart new file mode 100644 index 0000000..5b83ed2 --- /dev/null +++ b/lib/features/courses/cubit/course_cubit.dart @@ -0,0 +1,29 @@ +import 'package:academia/database/database.dart'; +import 'package:academia/features/courses/cubit/course_state.dart'; +import 'package:academia/features/courses/repository/course_repository.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CourseCubit extends Cubit { + final CourseRepository _courseRepository = CourseRepository(); + CourseCubit() : super(CourseStateInitial()); + + Future syncCourses(UserData user) async { + emit(CourseStateLoading()); + final res = await _courseRepository.syncCoursesWithMagnet(user); + if (res.isLeft()) { + emit(CourseStateError(error: (res as Left).value)); + } + emit(CourseStateLoaded(courses: (res as Right).value)); + } + + Future fetchCachedCourses(UserData user) async { + emit(CourseStateLoading()); + final result = await _courseRepository.fetchAllCachedCourses(user); + result.fold((error) { + emit(CourseStateError(error: error)); + }, (courses) { + emit(CourseStateLoaded(courses: courses)); + }); + } +} diff --git a/lib/features/courses/cubit/course_state.dart b/lib/features/courses/cubit/course_state.dart new file mode 100644 index 0000000..9251005 --- /dev/null +++ b/lib/features/courses/cubit/course_state.dart @@ -0,0 +1,37 @@ +import 'package:academia/database/database.dart'; + +/// The base course state +/// note that the [busy] flag will be inherited by all +/// states to indicate that the state is busy and might +/// change +abstract class CourseState { + CourseState({this.busy = false}); + bool busy; +} + +/// The [CourseStateInitial] represents the first state +/// of courses +final class CourseStateInitial extends CourseState {} + +/// The [CourseStateLoading] indicates that the courses +/// are loading either from remote or cache and they're +/// bound to change +final class CourseStateLoading extends CourseState {} + +/// [CourseStateLoaded] indicates that the courses are loaded +/// The [courses] member contains a list of all courses +final class CourseStateLoaded extends CourseState { + final List courses; + CourseStateLoaded({required this.courses}); +} + +/// The [CourseStateError] represents the courses error state +/// It should be emitted if for example fetching was an error +/// or parsing the course data was an error +/// +/// The [error] member contains a [String] message of +/// what went wrong +final class CourseStateError extends CourseState { + final String error; + CourseStateError({required this.error}); +} diff --git a/lib/features/courses/models/course.dart b/lib/features/courses/models/course.dart new file mode 100644 index 0000000..91d9ac3 --- /dev/null +++ b/lib/features/courses/models/course.dart @@ -0,0 +1,21 @@ +import 'package:academia/features/auth/models/user.dart'; +import 'package:drift/drift.dart'; + +class Course extends Table { + TextColumn get unit => text()(); + TextColumn get user => text().references(User, #id).nullable()(); + TextColumn get section => text()(); + @JsonKey("day_of_the_week") + TextColumn get weekDay => text()(); + TextColumn get campus => text()(); + TextColumn get room => text()(); + TextColumn get lecturer => text()(); + TextColumn get period => text()(); + IntColumn get color => integer().nullable()(); + @JsonKey("created_at") + DateTimeColumn get createdAt => + dateTime().nullable().withDefault(Constant(DateTime.now()))(); + + @override + Set>? get primaryKey => {unit}; +} diff --git a/lib/features/courses/models/semester.dart b/lib/features/courses/models/semester.dart new file mode 100644 index 0000000..2e96da6 --- /dev/null +++ b/lib/features/courses/models/semester.dart @@ -0,0 +1,18 @@ +import 'package:drift/drift.dart'; + +class Semester extends Table { + TextColumn get id => text()(); + TextColumn get name => text()(); + TextColumn get description => text()(); + @JsonKey("begins_at") + DateTimeColumn get beginsAt => dateTime()(); + @JsonKey("ends_at") + DateTimeColumn get endsAt => dateTime()(); + @JsonKey("created_at") + DateTimeColumn get createdAt => dateTime()(); + @JsonKey("modified_at") + DateTimeColumn get modifiedAt => dateTime()(); + + @override + Set>? get primaryKey => {id}; +} diff --git a/lib/features/courses/repository/course_local_repository.dart b/lib/features/courses/repository/course_local_repository.dart new file mode 100644 index 0000000..7cd596a --- /dev/null +++ b/lib/features/courses/repository/course_local_repository.dart @@ -0,0 +1,73 @@ +import 'package:academia/database/database.dart'; +import 'package:dartz/dartz.dart'; +import 'package:drift/drift.dart'; +import 'package:get_it/get_it.dart'; + +/// [CourseLocalRepository] +/// A Helper class to manipulate course related information +/// on the device local storage +final class CourseLocalRepository { + // the db's instance + final AppDatabase _localDb = GetIt.instance.get(instanceName: "cacheDB"); + + /// Fetches all cached courses + /// Incase of an error a message of type [String] is returned + /// On success, a [List] of [CourseData] is returned + Future>> fetchAllCachedCourses( + UserData user) async { + try { + final users = await (_localDb.course.select() + ..orderBy([ + (c) => OrderingTerm( + expression: c.createdAt, + mode: OrderingMode.desc, + ), + ])) + .get(); + users.removeWhere((course) => course.user == user.id); + return right(users); + } catch (e) { + return left("Failed to retrieve users with message ${e.toString()}"); + } + } + + /// Adds a [CourseData] specified by [course] to [_localDb] cache + /// This method can also be used to update courses since it also updates the + /// information on conflict + Future> addCourseToCache(CourseData course) async { + try { + final ok = await _localDb.into(_localDb.course).insertOnConflictUpdate( + course.toCompanion(true), + ); + if (ok != 0) { + return right(true); + } + return left( + "The specified course data was not inserted since it exists and conflicted", + ); + } catch (e) { + return left( + "Failed to append course to cache with error description ${e.toString()}", + ); + } + } + + /// Delete the [CourseData] specified by [course] from local cache + /// It wil return an instance of [String] describing the error that it might have + /// encountered or a boolean [true] incase it was a success + Future> deleteCourseFromCache(CourseData course) async { + try { + final ok = await _localDb.delete(_localDb.course).delete(course); + if (ok != 0) { + return right(true); + } + return left( + "The specified course was not deleted because it does not exist", + ); + } catch (e) { + return left( + "Failed to delete course from cache with error description ${e.toString()}", + ); + } + } +} diff --git a/lib/features/courses/repository/course_remote_repository.dart b/lib/features/courses/repository/course_remote_repository.dart new file mode 100644 index 0000000..67d8379 --- /dev/null +++ b/lib/features/courses/repository/course_remote_repository.dart @@ -0,0 +1,17 @@ +import 'package:academia/database/database.dart'; +import 'package:dartz/dartz.dart'; +import 'package:get_it/get_it.dart'; +import 'package:magnet/magnet.dart'; + +final class CourseRemoteRepository { + /// Fetches courses from magnet. + Future>> fetchCoursesFromMagnet() async { + final magnetInstance = GetIt.instance.get(instanceName: "magnet"); + final magnetResult = await magnetInstance.fetchUserTimeTable(); + return magnetResult.fold((error) { + return left(error.toString()); + }, (courses) { + return right(courses.map((c) => CourseData.fromJson(c)).toList()); + }); + } +} diff --git a/lib/features/courses/repository/course_repository.dart b/lib/features/courses/repository/course_repository.dart new file mode 100644 index 0000000..f1eb9a1 --- /dev/null +++ b/lib/features/courses/repository/course_repository.dart @@ -0,0 +1,40 @@ +import 'package:academia/database/database.dart'; +import 'package:dartz/dartz.dart'; +import 'package:drift/drift.dart'; + +import 'course_local_repository.dart'; +import 'course_remote_repository.dart'; + +final class CourseRepository { + final CourseLocalRepository _localRepository = CourseLocalRepository(); + final CourseRemoteRepository _courseRemoteRepository = + CourseRemoteRepository(); + + /// Fetches all cached courses + /// Incase of an error a message of type [String] is returned + /// On success, a [List] of [CourseData] is returned + Future>> fetchAllCachedCourses( + UserData user) async { + return await _localRepository.fetchAllCachedCourses(user); + } + + Future>> syncCoursesWithMagnet( + UserData user) async { + final result = await _courseRemoteRepository.fetchCoursesFromMagnet(); + + return result.fold((error) { + return left(error); + }, (courses) async { + // Cache them to local db + for (final course in courses) { + final res = await _localRepository.addCourseToCache( + course.copyWith(user: Value(user.id)), + ); + if (res.isLeft()) { + return left((res as Left).value); + } + } + return right(courses); + }); + } +} diff --git a/lib/features/courses/views/courses_page.dart b/lib/features/courses/views/courses_page.dart new file mode 100644 index 0000000..29b9616 --- /dev/null +++ b/lib/features/courses/views/courses_page.dart @@ -0,0 +1,18 @@ +import 'package:academia/features/courses/views/courses_page_desktop.dart'; +import 'package:academia/features/courses/views/courses_page_mobile.dart'; +import 'package:academia/utils/utils.dart'; +import 'package:flutter/material.dart'; + +class CoursesPage extends StatelessWidget { + const CoursesPage({super.key}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) => + constraints.maxWidth < ScreenDimension.mobileWidth + ? const CoursesPageMobile() + : const CoursesPageDesktop(), + ); + } +} diff --git a/lib/features/courses/views/courses_page_desktop.dart b/lib/features/courses/views/courses_page_desktop.dart new file mode 100644 index 0000000..bb6c95f --- /dev/null +++ b/lib/features/courses/views/courses_page_desktop.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class CoursesPageDesktop extends StatelessWidget { + const CoursesPageDesktop({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Text("Course page desktop"), + ); + } +} diff --git a/lib/features/courses/views/courses_page_mobile.dart b/lib/features/courses/views/courses_page_mobile.dart new file mode 100644 index 0000000..e9f8ebd --- /dev/null +++ b/lib/features/courses/views/courses_page_mobile.dart @@ -0,0 +1,105 @@ +import 'package:academia/features/features.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +class CoursesPageMobile extends StatefulWidget { + const CoursesPageMobile({super.key}); + + @override + State createState() => _CoursesPageMobileState(); +} + +class _CoursesPageMobileState extends State { + late CourseCubit courseCubit = BlocProvider.of(context); + late AuthCubit authCubit = BlocProvider.of(context); + + @override + void initState() { + courseCubit.fetchCachedCourses( + (authCubit.state as AuthenticatedState).user, + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: RefreshIndicator( + onRefresh: () async { + await courseCubit.syncCourses( + (authCubit.state as AuthenticatedState).user, + ); + }, + child: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 250, + pinned: true, + floating: true, + snap: true, + flexibleSpace: FlexibleSpaceBar( + title: Text( + "Courses", + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontFamily: GoogleFonts.libreBaskerville().fontFamily, + ), + ), + ), + ), + BlocBuilder(builder: (context, state) { + if (state is CourseStateLoaded) { + if (state.courses.isNotEmpty) { + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 12), + sliver: SliverList.separated( + itemBuilder: (context, index) { + final course = state.courses[index]; + return ListTile( + leading: const CircleAvatar(), + title: Text("${course.unit} ${course.section}"), + subtitle: Text( + "${course.room} * ${course.period} * ${course.lecturer}", + ), + ); + }, + separatorBuilder: (context, index) => const SizedBox(), + itemCount: state.courses.length, + ), + ); + } + + return const SliverFillRemaining( + // TODO: erick add an illustration or animation + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("You have no courses yet, please pull to refresh"), + ], + ), + ); + } + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 12), + sliver: SliverList.separated( + itemCount: 8, + itemBuilder: (context, index) => const Skeletonizer( + enabled: true, + child: ListTile( + leading: CircleAvatar(), + title: Text("Some Couse"), + subtitle: Text("PLAB * 10:00 - 13:00 * Awesome Lecturer"), + ), + ), + separatorBuilder: (context, index) => const SizedBox(), + ), + ); + }), + ], + ), + ), + ); + } +} diff --git a/lib/features/essentials/essentials.dart b/lib/features/essentials/essentials.dart new file mode 100644 index 0000000..8c408fb --- /dev/null +++ b/lib/features/essentials/essentials.dart @@ -0,0 +1 @@ +export 'views/essentials_page.dart'; diff --git a/lib/features/essentials/views/essentials_mobile_page.dart b/lib/features/essentials/views/essentials_mobile_page.dart new file mode 100644 index 0000000..2bd8cb1 --- /dev/null +++ b/lib/features/essentials/views/essentials_mobile_page.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:icons_plus/icons_plus.dart'; +import 'package:sliver_tools/sliver_tools.dart'; + +class EssentialsMobilePage extends StatefulWidget { + const EssentialsMobilePage({super.key}); + + @override + State createState() => _EssentialsMobilePageState(); +} + +class _EssentialsMobilePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + snap: true, + floating: true, + expandedHeight: 250, + backgroundColor: Theme.of(context).colorScheme.errorContainer, + flexibleSpace: FlexibleSpaceBar( + title: Text( + "Essentials", + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontFamily: GoogleFonts.libreBaskerville().fontFamily, + ), + ), + ), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(Bootstrap.qr_code_scan), + ), + ], + ), + SliverPadding( + padding: const EdgeInsets.all(12), + sliver: MultiSliver( + children: const [ + Card( + elevation: 0, + margin: EdgeInsets.only(bottom: 2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12), + ), + ), + child: ListTile( + leading: Icon(Bootstrap.bell), + title: Text("Todos & Assigments"), + subtitle: Text("Keep track of your assignments and todos"), + ), + ), + Card( + elevation: 0, + margin: EdgeInsets.only(bottom: 2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.zero, + ), + ), + child: ListTile( + leading: Icon(Bootstrap.clock), + title: Text("Exam timetable"), + subtitle: Text("Psst.. Never miss an exam"), + ), + ), + Card( + elevation: 0, + margin: EdgeInsets.only(bottom: 2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.zero, + ), + ), + child: ListTile( + leading: Icon(Bootstrap.filetype_pdf), + title: Text("Past Revision Papers"), + subtitle: Text("You want them? You get them.."), + ), + ), + + Card( + elevation: 0, + margin: EdgeInsets.only(bottom: 2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + bottom: Radius.zero, + ), + ), + child: ListTile( + leading: Icon(Bootstrap.file_ppt), + title: Text("Ask Me"), + subtitle: Text("Boring notes? We'll help you revise"), + ), + ), + Card( + elevation: 0, + margin: EdgeInsets.only(bottom: 2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(12), + ), + ), + child: ListTile( + leading: Icon(Bootstrap.play), + title: Text("Flash Cards"), + subtitle: Text( + "Curious if you really understood? Try our flashcards", + ), + ), + ), + + //Page Break for student performance + SizedBox(height: 18), + Card( + margin: EdgeInsets.only(bottom: 2), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12), + ), + ), + child: ListTile( + leading: Icon(Bootstrap.bell), + title: Text("Student Audit"), + subtitle: Text("Keep track of your assignments and todos"), + ), + ), + Card( + elevation: 0, + margin: EdgeInsets.only(bottom: 2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + bottom: Radius.zero, + ), + ), + child: ListTile( + leading: Icon(Bootstrap.activity), + title: Text("GPA Calculator"), + subtitle: Text("Watch out for those grades!"), + ), + ), + + Card( + elevation: 0, + margin: EdgeInsets.only(bottom: 2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(12), + ), + ), + child: ListTile( + leading: Icon(Bootstrap.play), + title: Text("Flash Cards"), + subtitle: Text( + "Curious if you really understood? Try our flashcards", + ), + ), + ), + ], + ), + ) + ], + ), + ); + } +} diff --git a/lib/features/essentials/views/essentials_page.dart b/lib/features/essentials/views/essentials_page.dart new file mode 100644 index 0000000..51774cf --- /dev/null +++ b/lib/features/essentials/views/essentials_page.dart @@ -0,0 +1,19 @@ +import 'package:academia/features/essentials/views/essentials_mobile_page.dart'; +import 'package:academia/utils/responsive/responsive.dart'; +import 'package:flutter/material.dart'; + +class EssentialsPage extends StatelessWidget { + const EssentialsPage({super.key}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) => + constraints.maxWidth < ScreenDimension.mobileWidth + ? const EssentialsMobilePage() + : const Center( + child: Text("Essentials coming soon"), + ), + ); + } +} diff --git a/lib/features/features.dart b/lib/features/features.dart index 6a4ff23..5c34560 100644 --- a/lib/features/features.dart +++ b/lib/features/features.dart @@ -2,3 +2,4 @@ export 'onboarding/onboarding.dart'; export 'auth/auth.dart'; export 'home/home.dart'; export 'profile/profile.dart'; +export 'courses/courses.dart'; diff --git a/lib/features/home/views/layout.dart b/lib/features/home/views/layout.dart index 310ae91..859655e 100644 --- a/lib/features/home/views/layout.dart +++ b/lib/features/home/views/layout.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:academia/features/essentials/essentials.dart'; import 'package:academia/features/features.dart'; import 'package:academia/utils/responsive/responsive.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; @@ -78,15 +79,11 @@ class _LayoutState extends State { Center( child: Text("Statistics"), ), - Center( - child: Text("Courses"), - ), + CoursesPage(), Center( child: Text("Social"), ), - Center( - child: Text("Statistics"), - ), + EssentialsPage(), ProfilePage() ], ) @@ -137,15 +134,11 @@ class _LayoutState extends State { Center( child: Text("Statistics"), ), - Center( - child: Text("Courses"), - ), + CoursesPage(), Center( child: Text("Social"), ), - Center( - child: Text("Statistics"), - ), + EssentialsPage(), ProfilePage() ], ), diff --git a/lib/features/profile/views/profile_page_mobile.dart b/lib/features/profile/views/profile_page_mobile.dart index bdd775c..cf77b76 100644 --- a/lib/features/profile/views/profile_page_mobile.dart +++ b/lib/features/profile/views/profile_page_mobile.dart @@ -1,8 +1,11 @@ import 'package:academia/database/database.dart'; import 'package:academia/features/features.dart'; +import 'package:academia/utils/router/router.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:sliver_tools/sliver_tools.dart'; @@ -65,7 +68,12 @@ class _ProfilePageMobileState extends State { icon: const Icon(Bootstrap.pencil), ), IconButton( - onPressed: () {}, + onPressed: () { + HapticFeedback.heavyImpact().then((val) { + if (!context.mounted) return; + GoRouter.of(context).pushNamed(AcademiaRouter.auth); + }); + }, icon: const Icon(Bootstrap.person_add), ) ], @@ -180,6 +188,7 @@ class _ProfilePageMobileState extends State { ), Card( elevation: 0, + margin: const EdgeInsets.only(bottom: 2), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(12), @@ -193,9 +202,10 @@ class _ProfilePageMobileState extends State { ), Card( elevation: 0, + margin: const EdgeInsets.only(bottom: 2), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( - top: Radius.circular(12), + top: Radius.zero, ), ), child: ListTile( @@ -206,9 +216,10 @@ class _ProfilePageMobileState extends State { ), Card( elevation: 0, + margin: const EdgeInsets.only(bottom: 2), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( - top: Radius.circular(12), + top: Radius.zero, ), ), child: ListTile( @@ -222,6 +233,7 @@ class _ProfilePageMobileState extends State { ), Card( elevation: 0, + margin: const EdgeInsets.only(bottom: 2), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.zero)), child: ListTile( @@ -232,6 +244,7 @@ class _ProfilePageMobileState extends State { ), Card( elevation: 0, + margin: const EdgeInsets.only(bottom: 2), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.zero)), child: ListTile( @@ -242,6 +255,7 @@ class _ProfilePageMobileState extends State { ), Card( elevation: 0, + margin: const EdgeInsets.only(bottom: 2), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.zero)), child: ListTile( @@ -252,6 +266,7 @@ class _ProfilePageMobileState extends State { ), Card( elevation: 0, + margin: const EdgeInsets.only(bottom: 2), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.zero)), child: ListTile( @@ -260,17 +275,18 @@ class _ProfilePageMobileState extends State { subtitle: Text(user.email ?? "unknown"), ), ), - const Card( + Card( + margin: EdgeInsets.zero, elevation: 0, - shape: RoundedRectangleBorder( + shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( bottom: Radius.circular(12), ), ), child: ListTile( - leading: Icon(Bootstrap.hash), - title: Text("Admission Number"), - subtitle: Text("21-1000"), + leading: const Icon(Bootstrap.house_heart), + title: const Text("Campus"), + subtitle: Text(profile.campus), ), ), ], diff --git a/lib/utils/network/auth_interceptor.dart b/lib/utils/network/auth_interceptor.dart index c143e47..b06dc49 100644 --- a/lib/utils/network/auth_interceptor.dart +++ b/lib/utils/network/auth_interceptor.dart @@ -35,11 +35,13 @@ class AuthInterceptor extends Interceptor { @override void onError(DioException err, ErrorInterceptorHandler handler) async { - // TODO: erick Add automatic token refreshing - if (err.response?.statusCode == 401) { - return handler - .resolve(await dio.fetch(err.requestOptions)); // Repeat the request. - } + // TODO: erick Add automatic token refreshing and code retrial + // if (err.response?.statusCode == 401) { + // print("Some really bad error"); + // + // return handler + // .resolve(await dio.fetch(err.requestOptions)); // Repeat the request. + // } return handler.reject(DioException( requestOptions: err.requestOptions, diff --git a/lib/utils/network/dio_client.dart b/lib/utils/network/dio_client.dart index 96c191b..1fb3318 100644 --- a/lib/utils/network/dio_client.dart +++ b/lib/utils/network/dio_client.dart @@ -4,8 +4,8 @@ import './auth_interceptor.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart'; class DioClient { - // static const String _baseUrl = "http://192.168.2.115:8000/v2"; - static const String _baseUrl = "http://127.0.0.1:8000/v2"; + static const String _baseUrl = "http://192.168.43.218:8000/v2"; + // static const String _baseUrl = "http://127.0.0.1:8000/v2"; DioClient() { dio.interceptors.add( diff --git a/pubspec.yaml b/pubspec.yaml index 606a5ba..7d55ba8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: get_it: ^8.0.2 pretty_dio_logger: ^1.4.0 connectivity_plus: ^6.1.1 + flex_color_picker: ^3.6.0 dev_dependencies: flutter_test: