diff --git a/filcnaplo/lib/models/self_note.dart b/filcnaplo/lib/models/self_note.dart new file mode 100644 index 0000000..58944d8 --- /dev/null +++ b/filcnaplo/lib/models/self_note.dart @@ -0,0 +1,22 @@ +class SelfNote { + String id; + String? title; + String content; + Map? json; + + SelfNote({ + required this.id, + this.title, + required this.content, + this.json, + }); + + factory SelfNote.fromJson(Map json) { + return SelfNote( + id: json['id'], + title: json['title'], + content: json['content'], + json: json, + ); + } +} diff --git a/filcnaplo/lib/providers/todo_notes_provider.dart b/filcnaplo/lib/providers/todo_notes_provider.dart new file mode 100644 index 0000000..0f1d848 --- /dev/null +++ b/filcnaplo/lib/providers/todo_notes_provider.dart @@ -0,0 +1,98 @@ +// // ignore_for_file: no_leading_underscores_for_local_identifiers + +// import 'package:filcnaplo/api/providers/user_provider.dart'; +// import 'package:filcnaplo/api/providers/database_provider.dart'; +// import 'package:filcnaplo/models/user.dart'; +// import 'package:filcnaplo_kreta_api/client/api.dart'; +// import 'package:filcnaplo_kreta_api/client/client.dart'; +// import 'package:filcnaplo_kreta_api/models/absence.dart'; +// import 'package:flutter/material.dart'; +// import 'package:provider/provider.dart'; + +// class TodoNotesProvider with ChangeNotifier { +// late Map<> _absences; +// late BuildContext _context; +// List get absences => _absences; + +// TodoNotesProvider({ +// List initialAbsences = const [], +// required BuildContext context, +// }) { +// _absences = List.castFrom(initialAbsences); +// _context = context; + +// if (_absences.isEmpty) restore(); +// } + +// Future restore() async { +// String? userId = Provider.of(_context, listen: false).id; + +// // Load absences from the database +// if (userId != null) { +// var dbAbsences = +// await Provider.of(_context, listen: false) +// .userQuery +// .getAbsences(userId: userId); +// _absences = dbAbsences; +// await convertBySettings(); +// } +// } + +// // for renamed subjects +// Future convertBySettings() async { +// final _database = Provider.of(_context, listen: false); +// Map renamedSubjects = +// (await _database.query.getSettings(_database)).renamedSubjectsEnabled +// ? await _database.userQuery.renamedSubjects( +// userId: +// // ignore: use_build_context_synchronously +// Provider.of(_context, listen: false).user!.id) +// : {}; +// Map renamedTeachers = +// (await _database.query.getSettings(_database)).renamedTeachersEnabled +// ? await _database.userQuery.renamedTeachers( +// userId: +// // ignore: use_build_context_synchronously +// Provider.of(_context, listen: false).user!.id) +// : {}; + +// for (Absence absence in _absences) { +// absence.subject.renamedTo = renamedSubjects.isNotEmpty +// ? renamedSubjects[absence.subject.id] +// : null; +// absence.teacher.renamedTo = renamedTeachers.isNotEmpty +// ? renamedTeachers[absence.teacher.id] +// : null; +// } + +// notifyListeners(); +// } + +// // Fetches Absences from the Kreta API then stores them in the database +// Future fetch() async { +// User? user = Provider.of(_context, listen: false).user; +// if (user == null) throw "Cannot fetch Absences for User null"; +// String iss = user.instituteCode; + +// List? absencesJson = await Provider.of(_context, listen: false) +// .getAPI(KretaAPI.absences(iss)); +// if (absencesJson == null) throw "Cannot fetch Absences for User ${user.id}"; +// List absences = +// absencesJson.map((e) => Absence.fromJson(e)).toList(); + +// if (absences.isNotEmpty || _absences.isNotEmpty) await store(absences); +// } + +// // Stores Absences in the database +// Future store(List absences) async { +// User? user = Provider.of(_context, listen: false).user; +// if (user == null) throw "Cannot store Absences for User null"; +// String userId = user.id; + +// await Provider.of(_context, listen: false) +// .userStore +// .storeAbsences(absences, userId: userId); +// _absences = absences; +// await convertBySettings(); +// } +// } diff --git a/filcnaplo_mobile_ui/lib/common/widgets/tick_tile.dart b/filcnaplo_mobile_ui/lib/common/widgets/tick_tile.dart new file mode 100644 index 0000000..37c8604 --- /dev/null +++ b/filcnaplo_mobile_ui/lib/common/widgets/tick_tile.dart @@ -0,0 +1,109 @@ +import 'package:filcnaplo/theme/colors/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; + +class TickTile extends StatefulWidget { + const TickTile({ + super.key, + this.onTap, + this.isTicked = false, + required this.title, + this.description, + this.padding, + }); + + final Function(bool)? onTap; + final bool isTicked; + final String title; + final String? description; + final EdgeInsetsGeometry? padding; + + @override + TickTileState createState() => TickTileState(); +} + +class TickTileState extends State { + late bool isTicked; + + @override + void initState() { + isTicked = widget.isTicked; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + child: Padding( + padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 8.0), + child: ListTile( + onTap: () { + widget.onTap!(!isTicked); + + setState(() { + isTicked == true ? isTicked = false : isTicked = true; + }); + }, + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.only(left: 8.0, right: 4.0), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)), + leading: !isTicked + ? Padding( + padding: const EdgeInsets.only(top: 2.0, left: 0.8), + child: Container( + width: 20.5, + height: 20.5, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 2.0, + ), + ), + ), + ) + : Icon( + FeatherIcons.checkCircle, + size: 22.0, + color: Theme.of(context).colorScheme.primary, + ), + title: Row( + children: [ + Expanded( + child: Text( + widget.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 15.5, + decoration: isTicked ? TextDecoration.lineThrough : null, + color: isTicked + ? AppColors.of(context).text.withOpacity(0.5) + : null, + ), + ), + ), + ], + ), + subtitle: widget.description != null + ? Text( + widget.description!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 12.0, + color: isTicked + ? AppColors.of(context).text.withOpacity(0.5) + : null, + ), + ) + : null, + ), + ), + ); + } +} diff --git a/filcnaplo_mobile_ui/lib/screens/notes/notes_screen.dart b/filcnaplo_mobile_ui/lib/screens/notes/notes_screen.dart index 12b7d06..f7478bd 100644 --- a/filcnaplo_mobile_ui/lib/screens/notes/notes_screen.dart +++ b/filcnaplo_mobile_ui/lib/screens/notes/notes_screen.dart @@ -1,25 +1,230 @@ -import 'package:filcnaplo/theme/colors/colors.dart'; -import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.i18n.dart'; -import 'package:flutter/material.dart'; +import 'dart:math'; -class NotesScreen extends StatelessWidget { - const NotesScreen({super.key}); +import 'package:filcnaplo/api/providers/database_provider.dart'; +import 'package:filcnaplo/api/providers/user_provider.dart'; +import 'package:filcnaplo/theme/colors/colors.dart'; +import 'package:filcnaplo/utils/format.dart'; +import 'package:filcnaplo_kreta_api/models/homework.dart'; +import 'package:filcnaplo_kreta_api/providers/homework_provider.dart'; +import 'package:filcnaplo_mobile_ui/common/empty.dart'; +import 'package:filcnaplo_mobile_ui/common/panel/panel.dart'; +import 'package:filcnaplo_mobile_ui/common/widgets/tick_tile.dart'; +import 'package:filcnaplo_mobile_ui/screens/notes/notes_screen.i18n.dart'; +import 'package:filcnaplo_premium/providers/premium_provider.dart'; +import 'package:filcnaplo_premium/ui/mobile/premium/premium_inline.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; + +class NotesScreen extends StatefulWidget { + const NotesScreen({super.key, required this.doneItems}); + + final Map doneItems; + + @override + NotesScreenState createState() => NotesScreenState(); +} + +class NotesScreenState extends State { + late UserProvider user; + late HomeworkProvider homeworkProvider; + late DatabaseProvider databaseProvider; + + List noteTiles = []; @override Widget build(BuildContext context) { + user = Provider.of(context); + homeworkProvider = Provider.of(context); + databaseProvider = Provider.of(context); + + void generateTiles() { + List tiles = []; + + List hw = homeworkProvider.homework + .where((e) => e.deadline.isAfter(DateTime.now())) + // e.deadline.isBefore(DateTime(DateTime.now().year, + // DateTime.now().month, DateTime.now().day + 3))) + .toList(); + + List toDoTiles = []; + + if (hw.isNotEmpty) { + toDoTiles.addAll(hw.map((e) => TickTile( + padding: EdgeInsets.zero, + title: 'homework'.i18n, + description: + '${(e.subject.isRenamed ? e.subject.renamedTo : e.subject.name) ?? ''}, ${e.content.escapeHtml()}', + isTicked: widget.doneItems[e.id] ?? false, + onTap: (p0) async { + if (!widget.doneItems.containsKey(e.id)) { + widget.doneItems.addAll({e.id: p0}); + } else { + widget.doneItems[e.id] = p0; + } + await databaseProvider.userStore + .storeToDoItem(widget.doneItems, userId: user.id!); + }, + ))); + } + + if (toDoTiles.isNotEmpty) { + tiles.add(Panel( + title: Text('todo'.i18n), + child: Column( + children: toDoTiles, + ), + )); + } + + if (tiles.isNotEmpty) { + } else { + tiles.insert( + 0, + Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Empty(subtitle: "empty".i18n), + ), + ); + } + + tiles.add(Provider.of(context, listen: false).hasPremium + ? const SizedBox() + : const Padding( + padding: EdgeInsets.only(top: 24.0), + child: PremiumInline(features: [ + PremiumInlineFeature.stats, + ]), + )); + + // padding + tiles.add(const SizedBox(height: 32.0)); + + noteTiles = List.castFrom(tiles); + } + + generateTiles(); + return Scaffold( appBar: AppBar( surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, leading: BackButton(color: AppColors.of(context).text), - title: Text("notes".i18n, - style: TextStyle(color: AppColors.of(context).text)), + title: Text( + "notes".i18n, + style: TextStyle( + color: AppColors.of(context).text, + fontSize: 26.0, + fontWeight: FontWeight.bold, + ), + ), + actions: [ + ClipRRect( + borderRadius: BorderRadius.circular(10.1), + child: GestureDetector( + onTap: () { + // handle tap + }, + child: Container( + color: Theme.of(context).colorScheme.primary.withOpacity(0.4), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Stack( + children: [ + IconTheme( + data: IconThemeData( + color: Theme.of(context).colorScheme.secondary, + ), + child: const Icon( + FeatherIcons.search, + size: 20.0, + ), + ), + IconTheme( + data: IconThemeData( + color: + Theme.of(context).brightness == Brightness.light + ? Colors.black.withOpacity(.5) + : Colors.white.withOpacity(.3), + ), + child: const Icon( + FeatherIcons.search, + size: 20.0, + ), + ), + ], + ), + ), + ), + ), + ), + const SizedBox( + width: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular(10.1), + child: GestureDetector( + onTap: () { + // handle tap + }, + child: Container( + color: Theme.of(context).colorScheme.primary.withOpacity(0.4), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Stack( + children: [ + IconTheme( + data: IconThemeData( + color: Theme.of(context).colorScheme.secondary, + ), + child: const Icon( + FeatherIcons.plus, + size: 20.0, + ), + ), + IconTheme( + data: IconThemeData( + color: + Theme.of(context).brightness == Brightness.light + ? Colors.black.withOpacity(.5) + : Colors.white.withOpacity(.3), + ), + child: const Icon( + FeatherIcons.plus, + size: 20.0, + ), + ), + ], + ), + ), + ), + ), + ), + const SizedBox( + width: 20, + ), + ], ), body: SafeArea( child: RefreshIndicator( - onRefresh: () { - return Future(() => null); + onRefresh: () async { + await homeworkProvider.fetch(); + }, + child: ListView.builder( + padding: EdgeInsets.zero, + physics: const BouncingScrollPhysics(), + itemCount: max(noteTiles.length, 1), + itemBuilder: (context, index) { + if (noteTiles.isNotEmpty) { + EdgeInsetsGeometry panelPadding = + const EdgeInsets.symmetric(horizontal: 24.0); + + return Padding(padding: panelPadding, child: noteTiles[index]); + } else { + return Container(); + } }, - child: Text('soon')), + ), + ), ), ); } diff --git a/filcnaplo_mobile_ui/lib/screens/notes/notes_screen.i18n.dart b/filcnaplo_mobile_ui/lib/screens/notes/notes_screen.i18n.dart new file mode 100644 index 0000000..37cc404 --- /dev/null +++ b/filcnaplo_mobile_ui/lib/screens/notes/notes_screen.i18n.dart @@ -0,0 +1,30 @@ +import 'package:i18n_extension/i18n_extension.dart'; + +extension SettingsLocalization on String { + static final _t = Translations.byLocale("hu_hu") + + { + "en_en": { + "notes": "Notes", + "empty": "You don't have any notes", + "todo": "Tasks", + "homework": "Homework", + }, + "hu_hu": { + "notes": "Füzet", + "empty": "Nincsenek jegyzeteid", + "todo": "Feladatok", + "homework": "Házi feladat", + }, + "de_de": { + "notes": "Broschüre", + "empty": "Sie haben keine Notizen", + "todo": "Aufgaben", + "homework": "Hausaufgaben", + }, + }; + + String get i18n => localize(this, _t); + String fill(List params) => localizeFill(this, params); + String plural(int value) => localizePlural(value, this, _t); + String version(Object modifier) => localizeVersion(modifier, this, _t); +} diff --git a/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart b/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart index a29245b..c2e334a 100755 --- a/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart +++ b/filcnaplo_mobile_ui/lib/screens/settings/settings_screen.dart @@ -65,7 +65,9 @@ class SettingsScreenState extends State late UserProvider user; late UpdateProvider updateProvider; late SettingsProvider settings; + late DatabaseProvider databaseProvider; late KretaClient kretaClient; + late String firstName; List accountTiles = []; @@ -181,6 +183,7 @@ class SettingsScreenState extends State user = Provider.of(context); settings = Provider.of(context); updateProvider = Provider.of(context); + databaseProvider = Provider.of(context); kretaClient = Provider.of(context); List nameParts = user.displayName?.split(" ") ?? ["?"]; @@ -237,7 +240,10 @@ class SettingsScreenState extends State // ), IconButton( splashRadius: 32.0, - onPressed: () => _openNotes(context), + onPressed: () async => _openNotes( + context, + await databaseProvider.userQuery + .toDoItems(userId: user.id!)), // _showBottomSheet(user.getUser(user.id ?? "")), icon: Icon(FeatherIcons.fileText, color: AppColors.of(context).text.withOpacity(0.8)), @@ -1106,7 +1112,9 @@ class SettingsScreenState extends State void _openUpdates(BuildContext context) => UpdateView.show(updateProvider.releases.first, context: context); void _openPrivacy(BuildContext context) => PrivacyView.show(context); - void _openNotes(BuildContext context) => - Navigator.of(context, rootNavigator: true) - .push(CupertinoPageRoute(builder: (context) => const NotesScreen())); + void _openNotes(BuildContext context, Map doneItems) async => + Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute( + builder: (context) => NotesScreen( + doneItems: doneItems, + ))); }