diff --git a/project/FrontEnd/collaborative_science_platform/ios/Podfile.lock b/project/FrontEnd/collaborative_science_platform/ios/Podfile.lock index b8ae62a0..645bec45 100644 --- a/project/FrontEnd/collaborative_science_platform/ios/Podfile.lock +++ b/project/FrontEnd/collaborative_science_platform/ios/Podfile.lock @@ -5,6 +5,9 @@ PODS: - FlutterMacOS - share_plus (0.0.1): - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - url_launcher_ios (0.0.1): - Flutter - webview_flutter_wkwebview (0.0.1): @@ -14,6 +17,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) @@ -24,6 +28,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" share_plus: :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" webview_flutter_wkwebview: @@ -33,6 +39,7 @@ SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86 webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f diff --git a/project/FrontEnd/collaborative_science_platform/lib/exceptions/question_exceptions.dart b/project/FrontEnd/collaborative_science_platform/lib/exceptions/question_exceptions.dart new file mode 100644 index 00000000..df4083ce --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/exceptions/question_exceptions.dart @@ -0,0 +1,9 @@ +class PostQuestionError implements Exception { + String message; + PostQuestionError({this.message = "Post Question Error"}); +} + +class PostAnswerError implements Exception { + String message; + PostAnswerError({this.message = "Post Answer Error"}); +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/extensions/string_extensions.dart b/project/FrontEnd/collaborative_science_platform/lib/extensions/string_extensions.dart new file mode 100644 index 00000000..5d929489 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/extensions/string_extensions.dart @@ -0,0 +1,5 @@ +extension StringExtension on String { + String capitalize() { + return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/helpers/select_buttons_helper.dart b/project/FrontEnd/collaborative_science_platform/lib/helpers/select_buttons_helper.dart new file mode 100644 index 00000000..b4d18d14 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/helpers/select_buttons_helper.dart @@ -0,0 +1,3 @@ +class SelectButtonsHelper { + static int selectedIndex = 0; +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/main.dart b/project/FrontEnd/collaborative_science_platform/lib/main.dart index 98a9f70e..f56de687 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/main.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/main.dart @@ -1,7 +1,12 @@ +import 'package:collaborative_science_platform/providers/admin_provider.dart'; +import 'package:collaborative_science_platform/providers/annotation_provider.dart'; import 'package:collaborative_science_platform/providers/auth.dart'; import 'package:collaborative_science_platform/providers/profile_data_provider.dart'; import 'package:collaborative_science_platform/providers/node_provider.dart'; +import 'package:collaborative_science_platform/providers/question_provider.dart'; +import 'package:collaborative_science_platform/providers/settings_provider.dart'; import 'package:collaborative_science_platform/providers/user_provider.dart'; +import 'package:collaborative_science_platform/providers/wiki_data_provider.dart'; import 'package:collaborative_science_platform/providers/workspace_provider.dart'; import 'package:collaborative_science_platform/services/screen_navigation.dart'; import 'package:collaborative_science_platform/utils/constants.dart'; @@ -12,9 +17,9 @@ import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:provider/provider.dart'; -void main() { +void main() async { configureApp(); - runApp(const MyApp()); + runApp(ChangeNotifierProvider.value(value: Auth(), child: const MyApp())); } void configureApp() { @@ -25,55 +30,50 @@ void configureApp() { class MyApp extends StatelessWidget { const MyApp({super.key}); + + Future checkTokenAndLogin(BuildContext context) async { + final auth = Provider.of(context, listen: false); + await auth.checkTokenAndLogin(); + } + @override Widget build(BuildContext context) { return MultiProvider( providers: [ - ChangeNotifierProvider(create: (context) => Auth()), ChangeNotifierProvider(create: (context) => ScreenNavigation()), ChangeNotifierProvider(create: (context) => ProfileDataProvider()), ChangeNotifierProvider(create: (context) => NodeProvider()), ChangeNotifierProvider(create: (context) => UserProvider()), ChangeNotifierProvider(create: (context) => WorkspaceProvider()), + ChangeNotifierProvider( + create: (context) => QuestionAnswerProvider()), + ChangeNotifierProvider(create: (context) => AnnotationProvider()), + ChangeNotifierProvider(create: (context) => WikiDataProvider()), + ChangeNotifierProvider(create: (context) => AdminProvider()), + ChangeNotifierProvider(create: (context) => SettingsProvider()), ], - // child: MaterialApp( - // debugShowCheckedModeBanner: false, - // title: Constants.appName, - // routes: { - // '/': (context) => const HomePage(), - // LoginPage.routeName: (context) => const LoginPage(), - // SignUpPage.routeName: (context) => const SignUpPage(), - // WorkspacesPage.routeName: (context) => const WorkspacesPage(), -// - // ///ProfilePage.routeName: (context) => const ProfilePage(), - // GraphPage.routeName: (context) => const GraphPage(), - // NotificationPage.routeName: (context) => const NotificationPage(), - // AccountSettingsPage.routeName: (context) => const AccountSettingsPage(), - // PleaseLoginPage2.routeName: (context) => const PleaseLoginPage2(), - // NodeDetailsPage.routeName: (context) { - // final int nodeId = ModalRoute.of(context)!.settings.arguments as int; - // return NodeDetailsPage(nodeID: nodeId); - // }, - // ProfilePage.routeName: (context) { - // final String email = ModalRoute.of(context)!.settings.arguments as String ?? ""; - // return ProfilePage(email: email); - // }, - // }, - // navigatorKey: ScreenNavigation.navigatorKey, - // theme: ThemeData( - // colorScheme: ColorScheme.fromSeed(seedColor: AppColors.primaryColor), - // useMaterial3: true, - // ), - child: Portal( - child: MaterialApp.router( - routerConfig: router, - debugShowCheckedModeBanner: false, - title: Constants.appName, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Color.fromARGB(255, 85, 234, 145)), - useMaterial3: true, - ), - ), + child: FutureBuilder( + future: checkTokenAndLogin(context), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Portal( + child: MaterialApp.router( + routerConfig: router, + debugShowCheckedModeBanner: false, + title: Constants.appName, + theme: ThemeData( + colorScheme: + ColorScheme.fromSeed(seedColor: const Color.fromARGB(255, 85, 234, 145)), + useMaterial3: true, + ), + ), + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, ), ); } diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/annotation.dart b/project/FrontEnd/collaborative_science_platform/lib/models/annotation.dart index fe20d8c4..b787349e 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/models/annotation.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/models/annotation.dart @@ -1,25 +1,24 @@ -import 'package:collaborative_science_platform/models/basic_user.dart'; - class Annotation { - int annotationID; - String annotationType; - String annotationVisibilityType; - BasicUser owner; - Object annotationLocation; + int? annotationID; +// String annotationType; +// String annotationVisibilityType; +// BasicUser owner; +// Object annotationLocation; + String annotationContent; + String annotationAuthor; + String + sourceLocation; // ${Constants.appUrl}/node/{nodeId}%23{theorem|proof}%23{theoremId|proofId} int startOffset; int endOffset; - DateTime createdAt; - DateTime updatedAt; + DateTime dateCreated; Annotation({ - required this.annotationID, - required this.annotationType, - required this.annotationVisibilityType, - required this.owner, - required this.annotationLocation, + this.annotationID, required this.startOffset, required this.endOffset, - required this.createdAt, - required this.updatedAt, + required this.annotationContent, + required this.annotationAuthor, + required this.sourceLocation, + required this.dateCreated, }); } diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/basic_user.dart b/project/FrontEnd/collaborative_science_platform/lib/models/basic_user.dart index a8d635af..a98af6e0 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/models/basic_user.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/models/basic_user.dart @@ -3,12 +3,14 @@ class BasicUser { String bio; bool emailNotificationPreference; bool showActivity; + String userType; BasicUser({ - required this.basicUserId, - required this.bio, - required this.emailNotificationPreference, - required this.showActivity, + this.basicUserId = 0, + this.bio = "", + this.emailNotificationPreference = true, + this.showActivity = true, + this.userType = "", }); factory BasicUser.fromJson(Map jsonString) { return BasicUser( @@ -16,7 +18,7 @@ class BasicUser { bio: jsonString["bio"], emailNotificationPreference: jsonString["email_notification_preference"], showActivity: jsonString["show_activity_preference"], + userType: jsonString["user_type"], ); } - } diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/node_details_page/node_detailed.dart b/project/FrontEnd/collaborative_science_platform/lib/models/node_details_page/node_detailed.dart index 6f52fe18..429c7f92 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/models/node_details_page/node_detailed.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/models/node_details_page/node_detailed.dart @@ -1,6 +1,7 @@ import 'package:collaborative_science_platform/models/node.dart'; import 'package:collaborative_science_platform/models/node_details_page/proof.dart'; import 'package:collaborative_science_platform/models/node_details_page/question.dart'; +import 'package:collaborative_science_platform/models/semantic_tag.dart'; import 'package:collaborative_science_platform/models/theorem.dart'; import 'package:collaborative_science_platform/models/user.dart'; @@ -16,8 +17,10 @@ class NodeDetailed { List reviewers; List references; List citations; + List semanticTags; bool isValid; int noVisits; + bool isHidden; List questions; //List semanticTags; @@ -37,6 +40,8 @@ class NodeDetailed { this.isValid = true, this.noVisits = 0, this.questions = const [], + this.semanticTags = const [], + this.isHidden = false, //required this.semanticTags, //required this.wikiTags, //required this.annotations, @@ -55,6 +60,7 @@ class NodeDetailed { var proofsList = jsonString['proofs'] as List; var theorem = Theorem.fromJson(jsonString['theorem']); var questionsList = jsonString['question_set'] as List; + var semanticTagsList = jsonString['semantic_tags'] as List; List references = referencesList.map((e) => Node.fromJsonforNodeDetailPage(e)).toList(); List citations = citationsList.map((e) => Node.fromJsonforNodeDetailPage(e)).toList(); List contributors = @@ -62,10 +68,13 @@ class NodeDetailed { //List reviewers = reviewersList.map((e) => User.fromJsonforNodeDetailPage(e)).toList(); List proof = proofsList.map((e) => Proof.fromJson(e)).toList(); List questions = questionsList.map((e) => Question.fromJson(e)).toList(); + List semanticTags = + semanticTagsList.map((e) => SemanticTag.fromJsonforNodeDetailPage(e)).toList(); return NodeDetailed( citations: citations, contributors: contributors, isValid: jsonString['is_valid'], + isHidden: jsonString['removed_by_admin'], nodeId: jsonString['node_id'], nodeTitle: jsonString['node_title'], noVisits: jsonString['num_visits'], @@ -74,6 +83,7 @@ class NodeDetailed { questions: questions, references: references, //reviewers: reviewers, + semanticTags: semanticTags, theorem: theorem, ); } diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/node_details_page/question.dart b/project/FrontEnd/collaborative_science_platform/lib/models/node_details_page/question.dart index 51627119..ab65c9b5 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/models/node_details_page/question.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/models/node_details_page/question.dart @@ -1,22 +1,30 @@ import 'package:collaborative_science_platform/models/user.dart'; class Question { + int id; String content; String createdAt; - User? asker; - String answer; + User asker; + String? answer; User? answerer; - String answeredAt; - Question({ - required this.content, - required this.createdAt, - required this.answer, - required this.answeredAt, - required this.answerer, - required this.asker, - }); + String? answeredAt; + int? nodeId; + bool isAnswered; + bool isHidden; + Question( + {required this.id, + required this.content, + required this.createdAt, + required this.asker, + required this.answer, + required this.answerer, + required this.answeredAt, + required this.nodeId, + required this.isAnswered, + required this.isHidden}); factory Question.fromJson(Map jsonString) { return Question( + id: jsonString['id'] ?? -1, content: jsonString['question_content'] ?? "", createdAt: jsonString['created_at'] ?? "", answer: jsonString['answer_content'] ?? "", @@ -24,8 +32,37 @@ class Question { answerer: jsonString['answerer'] == null ? null : User.fromJsonforNodeDetailPage(jsonString['answerer']), - asker: - jsonString['asker'] == null ? null : User.fromJsonforNodeDetailPage(jsonString['asker']), + asker: User.fromJsonforNodeDetailPage(jsonString['asker']), + nodeId: jsonString['node_id'] ?? -1, + isAnswered: jsonString['answer_content'] != null, + isHidden: jsonString['removed_by_admin'] ?? false, + ); + } + + factory Question.fromJsonforProfilePage(Map jsonString) { + return Question( + id: jsonString['id'] ?? -1, + content: jsonString['question_content'] ?? "", + createdAt: jsonString['ask_date'] ?? "", + asker: User( + id: jsonString['asker_id'], + email: jsonString['asker_mail'], + firstName: jsonString['asker_name'], + lastName: jsonString['asker_surname'], + ), + answer: jsonString.containsKey("answer_content") ? jsonString["answer_content"] : "", + answerer: jsonString.containsKey("answerer") + ? User( + id: jsonString['answerer_id'], + email: jsonString['answerer_mail'], + firstName: jsonString['answerer_name'], + lastName: jsonString['answerer_surname'], + ) + : null, + answeredAt: jsonString.containsKey("answer_date") ? jsonString["answer_date"] as String : "", + nodeId: jsonString['node_id'] ?? -1, + isAnswered: jsonString['is_answered'] == 1, + isHidden: jsonString['removed_by_admin'] ?? false, ); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/profile_data.dart b/project/FrontEnd/collaborative_science_platform/lib/models/profile_data.dart index be633822..28154e3c 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/models/profile_data.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/models/profile_data.dart @@ -1,4 +1,6 @@ +import 'package:collaborative_science_platform/models/node_details_page/question.dart'; import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/models/workspace_semantic_tag.dart'; class Node { int id; @@ -12,40 +14,68 @@ class Node { required this.publishDate, }); factory Node.fromJson(Map jsonString) { - var list = jsonString['authors'] as List; - List contributors = list.map((e) => User.fromJson(e)).toList(); + var contributorList = jsonString['authors'] as List; + List contributors = contributorList.map((e) => User.fromJson(e)).toList(); return Node( - id: jsonString['id'], - nodeTitle: jsonString['title'], - publishDate: jsonString['date'], - contributors: contributors); + id: jsonString['id'], + nodeTitle: jsonString['title'], + publishDate: jsonString['date'], + contributors: contributors, + ); } } class ProfileData { + int id; String name; String surname; String email; String aboutMe; + String orcid; List nodes; - List askedQuestionIDs; - List answeredQuestionIDs; - ProfileData( - {this.aboutMe = "", - this.email = "", - this.name = "", - this.surname = "", - this.nodes = const [], - this.askedQuestionIDs = const [], - this.answeredQuestionIDs = const []}); + List askedQuestions; + List answeredQuestions; + String userType; + bool isBanned; + List tags; + ProfileData({ + this.id = 0, + this.aboutMe = "", + this.email = "", + this.name = "", + this.surname = "", + this.orcid = "", + this.tags = const [], + this.nodes = const [], + this.askedQuestions = const [], + this.answeredQuestions = const [], + this.userType = "", + this.isBanned = false, + }); + factory ProfileData.fromJson(Map jsonString) { - var list = jsonString['nodes'] as List; - List nodes = list.map((e) => Node.fromJson(e)).toList(); + var nodeList = jsonString['nodes'] as List; + var askedList = jsonString['asked_questions'] as List; + var answeredList = jsonString['answered_questions'] as List; + var tagList = jsonString['semantic_tags'] as List; + + List nodes = nodeList.map((e) => Node.fromJson(e)).toList(); + List asked = askedList.map((e) => Question.fromJsonforProfilePage(e)).toList(); + List answered = answeredList.map((e) => Question.fromJsonforProfilePage(e)).toList(); + List tags = tagList.map((e) => WorkspaceSemanticTag.fromJson(e)).toList(); + return ProfileData( - nodes: nodes, + id: jsonString['id'], name: jsonString['name'], surname: jsonString['surname'], aboutMe: jsonString['bio'], + orcid: jsonString['orcid'] ?? "", + nodes: nodes, + askedQuestions: asked, + answeredQuestions: answered, + userType: jsonString['user_type'], + isBanned: jsonString['is_banned'] ?? false, + tags: tags, ); } @@ -70,8 +100,7 @@ class ProfileData { ], ), ], - askedQuestionIDs: [1, 2, 3, 4, 5], - answeredQuestionIDs: [1, 2, 3, 4, 5], + tags: [], ); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/question.dart b/project/FrontEnd/collaborative_science_platform/lib/models/question.dart deleted file mode 100644 index c75ffe16..00000000 --- a/project/FrontEnd/collaborative_science_platform/lib/models/question.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:collaborative_science_platform/models/basic_user.dart'; -import 'package:collaborative_science_platform/models/contributor_user.dart'; - -class Question { - int questionID; - BasicUser askedBy; - String questionContent; - String answer; - DateTime publishDate; - Contributor respondedBy; - - Question({ - required this.questionID, - required this.askedBy, - required this.questionContent, - required this.answer, - required this.publishDate, - required this.respondedBy, - }); -} diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/semantic_tag.dart b/project/FrontEnd/collaborative_science_platform/lib/models/semantic_tag.dart index 8e078715..b240dc81 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/models/semantic_tag.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/models/semantic_tag.dart @@ -1,19 +1,27 @@ class SemanticTag { - final String id; + final String wid; final String label; final String description; SemanticTag({ - required this.id, + required this.wid, required this.label, required this.description, }); factory SemanticTag.fromJson(Map json) { return SemanticTag( - id: json['id'], + wid: json['id'], label: json['label'], description: json['description'], ); } + + factory SemanticTag.fromJsonforNodeDetailPage(Map json) { + return SemanticTag( + wid: json['wid'].toString(), + label: json['label'], + description: "", + ); + } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/user.dart b/project/FrontEnd/collaborative_science_platform/lib/models/user.dart index 364cb638..188f0bb7 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/models/user.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/models/user.dart @@ -4,13 +4,16 @@ class User { String firstName; String lastName; String token; + int requestId; User( {this.id = 0, this.token = '', required this.email, required this.firstName, - required this.lastName}); + required this.lastName, + this.requestId = -1, + }); factory User.fromJson(Map jsonString) { return User( @@ -28,4 +31,13 @@ class User { lastName: jsonString['last_name'], ); } + factory User.fromJsonforNodeDetailPagePendingContributors(Map jsonString) { + return User( + id: jsonString['id'], + email: jsonString['username'], + firstName: jsonString['first_name'], + lastName: jsonString['last_name'], + requestId: jsonString['request_id'], + ); + } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/workspace_semantic_tag.dart b/project/FrontEnd/collaborative_science_platform/lib/models/workspace_semantic_tag.dart new file mode 100644 index 00000000..b6ad94dd --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/models/workspace_semantic_tag.dart @@ -0,0 +1,20 @@ + +class WorkspaceSemanticTag { + final int tagId; + final String wid; + final String label; + + WorkspaceSemanticTag({ + required this.tagId, + required this.wid, + required this.label, + }); + + factory WorkspaceSemanticTag.fromJson(Map json) { + return WorkspaceSemanticTag( + tagId: json['id'], + wid: json['wid'], + label: json['label'], + ); + } +} \ No newline at end of file diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/comment.dart b/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/comment.dart new file mode 100644 index 00000000..6865ae8d --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/comment.dart @@ -0,0 +1,28 @@ +import 'package:collaborative_science_platform/models/workspaces_page/workspace.dart'; + +class Comment { + String comment; + String reviewer; + RequestStatus response; + + Comment({ + required this.comment, + required this.reviewer, + required this.response, + }); + factory Comment.fromJson(Map jsonString) { + String responseString = jsonString['response']; + RequestStatus response; + switch (responseString) { + case 'A': + response = RequestStatus.approved; + case 'R': + response = RequestStatus.rejected; + default: + response = RequestStatus.pending; + } + + return Comment( + comment: jsonString['comment'], reviewer: jsonString['reviewer'], response: response); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/entry.dart b/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/entry.dart index 2c3b235e..6a8c539d 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/entry.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/entry.dart @@ -4,6 +4,7 @@ class Entry { bool isEditable; bool isFinalEntry; bool isProofEntry; + bool isDisproofEntry; bool isTheoremEntry; DateTime entryDate; int entryId; @@ -21,6 +22,7 @@ class Entry { required this.isFinalEntry, required this.isProofEntry, required this.isTheoremEntry, + required this.isDisproofEntry, }); String get publishDateFormatted { DateFormat formatter = DateFormat('dd-MM-yyyy'); @@ -34,9 +36,11 @@ class Entry { isProofEntry: jsonString['is_proof_entry'], isTheoremEntry: jsonString['is_theorem_entry'], entryDate: DateTime.parse(jsonString['entry_date']), - entryId: jsonString['entry_id'], - entryNumber: jsonString['entry_number'], - index: jsonString['entry_index'], - content: jsonString['content']); + entryId: jsonString['entry_id'] ?? -1, + entryNumber: jsonString['entry_number'] ?? -1, + index: jsonString['entry_index'] ?? -1, + content: jsonString['content'], + isDisproofEntry: jsonString['is_disproof_entry'], + ); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspace.dart b/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspace.dart index de970972..aa84872d 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspace.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspace.dart @@ -1,8 +1,12 @@ import 'package:collaborative_science_platform/models/node.dart'; import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/models/workspace_semantic_tag.dart'; +import 'package:collaborative_science_platform/models/workspaces_page/comment.dart'; import 'package:collaborative_science_platform/models/workspaces_page/entry.dart'; -enum WorkspaceStatus {finalized, workable, inReview, published, rejected} +enum WorkspaceStatus { finalized, workable, inReview, published, rejected } + +enum RequestStatus { approved, rejected, pending } class Workspace { int workspaceId; @@ -10,48 +14,83 @@ class Workspace { List entries; WorkspaceStatus status; int numApprovals; + List tags; List contributors; List pendingContributors; List references; + int fromNodeId; + int requestId; + bool pending; + bool pendingReviewer; + bool pendingContributor; + + List comments; + Workspace({ required this.workspaceId, required this.workspaceTitle, required this.entries, required this.status, required this.numApprovals, + required this.tags, required this.contributors, required this.pendingContributors, required this.references, + required this.fromNodeId, + required this.requestId, + required this.pending, + required this.pendingContributor, + required this.pendingReviewer, + required this.comments, + }); factory Workspace.fromJson(Map jsonString) { var entryList = jsonString['workspace_entries'] as List; + var tagList = jsonString['semantic_tags'] as List; var contributorsList = jsonString['contributors'] as List; var pendingContributorsList = jsonString['pending_contributors'] as List; var referencesList = jsonString['references'] as List; + var commentsList = jsonString['comments'] as List; List entries = entryList.map((e) => Entry.fromJson(e)).toList(); + List tags = tagList.map((e) => WorkspaceSemanticTag.fromJson(e)).toList(); List contributors = contributorsList.map((e) => User.fromJsonforNodeDetailPage(e)).toList(); - List pendingContributors = - pendingContributorsList.map((e) => User.fromJsonforNodeDetailPage(e)).toList(); + List pendingContributors = pendingContributorsList + .map((e) => User.fromJsonforNodeDetailPagePendingContributors(e)) + .toList(); List references = referencesList.map((e) => Node.fromJsonforNodeDetailPage(e)).toList(); - +List comments = commentsList.map((e) => Comment.fromJson(e)).toList(); String statusString = jsonString['status']; - WorkspaceStatus status = (statusString == "finalized") ? WorkspaceStatus.finalized - : (statusString == "workable") ? WorkspaceStatus.workable - : (statusString == "in_review") ? WorkspaceStatus.inReview - : (statusString == "published") ? WorkspaceStatus.published - : WorkspaceStatus.rejected; - + WorkspaceStatus status = (statusString == "finalized") + ? WorkspaceStatus.finalized + : (statusString == "workable") + ? WorkspaceStatus.workable + : (statusString == "in_review") + ? WorkspaceStatus.inReview + : (statusString == "published") + ? WorkspaceStatus.published + : WorkspaceStatus.rejected; return Workspace( - workspaceId: jsonString['workspace_id'], - workspaceTitle: jsonString['workspace_title'], - entries: entries, - status: status, - numApprovals: jsonString['num_approvals'], - contributors: contributors, - pendingContributors: pendingContributors, - references: references); + workspaceId: jsonString['workspace_id'], + workspaceTitle: jsonString['workspace_title'], + entries: entries, + status: status, + tags: tags, + numApprovals: jsonString['num_approvals'], + contributors: contributors, + pendingContributors: pendingContributors, + references: references, + requestId: jsonString["request_id"] == "" ? -1 : jsonString["request_id"], + fromNodeId: jsonString["from_node_id"] == "" ? -1 : jsonString["from_node_id"], + + pending: jsonString["pending_reviewer"] || jsonString["pending_collab"], + pendingReviewer: jsonString["pending_reviewer"], + pendingContributor: jsonString["pending_collab"], + + comments: comments, + + ); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspaces.dart b/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspaces.dart index a2bc6e2f..9c3c6aa4 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspaces.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspaces.dart @@ -3,20 +3,36 @@ import 'package:collaborative_science_platform/models/workspaces_page/workspaces class Workspaces { List workspaces; List pendingWorkspaces; + List reviewWorkspaces; + List pendingReviewWorkspaces; Workspaces({ required this.workspaces, required this.pendingWorkspaces, + required this.pendingReviewWorkspaces, + required this.reviewWorkspaces, }); factory Workspaces.fromJson(Map jsonString) { var workspacesList = jsonString['workspaces'] as List; var pendingWorkspacesList = jsonString['pending_workspaces'] as List; + var reviewWorkspacesList = jsonString['review_workspaces'] as List; + var pendingReviewWorkspacesList = jsonString['pending_review_workspaces'] as List; List workspaces = workspacesList.map((e) => WorkspacesObject.fromJson(e)).toList(); List pendingWorkspaces = - pendingWorkspacesList.map((e) => WorkspacesObject.fromJson(e)).toList(); - return Workspaces(workspaces: workspaces, pendingWorkspaces: pendingWorkspaces); + pendingWorkspacesList.map((e) => WorkspacesObject.fromJsonforRequests(e)).toList(); + + List reviewWorkspaces = + reviewWorkspacesList.map((e) => WorkspacesObject.fromJson(e)).toList(); + + List pendingReviewWorkspaces = + pendingReviewWorkspacesList.map((e) => WorkspacesObject.fromJsonforRequests(e)).toList(); + return Workspaces( + workspaces: workspaces, + pendingWorkspaces: pendingWorkspaces, + reviewWorkspaces: reviewWorkspaces, + pendingReviewWorkspaces: pendingReviewWorkspaces); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspaces_object.dart b/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspaces_object.dart index 04806c29..973a677a 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspaces_object.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/models/workspaces_page/workspaces_object.dart @@ -2,10 +2,12 @@ class WorkspacesObject { int workspaceId; String workspaceTitle; bool pending; + int requestId; WorkspacesObject({ required this.workspaceId, required this.workspaceTitle, required this.pending, + this.requestId = -1, }); factory WorkspacesObject.fromJson(Map jsonString) { @@ -14,4 +16,13 @@ class WorkspacesObject { workspaceTitle: jsonString['workspace_title'], pending: jsonString['pending']); } + factory WorkspacesObject.fromJsonforRequests(Map jsonString) { + return WorkspacesObject( + workspaceId: jsonString['workspace_id'], + workspaceTitle: jsonString['workspace_title'], + pending: jsonString['pending'], + requestId: jsonString['request_id'], + ); + + } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/providers/admin_provider.dart b/project/FrontEnd/collaborative_science_platform/lib/providers/admin_provider.dart new file mode 100644 index 00000000..11173198 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/providers/admin_provider.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; +import 'package:collaborative_science_platform/models/node_details_page/node_detailed.dart'; +import 'package:collaborative_science_platform/models/node_details_page/question.dart'; +import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/utils/constants.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +class AdminProvider with ChangeNotifier { + Future banUser(User? admin, int userId, bool isBanned) async { + final Map header = { + "Accept": "application/json", + "content-type": "application/json", + 'Authorization': "Token ${admin!.token}", + }; + final String body = json.encode({ + 'context': "user", + 'content_id': userId, + 'hide': isBanned, + }); + try { + final response = await http.put( + Uri.parse("${Constants.apiUrl}/update_content_status/"), + headers: header, + body: body, + ); + print(response.statusCode); + return response.statusCode; + } catch (e) { + rethrow; + } + } + + Future hideNode(User? admin, NodeDetailed node, bool isHidden) async { + final Map header = { + "Accept": "application/json", + "content-type": "application/json", + 'Authorization': "Token ${admin!.token}", + }; + final String body = json.encode({ + 'context': "node", + 'content_id': node.nodeId, + 'hide': isHidden, + }); + try { + final response = await http.put( + Uri.parse("${Constants.apiUrl}/update_content_status/"), + headers: header, + body: body, + ); + print(response.statusCode); + } catch (e) { + rethrow; + } + } + + Future hideQuestion(User? admin, Question question, bool isHidden) async { + final Map header = { + "Accept": "application/json", + "content-type": "application/json", + 'Authorization': "Token ${admin!.token}", + }; + final String body = json.encode({ + 'context': "question", + 'content_id': question.id, + 'hide': isHidden, + }); + try { + final response = await http.put( + Uri.parse("${Constants.apiUrl}/update_content_status/"), + headers: header, + body: body, + ); + print(response.statusCode); + return response.statusCode; + } catch (e) { + rethrow; + } + } + + Future promoteUser(User? admin, int userId) async { + final Map header = { + "Accept": "application/json", + "content-type": "application/json", + 'Authorization': "Token ${admin!.token}", + }; + final String body = json.encode({'cont_id': userId}); + try { + final response = await http.post(Uri.parse("${Constants.apiUrl}/promote_contributor/"), + headers: header, body: body); + + if (response.statusCode == 200) { + print("User is promoted to reviewer."); + } + print(response.statusCode); + return response.statusCode; + } catch (e) { + rethrow; + } + } + + Future demoteUser(User? admin, int userId) async { + final Map header = { + "Accept": "application/json", + "content-type": "application/json", + 'Authorization': "Token ${admin!.token}", + }; + try { + final response = await http.delete( + Uri.parse("${Constants.apiUrl}/demote_reviewer/?reviewer_id=${userId.toString()}"), + headers: header); + if (response.statusCode == 200) { + print("User is demoted to contributor."); + } + print(response.statusCode); + return response.statusCode; + } catch (e) { + rethrow; + } + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/providers/annotation_provider.dart b/project/FrontEnd/collaborative_science_platform/lib/providers/annotation_provider.dart new file mode 100644 index 00000000..ed3190c0 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/providers/annotation_provider.dart @@ -0,0 +1,173 @@ +import 'package:collaborative_science_platform/models/annotation.dart'; +import 'package:collaborative_science_platform/utils/constants.dart'; +import 'package:flutter/material.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; + +enum AnnotationType { theorem, proof } + +class AnnotationProvider with ChangeNotifier { + final List _annotations = []; + + List get annotations => _annotations; + + Future getAnnotations( + String annotationSourceLocation, List annotationAuthors) async { + String baseUrl = "${Constants.annotationUrl}/annotations/get_annotation"; + + // API can match multiple authors + // WARNING: not sure exactly how to give it as a list + // Therefore, constructing query parameters manually + List queryParams = [ + 'source=${Uri.encodeQueryComponent(annotationSourceLocation)}' + .replaceAll('%2F', '/') + .replaceAll('%3A', ':') + ]; + + // Append each author as a separate 'creator' parameter as in Postman + for (String author in annotationAuthors) { + queryParams.add('creator=${Uri.encodeQueryComponent(author)}' + .replaceAll('%2F', '/') + .replaceAll('%3A', ':')); + } + + // Join all query parameters with '&' and append to the base URL + String finalUrl = '$baseUrl?${queryParams.join('&')}'; + print(finalUrl); + + try { + var response = await http.get(Uri.parse(finalUrl)); + print(response.body); + // print(response.statusCode); + if (response.statusCode == 404) { + // no annotations found + _annotations.clear(); + notifyListeners(); + return; + } + if (response.statusCode == 200) { + List annotationsJson = jsonDecode(response.body); + + _annotations.clear(); + + // Parse each JSON object into an Annotation and add to _annotations + for (var annotationJson in annotationsJson) { + Uri idUri = Uri.parse(annotationJson['id']); + _annotations.add(Annotation( + annotationID: int.tryParse(idUri.pathSegments.last) ?? + -1, // Adjust according to your JSON structure + startOffset: annotationJson['target']['selector']['start'], + endOffset: annotationJson['target']['selector']['end'], + annotationContent: annotationJson['body']['value'], + annotationAuthor: annotationJson['creator']['id'], + sourceLocation: annotationJson['target']['id'], + dateCreated: DateTime.parse(annotationJson['created']), + )); + print(annotationJson['body']['value']); + } + notifyListeners(); + } else { + throw Exception("Something has happened"); + } + } catch (e) { + rethrow; + } + } + + Future addAnnotation(Annotation annotation) async { + print("debugging myself"); + print(annotation.annotationContent); + print(annotation.sourceLocation); + print(annotation.annotationAuthor); + print(annotation.startOffset); + print(annotation.endOffset); + Uri url = Uri.parse("${Constants.annotationUrl}/annotations/create_annotation/"); + /* + var annotationJson = { + "@context": "http://www.w3.org/ns/anno.jsonld", + "type": "Annotation", + "body": jsonEncode({ + // Convert the body object to a JSON string + "type": "TextualBody", + "format": "text/html", + "language": "en", + "value": annotation.annotationContent, + }), + "target": jsonEncode({ + // Convert the target object to a JSON string + "id": annotation.sourceLocation, + "type": "text", + "selector": { + "type": "TextPositionSelector", + "start": annotation.startOffset, + "end": annotation.endOffset, + } + }), + "creator": jsonEncode({ + // Convert the creator object to a JSON string + "id": annotation.annotationAuthor, + "type": "Person", + }) + }; + print('Sending JSON: ${jsonEncode(annotationJson)}'); + */ + var request = http.MultipartRequest('POST', url); + +// Add each field as a string + request.fields['@context'] = "http://www.w3.org/ns/anno.jsonld"; + request.fields['type'] = "Annotation"; + request.fields['body'] = jsonEncode({ + "type": "TextualBody", + "format": "text/html", + "language": "en", + "value": annotation.annotationContent, + }); + request.fields['target'] = jsonEncode({ + "id": annotation.sourceLocation, + "type": "text", + "selector": { + "type": "TextPositionSelector", + "start": annotation.startOffset, + "end": annotation.endOffset, + } + }); + request.fields['creator'] = jsonEncode({ + "id": annotation.annotationAuthor, + "type": "Person", + }); + try { + var response = await request.send(); + print('Response Status: ${response.statusCode}'); + if (response.statusCode == 201) { + var responseBody = await response.stream.bytesToString(); + print(responseBody); + annotation.annotationID = _annotations.length + 1; + _annotations.add(annotation); + notifyListeners(); + } else { + throw Exception("Something has happened"); + } + } catch (e) { + rethrow; + } + } + + Future deleteAnnotation(Annotation annotation) async { + Uri url = + Uri.parse("${Constants.annotationUrl}/annotations/annotation/${annotation.annotationID}"); + try { + var response = await http.delete(url); + print(response.statusCode); + print(response.body); + if (response.statusCode == 204 || response.statusCode == 201 || response.statusCode == 200) { + print("Annotation deleted"); + _annotations.remove(annotation); + notifyListeners(); + } else { + throw Exception("Something has happened"); + } + } catch (e) { + rethrow; + } + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/providers/auth.dart b/project/FrontEnd/collaborative_science_platform/lib/providers/auth.dart index 75c77ca0..0b24dc07 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/providers/auth.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/providers/auth.dart @@ -6,6 +6,7 @@ import 'package:collaborative_science_platform/models/user.dart'; import 'package:collaborative_science_platform/utils/constants.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; class Auth with ChangeNotifier { User? user; @@ -42,6 +43,10 @@ class Auth with ChangeNotifier { 'Authorization': "Token $token" }; + //Add token to SharedPreferences + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString('token', token); + final tokenResponse = await http.get(url, headers: tokenHeaders); if (tokenResponse.statusCode == 200) { final userData = json.decode(tokenResponse.body); @@ -113,8 +118,63 @@ class Auth with ChangeNotifier { } } - void logout() { + Future checkTokenAndLogin() async { + // Step 1: Retrieve the token from SharedPreferences + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? token = prefs.getString('token'); + + if (token != null) { + // Step 2: Make the HTTP request with the token + Uri url = Uri.parse("${Constants.apiUrl}/get_authenticated_user/"); + final tokenHeaders = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': "Token $token", + }; + + try { + final response = await http.get(url, headers: tokenHeaders); + if (response.statusCode == 200) { + final userData = json.decode(response.body); + user = User( + id: userData['id'], + email: userData['email'], + firstName: userData['first_name'], + lastName: userData['last_name'], + token: token); + } else { + // This means that the token is not valid anymore + user = null; + //Delete it from SharedPreferences + prefs.remove('token'); + return false; + } + Uri urlBasicUser = Uri.parse("${Constants.apiUrl}/get_authenticated_basic_user/"); + + final basicUserResponse = await http.get(urlBasicUser, headers: tokenHeaders); + + if (basicUserResponse.statusCode == 200) { + final basicUserData = json.decode(basicUserResponse.body); + basicUser = BasicUser.fromJson(basicUserData); + } else { + throw Exception("Something has happened"); + } + notifyListeners(); + return true; + } catch (e) { + print("Error: $e"); + throw Exception("Something has happened"); + } + } else { + return false; + } + } + + void logout() async { user = null; + basicUser = null; + //Delete token from shared preferences + SharedPreferences.getInstance().then((prefs) => prefs.remove('token')); notifyListeners(); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/providers/node_provider.dart b/project/FrontEnd/collaborative_science_platform/lib/providers/node_provider.dart index 5532ab0e..4d161161 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/providers/node_provider.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/providers/node_provider.dart @@ -34,7 +34,7 @@ class NodeProvider with ChangeNotifier { } Future search(SearchType type, String query, - {bool random = false, bool semantic = false, bool suggestions = false}) async { + {bool random = false, bool semantic = false}) async { if (type == SearchType.author) { throw WrongSearchTypeError(); } @@ -52,26 +52,9 @@ class NodeProvider with ChangeNotifier { }; try { final response = await http.get(url, headers: headers); - if (response.statusCode == 200) { final data = json.decode(response.body); - if (suggestions) { - _youMayLikeNodeResult.clear(); - _youMayLikeNodeResult.addAll((data['nodes'] as List).map((node) => Node( - contributors: (node['authors'] as List) - .map((author) => User( - id: author['id'], - firstName: author['name'], - lastName: author['surname'], - email: author['username'])) - .toList(), - id: node['id'], - nodeTitle: node['title'], - publishDate: DateTime.parse(node['date']), - ))); - notifyListeners(); - return; - } + _searchNodeResult.clear(); _searchNodeResult.addAll((data['nodes'] as List).map((node) => Node( contributors: (node['authors'] as List) @@ -107,9 +90,47 @@ class NodeProvider with ChangeNotifier { final response = await http.get(url, headers: headers); if (response.statusCode == 200) { var data = json.decode(response.body); - data = data["suggestions"]; + _youMayLikeNodeResult.clear(); + _youMayLikeNodeResult.addAll((data['nodes'] as List).map((node) => Node( + contributors: (node['authors'] as List) + .map((author) => User( + id: author['id'], + firstName: author['name'], + lastName: author['surname'], + email: author['username'])) + .toList(), + id: node['id'], + nodeTitle: node['title'], + publishDate: DateTime.parse(node['date']), + ))); + notifyListeners(); + } else if (response.statusCode == 400) { + throw SearchError(); + } else { + if (json.decode(response.body)["message"] == "There are no nodes with this semantic tag.") { + throw SearchError(); + } + throw Exception("Error"); + } + } catch (e) { + rethrow; + } + } + + Future getRelatedNodes(int nodeId) async { + _youMayLikeNodeResult.clear(); + Uri url = Uri.parse("${Constants.apiUrl}/get_related_nodes/?node_id=$nodeId"); + final Map headers = { + "Accept": "application/json", + "content-type": "application/json" + }; + try { + final response = await http.get(url, headers: headers); + if (response.statusCode == 200) { + var data = json.decode(response.body); + data = data["nodes"]; for (var element in data) { - semanticTags.add(SemanticTag.fromJson(element)); + _youMayLikeNodeResult.add(Node.fromJson(element)); } } else if (response.statusCode == 400) { throw SearchError(); @@ -124,18 +145,22 @@ class NodeProvider with ChangeNotifier { } } - Future getNode(int id) async { + Future getNode(int id, String token) async { clearAll(); Uri url = Uri.parse("${Constants.apiUrl}/get_node/"); - if (id > -1) { url = Uri.parse("${Constants.apiUrl}/get_node/?node_id=$id"); } final Map headers = { "Accept": "application/json", - "content-type": "application/json" + "content-type": "application/json", }; + + if (token.isNotEmpty) { + headers.addAll({"Authorization": "Token $token"}); + } + try { final response = await http.get(url, headers: headers); if (response.statusCode == 200) { @@ -152,8 +177,38 @@ class NodeProvider with ChangeNotifier { } } - Future getNodeSuggestions() async { - await search(SearchType.theorem, "", random: true, suggestions: true); + Future getNodeByType(String queryType) async { + Uri url = Uri.parse("${Constants.apiUrl}/search/?type=$queryType"); + final Map headers = { + "Accept": "application/json", + "content-type": "application/json" + }; + try { + final response = await http.get(url, headers: headers); + if (response.statusCode == 200) { + final data = json.decode(response.body); + _searchNodeResult.clear(); + _searchNodeResult.addAll((data['nodes'] as List).map((node) => Node( + contributors: (node['authors'] as List) + .map((author) => User( + id: author['id'], + firstName: author['name'], + lastName: author['surname'], + email: author['username'])) + .toList(), + id: node['id'], + nodeTitle: node['title'], + publishDate: DateTime.parse(node['date']), + ))); + notifyListeners(); + } else if (response.statusCode == 400) { + throw SearchError(); + } else { + throw Exception("Error"); + } + } catch (e) { + rethrow; + } } Map searchTypeToString = { diff --git a/project/FrontEnd/collaborative_science_platform/lib/providers/profile_data_provider.dart b/project/FrontEnd/collaborative_science_platform/lib/providers/profile_data_provider.dart index fc6a4758..522dbb8f 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/providers/profile_data_provider.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/providers/profile_data_provider.dart @@ -27,6 +27,7 @@ class ProfileDataProvider with ChangeNotifier { throw Exception("Something has happened"); } } catch (e) { + print(e); rethrow; } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/providers/question_provider.dart b/project/FrontEnd/collaborative_science_platform/lib/providers/question_provider.dart new file mode 100644 index 00000000..4286557e --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/providers/question_provider.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; +import 'package:collaborative_science_platform/exceptions/question_exceptions.dart'; +import 'package:collaborative_science_platform/models/node_details_page/question.dart'; +import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/utils/constants.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +class QuestionAnswerProvider with ChangeNotifier { + final List _questions = []; + + List get questions { + return [..._questions]; + } + + Future postQuestion(String questionText, int nodeId, User user) async { + Uri url = Uri.parse("${Constants.apiUrl}/ask_question/"); + + final Map headers = { + "Accept": "application/json", + "content-type": "application/json", + "Authorization": "Token ${user.token}", + }; + final String body = json.encode({ + 'question_content': questionText, + 'node_id': nodeId, + }); + + try { + final response = await http.post( + url, + headers: headers, + body: body, + ); + if (response.statusCode == 201) { + _questions.add(Question( + id: json.decode(response.body)['QuestionID'], + answer: null, + content: questionText, + createdAt: DateTime.now().toString(), + asker: user, + answerer: null, + answeredAt: null, + nodeId: nodeId, + isAnswered: false, + isHidden: false)); + + _questions + .sort((a, b) => DateTime.parse(b.createdAt).compareTo(DateTime.parse(a.createdAt))); + notifyListeners(); + } else if (response.statusCode == 401) { + throw PostQuestionError(); + } else { + throw Exception("Error posting question"); + } + } catch (error) { + rethrow; + } + } + + Future postAnswer(String answerText, Question question, User user) async { + Uri url = Uri.parse("${Constants.apiUrl}/answer_question/"); + final Map headers = { + "Accept": "application/json", + "content-type": "application/json", + "Authorization": "Token ${user.token}", + }; + final Map postData = { + "answer_content": answerText, + "question_id": question.id, + }; + try { + final response = await http.post( + url, + headers: headers, + body: json.encode(postData), + ); + if (response.statusCode == 201) { + question.isAnswered = true; + question.answer = answerText; + question.answeredAt = DateTime.now().toString(); + question.answerer = user; + notifyListeners(); + } else if (response.statusCode == 403) { + throw PostQuestionError(); + } else { + throw Exception("Error posting answer"); + } + } catch (error) { + rethrow; + } + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/providers/settings_provider.dart b/project/FrontEnd/collaborative_science_platform/lib/providers/settings_provider.dart index f122e1da..eba3c3a6 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/providers/settings_provider.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/providers/settings_provider.dart @@ -5,52 +5,53 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; class SettingsProvider with ChangeNotifier { + Future changePassword(User? user, String oldPass, String newPass) async { final Map header = { "Accept": "application/json", "content-type": "application/json", - 'Authorization': "Token"// ${user!.token}", + 'Authorization': "Token ${user!.token}", }; + final String body = json.encode({ + 'old_password': oldPass, + 'password': newPass, + }); + try { final response = await http.put( Uri.parse("${Constants.apiUrl}/change_password/"), headers: header, - body: jsonEncode( - { - 'old_password': oldPass, - 'new_password': newPass, - }, - ), + body: body, ); print(response.statusCode); return response.statusCode; } catch (e) { + print(e); rethrow; } } - Future changePreferences(User? user, String bio, bool sendNotification, bool showActivity) async { + Future changePreferences(User? user, String bio, bool sendNotification, bool showActivity, String orcid) async { final Map header = { "Accept": "application/json", "content-type": "application/json", - 'Authorization': "Token"// ${user!.token}", + 'Authorization': "Token ${user!.token}", }; - + final String body = json.encode({ + 'bio': bio, + 'email_notification_preference': sendNotification, + 'show_activity_preference': showActivity, + 'orcid' : orcid ?? "", + }); try { final response = await http.put( Uri.parse("${Constants.apiUrl}/change_profile_settings/"), headers: header, - body: jsonEncode( - { - 'bio': bio, - 'email_notification_preference': sendNotification.toString(), - 'show_activity_preference': showActivity.toString() - }, - ), + body: body ); - print(response.statusCode); } catch (e) { + print(e); rethrow; } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/providers/user_provider.dart b/project/FrontEnd/collaborative_science_platform/lib/providers/user_provider.dart index a2d243f1..02b2efff 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/providers/user_provider.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/providers/user_provider.dart @@ -37,6 +37,7 @@ class UserProvider with ChangeNotifier { name: author['name'], surname: author['surname'], email: author['username'], + id: author['id'], ))); notifyListeners(); } else if (response.statusCode == 400) { diff --git a/project/FrontEnd/collaborative_science_platform/lib/providers/wiki_data_provider.dart b/project/FrontEnd/collaborative_science_platform/lib/providers/wiki_data_provider.dart new file mode 100644 index 00000000..49dc1c37 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/providers/wiki_data_provider.dart @@ -0,0 +1,102 @@ +import 'dart:convert'; +import 'package:collaborative_science_platform/models/semantic_tag.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +import 'package:collaborative_science_platform/utils/constants.dart'; + +class WikiDataProvider with ChangeNotifier { + List tags = []; + + Future wikiDataSearch(String query, int maxLength) async { + Uri url = Uri.parse("https://www.wikidata.org/w/api.php?action=wbsearchentities&language=en&format=json&search=$query"); + final http.Response response = await http.get(url); + try { + if (response.statusCode == 200) { + final List data = json.decode(response.body)["search"]; + final int length = (data.length < maxLength) ? data.length : maxLength; + + tags.clear(); + tags = List.generate( + length, (index) => SemanticTag( + wid: data[index]["id"], + label: data[index]["display"]["label"]["value"], + description: data[index]["display"]["description"]["value"], + ), + ); + notifyListeners(); + } else { + throw Exception("An error occurred: ${response.statusCode}"); + } + } catch (error) { + rethrow; + } + } + + Future addSemanticTag(String wid, String label, int id, String type, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/add_semantic_tag/"); + http.MultipartRequest request = http.MultipartRequest('POST', url); + + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll( (type == 'workspace') ? { + 'wid': wid, + 'label': label, + 'workspace_id': "$id", + } : { + 'wid': wid, + 'label': label, + 'user_id': "$id", + }); + + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200 || response.statusCode == 201) { + notifyListeners(); + } else { + throw Exception("Something has gone wrong"); + } + } + + Future removeSemanticTag(int workspaceId, int tagId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/remove_workspace_tag/"); + http.MultipartRequest request = http.MultipartRequest('PUT', url); + + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + 'workspace_id': "$workspaceId", + 'tag_id': "$tagId", + }); + + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200 || response.statusCode == 201) { + notifyListeners(); + } else { + throw Exception("Something has gone wrong"); + } + } + + Future removeUserSemanticTag(int tagId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/remove_user_tag/"); + http.MultipartRequest request = http.MultipartRequest('POST', url); + + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + 'tag_id': "$tagId", + }); + + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200 || response.statusCode == 201) { + notifyListeners(); + } else { + throw Exception("Something has gone wrong"); + } + } +} \ No newline at end of file diff --git a/project/FrontEnd/collaborative_science_platform/lib/providers/workspace_provider.dart b/project/FrontEnd/collaborative_science_platform/lib/providers/workspace_provider.dart index 4d15c556..e589ec2a 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/providers/workspace_provider.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/providers/workspace_provider.dart @@ -57,306 +57,582 @@ class WorkspaceProvider with ChangeNotifier { } } - Future sendCollaborationRequest( - int senderId, int receiverId, String title, + Future sendCollaborationRequest(int senderId, int receiverId, String title, String requestBody, int workspaceId, String token) async { Uri url = Uri.parse("${Constants.apiUrl}/send_collab_req/"); - final Map headers = { + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ "Authorization": "Token $token", - "content-type": "application/json" - }; - - final String body = json.encode({ - 'sender': senderId, - 'receiver': receiverId, + "content-type": "application/json", + }); + request.fields.addAll({ + 'sender': "$senderId", + 'receiver': "$receiverId", 'title': title, 'body': requestBody, - 'workspace': workspaceId + 'workspace': "$workspaceId" }); - try { - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode == 200) { - notifyListeners(); - } else if (response.statusCode == 400) { - throw SendCollaborationRequestException(); - } else { - throw Exception("Something has happened"); - } - } catch (e) { - rethrow; + http.StreamedResponse response = await request.send(); + if (response.statusCode == 201) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw SendCollaborationRequestException(); + } else { + throw Exception("Something has happened"); } } - Future updateRequest(int id, String status, String token) async { + Future updateRequest(int id, RequestStatus status, String token) async { Uri url = Uri.parse("${Constants.apiUrl}/update_req"); - final Map headers = { + String requestStatus = ""; + if (status == RequestStatus.approved) { + requestStatus = "A"; + } else if (status == RequestStatus.rejected) { + requestStatus = "R"; + } else { + requestStatus = "P"; + } + + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ "Authorization": "Token $token", - "content-type": "application/json" - }; + "content-type": "application/json", + }); + request.fields.addAll({ + 'id': "$id", + 'status': requestStatus, + }); - final String body = json.encode({'id': id, 'status': status}); + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw SendCollaborationRequestException(); + } else { + throw Exception("Something has happened"); + } + } - try { - final response = await http.put(url, headers: headers, body: body); - if (response.statusCode == 200) { - notifyListeners(); - } else if (response.statusCode == 400) { - throw SendCollaborationRequestException(); - } else { - throw Exception("Something has happened"); - } - } catch (e) { - rethrow; + Future updateReviewRequest(int id, RequestStatus status, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/update_review_req/"); + + String requestStatus = ""; + if (status == RequestStatus.approved) { + requestStatus = "A"; + } else if (status == RequestStatus.rejected) { + requestStatus = "R"; + } else { + requestStatus = "P"; + } + + var request = http.MultipartRequest('PUT', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + 'id': "$id", + 'status': requestStatus, + 'comment': "OK", + }); + print(request.fields); + http.StreamedResponse response = await request.send(); + print(response.statusCode); + print(await response.stream.bytesToString()); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw SendCollaborationRequestException(); + } else { + throw Exception("Something has happened"); } } - Future createWorkspace(String title, String token) async { - Uri url = Uri.parse("${Constants.apiUrl}/workspace_post/?format=json"); + Future updateCollaborationRequest(int id, RequestStatus status, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/update_collab_req/"); + String requestStatus = ""; + if (status == RequestStatus.approved) { + requestStatus = "A"; + } else if (status == RequestStatus.rejected) { + requestStatus = "R"; + } else { + requestStatus = "P"; + } - final Map headers = { + var request = http.MultipartRequest('PUT', url); + request.headers.addAll({ "Authorization": "Token $token", - "content-type": "application/json" - }; + "content-type": "application/json", + }); + request.fields.addAll({ + 'id': "$id", + 'status': requestStatus, + }); + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw SendCollaborationRequestException(); + } else { + throw Exception("Something has happened"); + } + } - final String body = json.encode({ - 'workspace_title': title, + Future createWorkspacefromNode(int nodeId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/create_workspace/"); + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + 'node_id': "$nodeId", + 'workspace_title': " ", }); + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw CreateWorkspaceException(); + } else if (response.statusCode == 403) { + throw WorkspacePermissionException(); + } else { + throw Exception("Something has happened"); + } + } - try { - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode == 200) { - notifyListeners(); - } else if (response.statusCode == 400) { - throw CreateWorkspaceException(); - } else if (response.statusCode == 403) { - throw WorkspacePermissionException(); - } else { - throw Exception("Something has happened"); - } - } catch (e) { - rethrow; + Future createWorkspace(String title, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/workspace_post/"); + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + 'workspace_title': title, + }); + http.StreamedResponse response = await request.send(); + if (response.statusCode == 201) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw CreateWorkspaceException(); + } else if (response.statusCode == 403) { + throw WorkspacePermissionException(); + } else { + throw Exception("Something has happened"); } } Future addSemanticTags(int id, String token, List semanticTags) async { Uri url = Uri.parse("${Constants.apiUrl}/workspace_post/"); - final Map headers = { - "Authorization": token, - "content-type": "application/json" - }; - - final String body = json.encode({ - 'workspace_id': id, - 'semantic_tags': semanticTags, + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + "workspace_id": "$id", + 'semantic_tags': "$semanticTags", // check if the data converted correctly }); - try { - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode == 200) { - notifyListeners(); - } else if (response.statusCode == 400) { - throw CreateWorkspaceException(); - } else if (response.statusCode == 403) { - throw WorkspacePermissionException(); - } else { - throw Exception("Something has happened"); - } - } catch (e) { - rethrow; + http.StreamedResponse response = await request.send(); + if (response.statusCode == 201) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw CreateWorkspaceException(); + } else if (response.statusCode == 403) { + throw WorkspacePermissionException(); + } else { + throw Exception("Something has happened"); } } Future updateWorkspaceTitle(int id, String token, String title) async { Uri url = Uri.parse("${Constants.apiUrl}/workspace_post/"); - final Map headers = { - "Authorization": token, - "content-type": "application/json" - }; - - final String body = json.encode({ - 'workspace_id': id, + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + "workspace_id": "$id", 'workspace_title': title, }); - try { - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode == 200) { - notifyListeners(); - } else if (response.statusCode == 400) { - throw CreateWorkspaceException(); - } else if (response.statusCode == 403) { - throw WorkspacePermissionException(); - } else { - throw Exception("Something has happened"); - } - } catch (e) { - rethrow; + http.StreamedResponse response = await request.send(); + if (response.statusCode == 201) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw CreateWorkspaceException(); + } else if (response.statusCode == 403) { + throw WorkspacePermissionException(); + } else { + throw Exception("Something has happened"); } } Future addReference(int workspaceId, int nodeId, String token) async { Uri url = Uri.parse("${Constants.apiUrl}/add_reference/"); - final Map headers = { + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ "Authorization": "Token $token", - "content-type": "application/json" - }; - - final String body = json.encode({ - 'workspace_id': workspaceId, - 'node_id': nodeId, + "content-type": "application/json", + }); + request.fields.addAll({ + "workspace_id": "$workspaceId", + 'node_id': "$nodeId", }); - try { - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode == 200) { - notifyListeners(); - } else if (response.statusCode == 400) { - throw AddReferenceException(); - } else { - throw Exception("Something has happened"); - } - } catch (e) { - rethrow; + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw AddReferenceException(); + } else { + throw Exception("Something has happened"); } } Future addEntry(String content, int workspaceId, String token) async { Uri url = Uri.parse("${Constants.apiUrl}/add_entry/"); - - final Map headers = { + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ "Authorization": "Token $token", - "content-type": "application/json" - }; - - final String body = json.encode({ - 'workspace_id': workspaceId, + "content-type": "application/json", + }); + request.fields.addAll({ + "workspace_id": "$workspaceId", 'entry_content': content, }); - try { - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode == 200) { - notifyListeners(); - } else if (response.statusCode == 400) { - throw AddEntryException(); - } else { - throw Exception("Something has happened"); - } - } catch (e) { - rethrow; + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw AddEntryException(); + } else { + throw Exception("Something has happened"); } } Future finalizeWorkspace(int workspaceId, String token) async { Uri url = Uri.parse("${Constants.apiUrl}/finalize_workspace/"); - final Map headers = { + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ "Authorization": "Token $token", - "content-type": "application/json" - }; + "content-type": "application/json", + }); + request.fields.addAll({ + 'workspace_id': "$workspaceId", + }); - final String body = json.encode({ - 'workspace_id': workspaceId, + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw FinalizeWorkspaceException(); + } else { + throw Exception("Something has happened"); + } + } + + Future sendWorkspaceToReview(int workspaceId, int userId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/send_rev_req/"); + + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + 'workspace': "$workspaceId", + 'sender': "$userId", }); - try { - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode == 200) { - notifyListeners(); - } else if (response.statusCode == 400) { - throw FinalizeWorkspaceException(); - } else { - throw Exception("Something has happened"); - } - } catch (e) { - rethrow; + http.StreamedResponse response = await request.send(); + if (response.statusCode == 201) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw FinalizeWorkspaceException(); + } else { + throw Exception("Something has happened"); } } Future deleteReference(int workspaceId, int nodeId, String token) async { Uri url = Uri.parse("${Constants.apiUrl}/delete_reference/"); - final Map headers = { + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ "Authorization": "Token $token", - "content-type": "application/json" - }; - - final String body = json.encode({ - 'workspace_id': workspaceId, - 'node_id': nodeId, + "content-type": "application/json", + }); + request.fields.addAll({ + 'workspace_id': "$workspaceId", + 'node_id': "$nodeId", }); - try { - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode == 200) { - notifyListeners(); - } else if (response.statusCode == 400) { - throw DeleteReferenceException(); - } else { - throw Exception("Something has happened"); - } - } catch (e) { - rethrow; + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw DeleteReferenceException(); + } else { + throw Exception("Something has happened"); } } - Future editEntry(String content, int entryId, String token) async { + Future editEntry(String content, int entryId, int workspaceId, String token) async { Uri url = Uri.parse("${Constants.apiUrl}/edit_entry/"); + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + "entry_id": "$entryId", + 'workspace_id': "$workspaceId", + 'content': content, + }); + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw EditEntryException(); + } else { + throw Exception("Something has happened"); + } + } - final Map headers = { + Future deleteEntry(int entryId, int workspaceId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/delete_entry/"); + + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ "Authorization": "Token $token", - "content-type": "application/json" - }; + "content-type": "application/json", + }); + request.fields.addAll({ + 'workspace_id': "$workspaceId", + 'entry_id': "$entryId", + }); - final String body = json.encode({ - 'entry_id': entryId, - 'content': content, + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw DeleteEntryException(); + } else { + throw Exception("Something has happened"); + } + } + + Future addReview(int id, RequestStatus status, String comment, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/update_review_req/"); + + String requestStatus = ""; + if (status == RequestStatus.approved) { + requestStatus = "A"; + } else if (status == RequestStatus.rejected) { + requestStatus = "R"; + } else { + requestStatus = "P"; + } + + var request = http.MultipartRequest('PUT', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + 'id': "$id", + 'comment': comment, + 'response': requestStatus, }); - try { - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode == 200) { - notifyListeners(); - } else if (response.statusCode == 400) { - throw EditEntryException(); - } else { - throw Exception("Something has happened"); - } - } catch (e) { - rethrow; + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw DeleteEntryException(); + } else { + throw Exception("Something has happened"); } } - Future deleteEntry(int entryId, int workspaceId, String token) async { - Uri url = Uri.parse("${Constants.apiUrl}/add_entry/"); - final Map headers = { + Future resetWorkspace(int workspaceId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/reset_workspace_state/"); + + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ "Authorization": "Token $token", - "content-type": "application/json" - }; + "content-type": "application/json", + }); + request.fields.addAll({ + 'workspace_id': "$workspaceId", + }); - final String body = json.encode({ - 'workspace_id': workspaceId, - 'entry_id': entryId, + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw WorkspaceDoesNotExist(); + } else { + throw Exception("Something has happened"); + } + } + + Future setProof(int entryId, int workspaceId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/set_workspace_proof/"); + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", }); + request.fields.addAll({ + "entry_id": "$entryId", + 'workspace_id': "$workspaceId", + }); + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw EditEntryException(); + } else { + throw Exception("Something has happened"); + } + } - try { - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode == 200) { - notifyListeners(); - } else if (response.statusCode == 400) { - throw DeleteEntryException(); - } else { - throw Exception("Something has happened"); - } - } catch (e) { - rethrow; + Future setDisproof(int entryId, int workspaceId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/set_workspace_disproof/"); + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + "entry_id": "$entryId", + 'workspace_id': "$workspaceId", + }); + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw EditEntryException(); + } else { + throw Exception("Something has happened"); + } + } + + Future removeProof(int workspaceId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/remove_workspace_proof/"); + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + 'workspace_id': "$workspaceId", + }); + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw EditEntryException(); + } else { + throw Exception("Something has happened"); + } + } + + Future removeDisproof(int workspaceId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/remove_workspace_disproof/"); + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + 'workspace_id': "$workspaceId", + }); + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw EditEntryException(); + } else { + throw Exception("Something has happened"); + } + } + + Future setTheorem(int entryId, int workspaceId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/set_workspace_theorem/"); + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + "entry_id": "$entryId", + 'workspace_id': "$workspaceId", + }); + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw EditEntryException(); + } else { + throw Exception("Something has happened"); + } + } + + Future removeTheorem(int workspaceId, String token) async { + Uri url = Uri.parse("${Constants.apiUrl}/remove_workspace_theorem/"); + var request = http.MultipartRequest('POST', url); + request.headers.addAll({ + "Authorization": "Token $token", + "content-type": "application/json", + }); + request.fields.addAll({ + 'workspace_id': "$workspaceId", + }); + http.StreamedResponse response = await request.send(); + if (response.statusCode == 200) { + //print(await response.stream.bytesToString()); + notifyListeners(); + } else if (response.statusCode == 400) { + throw EditEntryException(); + } else { + throw Exception("Something has happened"); } } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/please_login_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/please_login_page.dart index 0d8e0d8d..c9b0b856 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/please_login_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/please_login_page.dart @@ -1,9 +1,11 @@ +import 'package:collaborative_science_platform/providers/auth.dart'; import 'package:collaborative_science_platform/screens/auth_screens/widgets/please_login_signup.dart'; import 'package:collaborative_science_platform/screens/auth_screens/widgets/please_login_prompts.dart'; import 'package:collaborative_science_platform/screens/home_page/widgets/home_page_appbar.dart'; import 'package:collaborative_science_platform/screens/page_with_appbar/page_with_appbar.dart'; import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class PleaseLoginPage extends StatelessWidget { static const routeName = '/please-login'; @@ -15,6 +17,7 @@ class PleaseLoginPage extends StatelessWidget { @override Widget build(BuildContext context) { + final auth = Provider.of(context, listen: false); return PageWithAppBar( appBar: const HomePageAppBar(), child: SizedBox( @@ -30,10 +33,11 @@ class PleaseLoginPage extends StatelessWidget { if (pageType == "workspaces") WorkspaceExplanation(), if (pageType == "profile") ProfileExplanation(), if (pageType != null) - Padding( + const Padding( padding: EdgeInsets.fromLTRB(15.0, 0.0, 15.0, 0.0), child: Divider(height: 40.0), ), + if (!auth.isSignedIn) PleaseLoginSignup(), const Padding( padding: EdgeInsets.fromLTRB(15.0, 0.0, 15.0, 0.0), diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/signup_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/signup_page.dart index d019f6d6..f6098660 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/signup_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/signup_page.dart @@ -11,6 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:go_router/go_router.dart'; +import 'package:collaborative_science_platform/screens/auth_screens/widgets/privacy_policy_form.dart'; class SignUpPage extends StatefulWidget { static const routeName = '/signup'; @@ -44,6 +45,8 @@ class _SignUpPageState extends State { String errorMessage = ""; + bool isChecked = false; + @override void dispose() { emailController.dispose(); @@ -93,7 +96,8 @@ class _SignUpPageState extends State { passwordController.text, confirmPasswordController.text) && nameController.text.isNotEmpty && surnameController.text.isNotEmpty && - emailController.text.isNotEmpty) { + emailController.text.isNotEmpty && + isChecked) { setState(() { buttonState = true; }); @@ -296,7 +300,54 @@ class _SignUpPageState extends State { style: const TextStyle(color: AppColors.dangerColor), ), ), - const SizedBox(height: 10.0), + Row( + children: [ + Checkbox( + value: isChecked, + activeColor: AppColors.primaryColor, // Set the checkbox color when checked + checkColor: Colors.white, // Set the check mark color + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.0), + ), + side: MaterialStateBorderSide.resolveWith( + (states) => const BorderSide( + width: 1.2, + color: AppColors.primaryColor, + ), + ), + onChanged: (bool? value) { + setState(() { + isChecked = value!; + }); + validateStrongPassword(); + }, + ), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) => const AlertDialog( + + backgroundColor: Colors.white, + shadowColor: Colors.white, + content: PrivacyPolicyForm(), + ), + ); + }, + child: const Text( + "Accept privacy policy.", + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppColors.hyperTextColor, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 15.0), AppButton( onTap: () async { if (await authenticate() && mounted) { diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/widgets/please_login_signup.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/widgets/please_login_signup.dart index 51f81617..774b7ed5 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/widgets/please_login_signup.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/widgets/please_login_signup.dart @@ -26,12 +26,14 @@ class PleaseLoginSignup extends StatelessWidget { child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'Login', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16.0, + SelectionContainer.disabled( + child: Text( + 'Login', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16.0, + ), ), ), ], @@ -56,12 +58,14 @@ class PleaseLoginSignup extends StatelessWidget { child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'Signup', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16.0, + SelectionContainer.disabled( + child: Text( + 'Signup', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16.0, + ), ), ), ], diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/widgets/privacy_policy_form.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/widgets/privacy_policy_form.dart new file mode 100644 index 00000000..f2e4c4df --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/auth_screens/widgets/privacy_policy_form.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; + +class PrivacyPolicyForm extends StatefulWidget { + const PrivacyPolicyForm({super.key}); + + @override + State createState() => _PrivacyPolicyForm(); +} + +class _PrivacyPolicyForm extends State { + @override + Widget build(BuildContext context) { + return const SizedBox( + width: 450, + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Padding( + padding: EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Privacy Policy for Collaborative Science Platform', + style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold), + ), + Text('Updated: 23 December 2023', + style: TextStyle(fontStyle: FontStyle.italic)), + SizedBox(height:4.0), + Divider(), + SizedBox(height:6.0), + Text('Introduction', style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600)), + Text( + 'Welcome to the Collaborative Science Platform. This Privacy Policy outlines our commitment to protecting the privacy and personal information of our users. This policy applies to all information collected through our platform by "Guests," "Basic Users," "Contributors," "Reviewers," and "Admins."\n', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.normal)), + Text('Information Collection and Use', + style: TextStyle(fontSize: 21, fontWeight: FontWeight.w600)), + SizedBox(height:8.0), + Text('Personal Data', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), + Text( + '''We collect information that you provide to us directly, such as when you create an account or use our services. This may include: +- Name and Contact Data (email address) +- Passwords (stored in a secure, hashed format) +- ORCID-ID for contributors (for identity verification) +- Any other information you choose to provide +''', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + Text( + 'Usage Data', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + '''We may also collect information on how our platform is accessed and used ("Usage Data"). This includes information such as your computer's Internet Protocol address (IP address), browser type, browser version, the pages of our platform that you visit, the time and date of your visit, and other diagnostic data.\n''', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + Text( + 'Data Use', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + '''We use the collected data for various purposes: +- To provide and maintain our platform +- To notify you about changes to our platform +- To allow you to participate in interactive features when you choose to do so +- To provide customer support +- To gather analysis or valuable information so that we can improve our platform +- To monitor the usage of our platform +- To detect, prevent, and address technical issues +''', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + Text( + 'Data Sharing and Disclosure', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + '''We may disclose your Personal Data in the following situations: +- To Comply with Laws:** If we are required to disclose your information in accordance with legal or regulatory requirements. +- For Platform Administration:** To administer our platform, including troubleshooting, data analysis, testing, and research. +- For External Processing:** To our affiliates, service providers, and other trusted businesses or persons who process it for us, based on our instructions and in compliance with our Privacy Policy and other appropriate confidentiality and security measures. +''', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + Text( + 'Data Security', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'The security of your data is important to us. We strive to use commercially acceptable means to protect your Personal Data, but remember that no method of transmission over the Internet or method of electronic storage is 100% secure.\n', style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + Text( + 'Your Data Protection Rights', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'In accordance with GDPR and KVKK, you have certain data protection rights. These include the right to access, update, or delete the information we hold about you, the right of rectification, the right to object, the right of restriction, the right to data portability, and the right to withdraw consent.\n', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + Text( + 'Changes to This Privacy Policy', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. We will let you know via email and/or a prominent notice on our platform, prior to the change becoming effective and update the "effective date" at the top of this Privacy Policy.\n', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + Text( + 'Contact Us', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'If you have any questions about this Privacy Policy, please contact us.', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/error_page/error_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/error_page/error_page.dart new file mode 100644 index 00000000..bc78d9b3 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/error_page/error_page.dart @@ -0,0 +1,57 @@ +import 'package:collaborative_science_platform/screens/home_page/home_page.dart'; +import 'package:collaborative_science_platform/utils/colors.dart'; +import 'package:collaborative_science_platform/utils/text_styles.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class ErrorPage extends StatelessWidget { + static const routeName = '/page_not_found/'; + const ErrorPage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 20), + Container( + margin: const EdgeInsets.fromLTRB(15.0, 0.0, 15.0, 0.0), + child: Text( + "404", + style: TextStyles.title4.copyWith(fontSize: 120), + ), + ), + Container( + margin: const EdgeInsets.fromLTRB(15.0, 5.0, 15.0, 0.0), + child: const Text( + "This page doesn't exist.", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColors.secondaryDarkColor, + ), + ), + ), + const SizedBox(height: 8.0), // Add space between texts + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + context.go(HomePage.routeName); + }, + child: const Text( + "Return to main page", + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + color: Color.fromARGB(255, 67, 85, 186), + ), + ), + ), + ), + ], + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/graph_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/graph_page.dart index 7ddc7a84..27163d4d 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/graph_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/graph_page.dart @@ -1,5 +1,6 @@ import 'package:collaborative_science_platform/exceptions/node_details_exceptions.dart'; import 'package:collaborative_science_platform/models/node_details_page/node_detailed.dart'; +import 'package:collaborative_science_platform/providers/auth.dart'; import 'package:collaborative_science_platform/providers/node_provider.dart'; import 'package:collaborative_science_platform/screens/graph_page/mobile_graph_page.dart'; import 'package:collaborative_science_platform/screens/graph_page/web_graph_page.dart'; @@ -12,7 +13,7 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; class GraphPage extends StatefulWidget { - static const routeName = '/graph'; + static const routeName = '/relation'; final int nodeId; const GraphPage({super.key, this.nodeId = -1}); @@ -55,6 +56,7 @@ class _GraphPageState extends State { void getNode() async { try { final nodeProvider = Provider.of(context, listen: false); + final auth = Provider.of(context); setState(() { error = false; isLoading = true; @@ -67,7 +69,7 @@ class _GraphPageState extends State { return; } } - await nodeProvider.getNode(widget.nodeId); + await nodeProvider.getNode(widget.nodeId, auth.isSignedIn ? auth.user!.token : ""); setState(() { node = nodeProvider.nodeDetailed!; }); diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/mobile_graph_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/mobile_graph_page.dart index dc75ac86..709bd6e9 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/mobile_graph_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/mobile_graph_page.dart @@ -135,29 +135,27 @@ class _MobileGraphPageState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: Center( - child: SizedBox( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - child: CarouselSlider( - carouselController: controller, - items: subpages, - options: CarouselOptions( - scrollPhysics: const ScrollPhysics(), - autoPlay: false, - viewportFraction: 1.0, - enableInfiniteScroll: false, - initialPage: current, - enlargeCenterPage: true, - enlargeStrategy: CenterPageEnlargeStrategy.zoom, - enlargeFactor: 0.3, - onPageChanged: (index, reason) { - setState(() { - current = index; - }); - }, - ), + Center( + child: SizedBox( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + child: CarouselSlider( + carouselController: controller, + items: subpages, + options: CarouselOptions( + scrollPhysics: const ScrollPhysics(), + autoPlay: false, + viewportFraction: 1.0, + enableInfiniteScroll: false, + initialPage: current, + enlargeCenterPage: true, + enlargeStrategy: CenterPageEnlargeStrategy.zoom, + enlargeFactor: 0.3, + onPageChanged: (index, reason) { + setState(() { + current = index; + }); + }, ), ), ), diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/web_graph_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/web_graph_page.dart index e6b77696..bda82575 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/web_graph_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/web_graph_page.dart @@ -25,52 +25,55 @@ class _WebGraphPageState extends State { return PageWithAppBar( appBar: const HomePageAppBar(), pageColor: Colors.grey.shade200, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - widget.node.references.isEmpty - ? const Center( - child: Text( - "No references", - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + widget.node.references.isEmpty + ? const Center( + child: Text( + "No references", + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), ), + ) + : Flexible( + flex: 2, + child: NodeList( + nodes: widget.node.references, + title: "References", + width: MediaQuery.of(context).size.width / 3.2), ), - ) - : Flexible( - flex: 2, - child: NodeList( - nodes: widget.node.references, - title: "References", - width: MediaQuery.of(context).size.width / 3.2), - ), - Flexible( - flex: 6, - child: GraphPageNodeCard( - node: widget.node, - onTap: () => context.go('${NodeDetailsPage.routeName}/${widget.node.nodeId}')), - ), - widget.node.citations.isEmpty - ? const Center( - child: Text( - "No citations", - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, + Flexible( + flex: 6, + child: GraphPageNodeCard( + node: widget.node, + onTap: () => context.go('${NodeDetailsPage.routeName}/${widget.node.nodeId}')), + ), + widget.node.citations.isEmpty + ? const Center( + child: Text( + "No citations", + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + ), + ) + : Flexible( + flex: 2, + child: NodeList( + nodes: widget.node.citations, + title: "Citations", + width: MediaQuery.of(context).size.width / 5, ), ), - ) - : Flexible( - flex: 2, - child: NodeList( - nodes: widget.node.citations, - title: "Citations", - width: MediaQuery.of(context).size.width / 5, - ), - ), - ], + ], + ), ), ); } diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/widgets/graph_node_popup.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/widgets/graph_node_popup.dart index c0c1ff3a..4d6d6ecc 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/widgets/graph_node_popup.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/widgets/graph_node_popup.dart @@ -5,10 +5,10 @@ import 'dart:ui'; import 'package:collaborative_science_platform/exceptions/node_details_exceptions.dart'; import 'package:collaborative_science_platform/helpers/node_helper.dart'; import 'package:collaborative_science_platform/models/node_details_page/node_detailed.dart'; +import 'package:collaborative_science_platform/providers/auth.dart'; import 'package:collaborative_science_platform/providers/node_provider.dart'; import 'package:collaborative_science_platform/screens/graph_page/graph_page.dart'; import 'package:collaborative_science_platform/screens/node_details_page/node_details_page.dart'; -import 'package:collaborative_science_platform/widgets/annotation_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_tex/flutter_tex.dart'; import 'package:collaborative_science_platform/helpers/date_to_string.dart'; @@ -44,6 +44,7 @@ class _NodeDetailsPopupState extends State { void getNode() async { try { final nodeProvider = Provider.of(context, listen: false); + final auth = Provider.of(context); setState(() { error = false; isLoading = true; @@ -57,7 +58,7 @@ class _NodeDetailsPopupState extends State { return; } } - await nodeProvider.getNode(widget.nodeId); + await nodeProvider.getNode(widget.nodeId, auth.isSignedIn ? auth.user!.token : ""); setState(() { node = nodeProvider.nodeDetailed!; isLoading = false; @@ -87,7 +88,7 @@ class _NodeDetailsPopupState extends State { return BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: AlertDialog( - title: AnnotationText( + title: SelectableText( utf8.decode(node.nodeTitle.codeUnits), style: const TextStyle( fontWeight: FontWeight.bold, diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/widgets/graph_page_node_card.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/widgets/graph_page_node_card.dart index 78cdd1a8..c89f5233 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/widgets/graph_page_node_card.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/graph_page/widgets/graph_page_node_card.dart @@ -1,7 +1,6 @@ import 'package:collaborative_science_platform/helpers/date_to_string.dart'; import 'package:collaborative_science_platform/helpers/node_helper.dart'; import 'package:collaborative_science_platform/models/node_details_page/node_detailed.dart'; -import 'package:collaborative_science_platform/widgets/annotation_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_tex/flutter_tex.dart'; import 'dart:convert'; @@ -47,7 +46,7 @@ class GraphPageNodeCard extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.only(bottom: 8.0), - child: AnnotationText( + child: SelectableText( utf8.decode(node.nodeTitle.codeUnits), style: const TextStyle( fontWeight: FontWeight.bold, @@ -71,28 +70,26 @@ class GraphPageNodeCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - node.contributors - .map((user) => - "${user.firstName} ${user.lastName} (${user.email})") - .join("\n"), - style: const TextStyle( - fontWeight: FontWeight.w400, - fontSize: 14.0, - color: Colors.black54, - ), + ListView.builder( + shrinkWrap: true, + itemCount: node.contributors.length, + itemBuilder: (context, index) => Text( + "${node.contributors[index].firstName} ${node.contributors[index].lastName} (${node.contributors[index].email})", + maxLines: 1, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14.0, + color: Colors.black54, + overflow: TextOverflow.ellipsis, ), - SelectableText( - getDurationFromNow(node.publishDate!), - style: const TextStyle( - color: Colors.grey, - fontWeight: FontWeight.w600, - ), - ), - ], + ), + ), + SelectableText( + getDurationFromNow(node.publishDate!), + style: const TextStyle( + color: Colors.grey, + fontWeight: FontWeight.w600, + ), ), ], ) diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/home_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/home_page.dart index 00de7d9f..d2f53538 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/home_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/home_page.dart @@ -1,5 +1,6 @@ import 'package:collaborative_science_platform/exceptions/search_exceptions.dart'; import 'package:collaborative_science_platform/helpers/search_helper.dart'; +import 'package:collaborative_science_platform/helpers/select_buttons_helper.dart'; import 'package:collaborative_science_platform/models/semantic_tag.dart'; import 'package:collaborative_science_platform/providers/node_provider.dart'; import 'package:collaborative_science_platform/providers/user_provider.dart'; @@ -29,7 +30,8 @@ class _HomePageState extends State { @override void didChangeDependencies() { if (_firstTime) { - randomNodes(); + final nodeProvider = Provider.of(context, listen: false); + if (nodeProvider.searchNodeResult.isEmpty) onTypeChange(0); _firstTime = false; } super.didChangeDependencies(); @@ -41,23 +43,26 @@ class _HomePageState extends State { super.dispose(); } - void randomNodes() async { + Future onTypeChange(int index) async { + final nodeProvider = Provider.of(context, listen: false); + String type = ""; + if (index == 0) { + type = "trending"; + } else if (index == 1) { + type = "latest"; + } else if (index == 2) { + type = "most_read"; + } else if (index == 3) { + type = "random"; + } else if (index == 4) { + type = "for_you"; + } try { - final nodeProvider = Provider.of(context, listen: false); setState(() { + error = false; isLoading = true; }); - await nodeProvider.search(SearchType.both, "", random: true); - } on WrongSearchTypeError { - setState(() { - error = true; - errorMessage = WrongSearchTypeError().message; - }); - } on SearchError { - setState(() { - error = true; - errorMessage = SearchError().message; - }); + await nodeProvider.getNodeByType(type); } catch (e) { setState(() { error = true; @@ -70,7 +75,37 @@ class _HomePageState extends State { } } + // void randomNodes() async { + // try { + // final nodeProvider = Provider.of(context, listen: false); + // setState(() { + // isLoading = true; + // }); + // await nodeProvider.search(SearchType.both, "", random: true); + // } on WrongSearchTypeError { + // setState(() { + // error = true; + // errorMessage = WrongSearchTypeError().message; + // }); + // } on SearchError { + // setState(() { + // error = true; + // errorMessage = SearchError().message; + // }); + // } catch (e) { + // setState(() { + // error = true; + // errorMessage = "Something went wrong!"; + // }); + // } finally { + // setState(() { + // isLoading = false; + // }); + // } + // } + void search(String text) async { + SelectButtonsHelper.selectedIndex = -1; if (text.isEmpty) return; if (text.length < 4) return; SearchType searchType = SearchHelper.searchType; @@ -118,7 +153,7 @@ class _HomePageState extends State { setState(() { isLoading = true; }); - await nodeProvider.search(searchType, tag.id, semantic: true); + await nodeProvider.search(searchType, tag.wid, semantic: true); } on WrongSearchTypeError { setState(() { error = true; @@ -146,6 +181,7 @@ class _HomePageState extends State { return MobileHomePage( searchBarFocusNode: searchBarFocusNode, onSearch: search, + onTypeChange: onTypeChange, onSemanticSearch: semanticSearch, isLoading: isLoading, error: error, diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/mobile_home_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/mobile_home_page.dart index 9d860e7e..b8c343b3 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/mobile_home_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/mobile_home_page.dart @@ -3,6 +3,7 @@ import 'package:collaborative_science_platform/providers/node_provider.dart'; import 'package:collaborative_science_platform/providers/user_provider.dart'; import 'package:collaborative_science_platform/screens/home_page/widgets/home_page_appbar.dart'; import 'package:collaborative_science_platform/screens/home_page/widgets/node_cards.dart'; +import 'package:collaborative_science_platform/screens/home_page/widgets/select_buttons.dart'; import 'package:collaborative_science_platform/screens/home_page/widgets/user_cards.dart'; import 'package:collaborative_science_platform/screens/page_with_appbar/page_with_appbar.dart'; import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; @@ -14,6 +15,7 @@ import 'package:provider/provider.dart'; class MobileHomePage extends StatelessWidget { final FocusNode searchBarFocusNode; final Function onSearch; + final Function onTypeChange; final Function onSemanticSearch; final bool isLoading; final bool error; @@ -23,6 +25,7 @@ class MobileHomePage extends StatelessWidget { super.key, required this.searchBarFocusNode, required this.onSearch, + required this.onTypeChange, required this.onSemanticSearch, required this.isLoading, required this.error, @@ -52,6 +55,8 @@ class MobileHomePage extends StatelessWidget { desktop: SearchBarExtended(exactSearch: onSearch, semanticSearch: onSemanticSearch)), ), + const SizedBox(height: 10.0), + SelectButtons(onTypeChange), Padding( padding: const EdgeInsets.only(top: 10.0), child: isLoading diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/widgets/home_page_appbar.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/widgets/home_page_appbar.dart index 7fb7be06..3b51bd1e 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/widgets/home_page_appbar.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/widgets/home_page_appbar.dart @@ -1,37 +1,34 @@ -import 'package:collaborative_science_platform/screens/notifications_page/notifications_page.dart'; import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; -import 'package:collaborative_science_platform/screens/page_with_appbar/widgets/app_bar_button.dart'; import 'package:collaborative_science_platform/screens/page_with_appbar/widgets/profile_menu.dart'; import 'package:collaborative_science_platform/screens/page_with_appbar/widgets/app_bar_logo.dart'; import 'package:collaborative_science_platform/screens/page_with_appbar/widgets/top_navigation_bar.dart'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; class HomePageAppBar extends StatelessWidget { const HomePageAppBar({super.key}); @override Widget build(BuildContext context) { - return Responsive( - mobile: const TopNavigationBar(), + return const Responsive( + mobile: TopNavigationBar(), desktop: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: EdgeInsets.symmetric(horizontal: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const AppBarLogo(height: 50.0), - const TopNavigationBar(), + AppBarLogo(height: 50.0), + TopNavigationBar(), Row(children: [ - if (!Responsive.isMobile(context)) - AppBarButton( - icon: Icons.notifications, - text: "Notifications", - onPressed: () { - context.push(NotificationPage.routeName); - }), - const SizedBox(width: 10.0), - const ProfileMenu(), + // if (!Responsive.isMobile(context)) + // AppBarButton( + // icon: Icons.notifications, + // text: "Notifications", + // onPressed: () { + // context.push(NotificationPage.routeName); + // }), + // const SizedBox(width: 10.0), + ProfileMenu(), ]), ], ), diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/widgets/select_button.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/widgets/select_button.dart new file mode 100644 index 00000000..3158ffe4 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/widgets/select_button.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +class SelectButton extends StatefulWidget { + final int index; + final String name; + final bool selected; + final Function onPressed; + const SelectButton( + {required this.index, + required this.name, + required this.selected, + required this.onPressed, + super.key}); + + @override + State createState() => _SelectButtonState(); +} + +class _SelectButtonState extends State { + bool isHovering = false; + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (event) => setState(() => isHovering = true), + onExit: (event) => setState(() => isHovering = false), + child: GestureDetector( + onTap: () { + widget.onPressed(widget.index); + }, + child: Container( + height: 35, + width: 90, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: widget.selected + ? Colors.indigo[600] + : isHovering + ? Colors.indigo[200] + : Colors.grey[200], + ), + padding: const EdgeInsets.all(10), + margin: const EdgeInsets.only(right: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SelectionContainer.disabled( + child: Text( + widget.name, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: widget.selected ? Colors.white : Colors.grey[700]), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/widgets/select_buttons.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/widgets/select_buttons.dart new file mode 100644 index 00000000..526ad3ba --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/home_page/widgets/select_buttons.dart @@ -0,0 +1,66 @@ +import 'package:collaborative_science_platform/helpers/select_buttons_helper.dart'; +import 'package:collaborative_science_platform/screens/home_page/widgets/select_button.dart'; +import 'package:flutter/material.dart'; + +class SelectButtons extends StatefulWidget { + final Function onTypeChange; + const SelectButtons(this.onTypeChange, {super.key}); + + @override + State createState() => _SelectButtonsState(); +} + +class _SelectButtonsState extends State { + void selectOne(int index) async { + setState(() { + SelectButtonsHelper.selectedIndex = index; + }); + widget.onTypeChange(index); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SelectButton( + index: 0, + name: "Trending", + selected: 0 == SelectButtonsHelper.selectedIndex, + onPressed: selectOne), + const SizedBox(width: 10.0), + SelectButton( + index: 1, + name: "Latest", + selected: 1 == SelectButtonsHelper.selectedIndex, + onPressed: selectOne), + const SizedBox(width: 10.0), + SelectButton( + index: 2, + name: "Most Read", + selected: 2 == SelectButtonsHelper.selectedIndex, + onPressed: selectOne), + const SizedBox(width: 10.0), + SelectButton( + index: 3, + name: "Random", + selected: 3 == SelectButtonsHelper.selectedIndex, + onPressed: selectOne), + // if (Provider.of(context).isSignedIn) + // Row( + // children: [ + // const SizedBox(width: 10.0), + // SelectButton( + // index: 4, name: "For You", selected: selectedIndex == 4, onPressed: selectOne), + // ], + // ), + ], + ), + ), + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/node_details_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/node_details_page.dart index a8a6e10d..962cdd1d 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/node_details_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/node_details_page.dart @@ -1,6 +1,13 @@ import 'package:collaborative_science_platform/exceptions/node_details_exceptions.dart'; +import 'package:collaborative_science_platform/exceptions/workspace_exceptions.dart'; +import 'package:collaborative_science_platform/providers/workspace_provider.dart'; +import 'package:collaborative_science_platform/models/basic_user.dart'; import 'package:collaborative_science_platform/models/node_details_page/node_detailed.dart'; +import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/providers/auth.dart'; import 'package:collaborative_science_platform/providers/node_provider.dart'; +import 'package:collaborative_science_platform/providers/admin_provider.dart'; +import 'package:collaborative_science_platform/screens/error_page/error_page.dart'; import 'package:collaborative_science_platform/screens/home_page/widgets/home_page_appbar.dart'; import 'package:collaborative_science_platform/screens/node_details_page/widgets/contributors_list_view.dart'; import 'package:collaborative_science_platform/screens/node_details_page/widgets/node_details.dart'; @@ -26,15 +33,20 @@ class _NodeDetailsPageState extends State { ScrollController controller2 = ScrollController(); bool _isFirstTime = true; NodeDetailed node = NodeDetailed(); + BasicUser basicUser = BasicUser(); + + bool isAuthLoading = false; bool error = false; String errorMessage = ""; + String message = ""; bool isLoading = false; @override void didChangeDependencies() { if (_isFirstTime) { + getAuthUser(); getNodeDetails(); _isFirstTime = false; } @@ -44,12 +56,12 @@ class _NodeDetailsPageState extends State { void getNodeDetails() async { try { final nodeDetailsProvider = Provider.of(context); + final auth = Provider.of(context); setState(() { error = false; isLoading = true; }); - await nodeDetailsProvider.getNode(widget.nodeID); - + await nodeDetailsProvider.getNode(widget.nodeID, auth.isSignedIn ? auth.user!.token : ""); setState(() { node = (nodeDetailsProvider.nodeDetailed ?? {} as NodeDetailed); }); @@ -70,39 +82,128 @@ class _NodeDetailsPageState extends State { } } + void createNewWorkspacefromNode() async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.createWorkspacefromNode(widget.nodeID, auth.user!.token); + } on CreateWorkspaceException { + setState(() { + error = true; + errorMessage = CreateWorkspaceException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void getAuthUser() async { + final User? user = Provider.of(context).user; + if (user != null) { + try { + final auth = Provider.of(context); + basicUser = (auth.basicUser ?? {} as BasicUser); + isAuthLoading = true; + // user = (auth.user ?? {} as User); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + rethrow; + } finally { + setState(() { + isAuthLoading = false; + }); + } + } + } + + void changeNodeStatus() async { + try { + final User? admin = Provider.of(context, listen: false).user; + final adminProvider = Provider.of(context, listen: false); + await adminProvider.hideNode(admin, node, !node.isHidden); + setState(() { + node.isHidden = !node.isHidden; + }); + error = false; + message = "Node status updated."; + } catch (e) { + setState(() { + error = true; + message = "Something went wrong!"; + }); + } + } + + void handleButton() { + setState(() { + changeNodeStatus(); + }); + } + @override Widget build(BuildContext context) { - return PageWithAppBar( - appBar: const HomePageAppBar(), - pageColor: Colors.grey.shade200, - child: isLoading - ? Container( - padding: const EdgeInsets.only(top: 32), - decoration: const BoxDecoration(color: Colors.white), - child: const Center( - child: CircularProgressIndicator(), - ), - ) - : error - ? SelectableText( - errorMessage, - style: const TextStyle(color: Colors.red), - textAlign: TextAlign.center, - ) - : Responsive.isDesktop(context) - ? WebNodeDetails(node: node) - : NodeDetails( - node: node, - controller: controller2, + return (!node.isHidden || basicUser.userType == "admin") + ? PageWithAppBar( + appBar: const HomePageAppBar(), + pageColor: Colors.grey.shade200, + child: isLoading + ? Container( + padding: const EdgeInsets.only(top: 32), + decoration: const BoxDecoration(color: Colors.white), + child: const Center( + child: CircularProgressIndicator(), ), - ); + ) + : error + ? SelectableText( + errorMessage, + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ) + : Responsive.isDesktop(context) + ? WebNodeDetails( + node: node, + handleButton: handleButton, + createNewWorkspacefromNode: createNewWorkspacefromNode, + onNodeStatusChanged: () => setState(() {}), + ) + : NodeDetails( + node: node, + controller: controller2, + userType: basicUser.userType, + onTap: handleButton, + createNewWorkspacefromNode: createNewWorkspacefromNode), + ) + : const ErrorPage(); } } class WebNodeDetails extends StatefulWidget { final NodeDetailed node; - - const WebNodeDetails({super.key, required this.node}); + final Function createNewWorkspacefromNode; + final Function() handleButton; + final Function() onNodeStatusChanged; + const WebNodeDetails({ + super.key, + required this.node, + required this.handleButton, + required this.createNewWorkspacefromNode, + required this.onNodeStatusChanged, + }); @override State createState() => _WebNodeDetailsState(); @@ -111,13 +212,19 @@ class WebNodeDetails extends StatefulWidget { class _WebNodeDetailsState extends State { final ScrollController controller1 = ScrollController(); final ScrollController controller2 = ScrollController(); + BasicUser basicUser = BasicUser(); + bool _isFirstTime = true; bool error = false; bool isLoading = false; + bool isAuthLoading = false; + String errorMessage = ""; + String message = ""; @override void didChangeDependencies() { if (_isFirstTime) { + getAuthUser(); getNodeSuggestions(); _isFirstTime = false; } @@ -131,7 +238,7 @@ class _WebNodeDetailsState extends State { error = false; isLoading = true; }); - await nodeDetailsProvider.getNodeSuggestions(); + await nodeDetailsProvider.getRelatedNodes(widget.node.nodeId); } on NodeDoesNotExist { setState(() { error = true; @@ -147,6 +254,28 @@ class _WebNodeDetailsState extends State { } } + void getAuthUser() async { + final User? user = Provider.of(context).user; + if (user != null) { + try { + final auth = Provider.of(context); + basicUser = (auth.basicUser ?? {} as BasicUser); + isAuthLoading = true; + // user = (auth.user ?? {} as User); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + rethrow; + } finally { + setState(() { + isAuthLoading = false; + }); + } + } + } + @override void dispose() { controller1.dispose(); @@ -161,35 +290,60 @@ class _WebNodeDetailsState extends State { super.initState(); } + void changeNodeStatus() async { + try { + final User? user = Provider.of(context, listen: false).user; + final adminProvider = Provider.of(context, listen: false); + await adminProvider.hideNode(user, widget.node, widget.node.isHidden); + setState(() { + widget.node.isHidden = !widget.node.isHidden; + }); + widget.onNodeStatusChanged(); + error = false; + message = "Node status updated."; + } catch (e) { + setState(() { + error = true; + message = "Something went wrong!"; + }); + } + } + @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Contributors( - contributors: widget.node.contributors, //widget.inputNode.contributors, - controller: controller1, - ), - const SizedBox(width: 12), - Flexible( - child: NodeDetails( - node: widget.node, - controller: controller2, - ), - ), - const SizedBox(width: 12), - SizedBox( - height: MediaQuery.of(context).size.height - 100, - child: YouMayLike( - isLoading: isLoading, - error: error, + return (!widget.node.isHidden || basicUser.userType == "admin") + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Contributors( + contributors: widget.node.contributors, //widget.inputNode.contributors, + semanticTags: widget.node.semanticTags, + controller: controller1, + ), + const SizedBox(width: 12), + Flexible( + child: NodeDetails( + node: widget.node, + controller: controller2, + userType: basicUser.userType, + onTap: widget.handleButton, + createNewWorkspacefromNode: widget.createNewWorkspacefromNode, + ), + ), + const SizedBox(width: 12), + SizedBox( + height: MediaQuery.of(context).size.height - 100, + child: YouMayLike( + isLoading: isLoading, + error: error, + ), + ), + ], ), - ), - ], - ), - ); + ) + : const ErrorPage(); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/answer_box.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/answer_box.dart new file mode 100644 index 00000000..5e49c7aa --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/answer_box.dart @@ -0,0 +1,72 @@ +import 'package:collaborative_science_platform/exceptions/question_exceptions.dart'; +import 'package:collaborative_science_platform/models/node_details_page/question.dart'; +import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/providers/auth.dart'; +import 'package:collaborative_science_platform/providers/question_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AnswerBox extends StatefulWidget { + final Question question; + final Function() onQuestionAnswered; + const AnswerBox({Key? key, required this.question, required this.onQuestionAnswered}) + : super(key: key); + + @override + State createState() => _AnswerBoxState(); +} + +class _AnswerBoxState extends State { + TextEditingController answerController = TextEditingController(); + bool isLoading = false; + bool error = false; + String errorMessage = ''; + + void answerQuestion() async { + try { + if (answerController.text.isNotEmpty) { + final questionAnswerProvider = Provider.of(context, listen: false); + User user = Provider.of(context, listen: false).user!; + setState(() { + isLoading = true; + }); + await questionAnswerProvider.postAnswer(answerController.text, widget.question, user); + widget.onQuestionAnswered(); + answerController.clear(); + } + } on PostAnswerError { + setState(() { + error = true; + errorMessage = PostAnswerError().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + controller: answerController, + decoration: const InputDecoration(labelText: 'Your Answer'), + ), + const SizedBox(height: 16.0), + ElevatedButton( + onPressed: isLoading ? null : answerQuestion, + child: const Text('Submit Answer'), + ), + if (error) Text(errorMessage, style: const TextStyle(color: Colors.red)), + ], + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/ask_question_form.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/ask_question_form.dart new file mode 100644 index 00000000..d93213c5 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/ask_question_form.dart @@ -0,0 +1,81 @@ +import 'package:collaborative_science_platform/exceptions/question_exceptions.dart'; +import 'package:collaborative_science_platform/models/node_details_page/question.dart'; +import 'package:collaborative_science_platform/providers/auth.dart'; +import 'package:collaborative_science_platform/providers/question_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AskQuestionForm extends StatefulWidget { + final int nodeId; + final Function(Question) onQuestionPosted; + const AskQuestionForm({Key? key, required this.nodeId, required this.onQuestionPosted}) + : super(key: key); + + @override + State createState() => _AskQuestionFormState(); +} + +class _AskQuestionFormState extends State { + bool error = false; + String errorMessage = ""; + bool isLoading = false; + + TextEditingController questionController = TextEditingController(); + + void askQuestion() async { + try { + if (questionController.text.isNotEmpty) { + final questionProvider = Provider.of(context, listen: false); + Auth authProvider = Provider.of(context, listen: false); + if (authProvider.isSignedIn) { + setState(() { + isLoading = true; + }); + await questionProvider.postQuestion( + questionController.text, widget.nodeId, authProvider.user!); + widget.onQuestionPosted(questionProvider.questions.last); + questionController.clear(); + } + } + } on PostQuestionError { + setState(() { + error = true; + errorMessage = PostQuestionError().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Ask a Question', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8.0), + TextField( + controller: questionController, + decoration: const InputDecoration(labelText: 'Your Question'), + ), + const SizedBox(height: 16.0), + ElevatedButton( + onPressed: isLoading ? null : askQuestion, + child: const Text('Submit Question'), + ), + if (error) Text(errorMessage, style: const TextStyle(color: Colors.red)), + const SizedBox(height: 16.0), + ], + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/confirmation_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/confirmation_page.dart new file mode 100644 index 00000000..02f5f497 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/confirmation_page.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class ConfirmationPage extends StatelessWidget { + const ConfirmationPage({super.key}); + + @override + Widget build(BuildContext context) { + return const AlertDialog( + title: SizedBox( + width: 500, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Are you sure?', style: TextStyle(fontSize: 20.0)), + ], + ), + ), + backgroundColor: Colors.white, + shadowColor: Colors.white, + content: null, + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/contributors_list_view.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/contributors_list_view.dart index 8a2c737e..b7a8d81f 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/contributors_list_view.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/contributors_list_view.dart @@ -1,67 +1,186 @@ +import 'package:collaborative_science_platform/extensions/string_extensions.dart'; +import 'package:collaborative_science_platform/helpers/select_buttons_helper.dart'; +import 'package:collaborative_science_platform/models/semantic_tag.dart'; import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/providers/node_provider.dart'; import 'package:collaborative_science_platform/screens/profile_page/profile_page.dart'; import 'package:collaborative_science_platform/utils/colors.dart'; import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; import 'package:collaborative_science_platform/utils/text_styles.dart'; +import 'package:collaborative_science_platform/widgets/app_search_bar.dart'; import 'package:collaborative_science_platform/widgets/card_container.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; class Contributors extends StatelessWidget { final List contributors; + final List semanticTags; final ScrollController controller; - const Contributors({super.key, required this.contributors, required this.controller}); + const Contributors( + {super.key, + required this.contributors, + required this.semanticTags, + required this.controller}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 16), - child: Column( - children: [ - const Text( - "Contributors", - style: TextStyle( - color: AppColors.secondaryDarkColor, - fontSize: 20, - fontWeight: FontWeight.bold, + child: SizedBox( + width: Responsive.isDesktop(context) ? Responsive.desktopPageWidth / 4 : double.infinity, + //decoration: BoxDecoration(color: Colors.grey[200]), + child: ListView.builder( + //controller: controller, + scrollDirection: Axis.vertical, + shrinkWrap: true, + padding: const EdgeInsets.all(8), + itemCount: contributors.length + semanticTags.length + 2, + itemBuilder: (BuildContext context, int index) { + if (index == 0) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + child: const Center( + child: Text( + "Contributors", + style: TextStyle( + color: AppColors.secondaryDarkColor, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + if (index < contributors.length + 1) { + return ContributerView(contributor: contributors[index - 1]); + } else if (index == contributors.length + 1 && semanticTags.isNotEmpty) { + return Container( + margin: const EdgeInsets.only(top: 16, bottom: 8), + child: const Center( + child: Text( + "Semantic Tags", + style: TextStyle( + color: AppColors.secondaryDarkColor, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } else if (index > contributors.length + 1) { + return SemanticTagBox(semanticTag: semanticTags[index - contributors.length - 2]); + } + return const SizedBox(); + }), + ), + ); + } +} + +class ContributerView extends StatelessWidget { + const ContributerView({ + super.key, + required this.contributor, + }); + + final User contributor; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(2), + child: CardContainer( + onTap: () { + final String email = contributor.email; + final String encodedEmail = Uri.encodeComponent(email); + context.push('${ProfilePage.routeName}/$encodedEmail'); + }, + child: Column( + children: [ + SelectableText( + "${contributor.firstName} ${contributor.lastName}", + style: TextStyles.title4, ), - ), - SizedBox( - width: - Responsive.isDesktop(context) ? Responsive.desktopPageWidth / 4 : double.infinity, - //decoration: BoxDecoration(color: Colors.grey[200]), - child: ListView.builder( - controller: controller, - scrollDirection: Axis.vertical, - shrinkWrap: true, - padding: const EdgeInsets.all(8), - itemCount: contributors.length, - itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.all(2), - child: CardContainer( - onTap: () { - final String email = contributors[index].email; - final String encodedEmail = Uri.encodeComponent(email); - context.push('${ProfilePage.routeName}/$encodedEmail'); - }, - child: Column( - children: [ - SelectableText( - "${contributors[index].firstName} ${contributors[index].lastName}", - style: TextStyles.title4, - ), - SelectableText( - contributors[index].email, - style: TextStyles.bodyGrey, - ) - ], + SelectableText( + contributor.email, + style: TextStyles.bodyGrey, + ) + ], + ), + ), + ); + } +} + +class SemantigTagsListView extends StatelessWidget { + final List semanticTags; + final ScrollController controller; + const SemantigTagsListView({super.key, required this.semanticTags, required this.controller}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 16), + child: SizedBox( + width: Responsive.isDesktop(context) ? Responsive.desktopPageWidth / 4 : double.infinity, + //decoration: BoxDecoration(color: Colors.grey[200]), + child: ListView.builder( + //controller: controller, + scrollDirection: Axis.vertical, + shrinkWrap: true, + padding: const EdgeInsets.all(8), + itemCount: semanticTags.length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == 0) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + child: const Center( + child: Text( + "Semantig Tags", + style: TextStyle( + color: AppColors.secondaryDarkColor, + fontSize: 20, + fontWeight: FontWeight.bold, ), ), - ); - }), - ), - ], + ), + ); + } + return SemanticTagBox(semanticTag: semanticTags[index - 1]); + }), + ), + ); + } +} + +class SemanticTagBox extends StatelessWidget { + const SemanticTagBox({ + super.key, + required this.semanticTag, + }); + + final SemanticTag semanticTag; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(2), + child: CardContainer( + onTap: () async { + await Provider.of(context, listen: false) + .search(SearchType.both, semanticTag.wid, semantic: true); + SelectButtonsHelper.selectedIndex = -1; + context.go('/'); + }, + child: Column( + children: [ + SelectableText( + semanticTag.label.capitalize(), + style: TextStyles.title4, + ), + ], + ), ), ); } diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details.dart index e1a9a948..d45d3f34 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details.dart @@ -1,12 +1,15 @@ import 'package:collaborative_science_platform/helpers/node_helper.dart'; import 'package:collaborative_science_platform/models/node_details_page/node_detailed.dart'; +import 'package:collaborative_science_platform/providers/auth.dart'; import 'package:collaborative_science_platform/screens/graph_page/graph_page.dart'; import 'package:collaborative_science_platform/screens/node_details_page/widgets/contributors_list_view.dart'; +import 'package:collaborative_science_platform/screens/node_details_page/widgets/node_details_menu.dart'; import 'package:collaborative_science_platform/screens/node_details_page/widgets/node_details_tab_bar.dart'; import 'package:collaborative_science_platform/screens/node_details_page/widgets/proof_list_view.dart'; import 'package:collaborative_science_platform/screens/node_details_page/widgets/questions_list_view.dart'; import 'package:collaborative_science_platform/screens/node_details_page/widgets/references_list_view.dart'; import 'package:collaborative_science_platform/services/share_page.dart'; +import 'package:collaborative_science_platform/utils/constants.dart'; import 'package:collaborative_science_platform/utils/text_styles.dart'; import 'package:collaborative_science_platform/widgets/annotation_text.dart'; import 'package:collaborative_science_platform/widgets/app_button.dart'; @@ -17,14 +20,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_tex/flutter_tex.dart'; import 'package:go_router/go_router.dart'; import 'dart:convert'; +import 'package:provider/provider.dart'; class NodeDetails extends StatefulWidget { final NodeDetailed node; final ScrollController controller; + final Function createNewWorkspacefromNode; + final String userType; + final Function() onTap; + const NodeDetails({ super.key, required this.node, required this.controller, + required this.createNewWorkspacefromNode, + required this.userType, + required this.onTap, }); @override @@ -33,6 +44,24 @@ class NodeDetails extends StatefulWidget { class _NodeDetailsState extends State { int currentIndex = 0; + bool canAnswerQuestions = false; + bool canAskQuestions = false; + + @override + void initState() { + super.initState(); + canAnswer(); + } + + @override + void didUpdateWidget(covariant NodeDetails oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.node != widget.node) { + canAnswer(); + } + } + + bool showAnnotations = false; void updateIndex(int index) { setState(() { @@ -40,6 +69,17 @@ class _NodeDetailsState extends State { }); } + void canAnswer() async { + Auth authProvider = Provider.of(context, listen: false); + setState(() { + canAnswerQuestions = authProvider.isSignedIn && + authProvider.basicUser != null && + widget.node.contributors + .any((contributor) => contributor.id == authProvider.basicUser!.basicUserId); + canAskQuestions = authProvider.isSignedIn; + }); + } + @override Widget build(BuildContext context) { return Container( @@ -47,52 +87,84 @@ class _NodeDetailsState extends State { color: Colors.grey[200], ), width: Responsive.isDesktop(context) - ? Responsive.desktopPageWidth * 0.8 + ? Responsive.desktopNodePageWidth * 0.8 : Responsive.getGenericPageWidth(context), height: MediaQuery.of(context).size.height - 60, child: SingleChildScrollView( primary: false, scrollDirection: Axis.vertical, child: Column( - mainAxisAlignment: MainAxisAlignment.start, + // mainAxisAlignment: MainAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), child: CardContainer( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: Responsive.isDesktop(context) - ? const EdgeInsets.all(70.0) - : const EdgeInsets.all(10.0), - child: AnnotationText(utf8.decode(widget.node.nodeTitle.codeUnits), - textAlign: TextAlign.center, style: TextStyles.title2)), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( + child: Column( + //mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - SelectableText.rich( - TextSpan(children: [ - const TextSpan( - text: "published on ", - style: TextStyles.bodyGrey, - ), - TextSpan( - text: widget.node.publishDateFormatted, - style: TextStyles.bodyBlack, - ) - ]), - ), + NodeDetailsMenu( + createNewWorkspacefromNode: widget.createNewWorkspacefromNode) ], ), - Column( + Padding( + padding: Responsive.isDesktop(context) + ? const EdgeInsets.all(70.0) + : const EdgeInsets.all(10.0), + child: SelectableText(utf8.decode(widget.node.nodeTitle.codeUnits), + textAlign: TextAlign.center, style: TextStyles.title2)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ + SelectableText.rich( + TextSpan( + text: widget.node.publishDateFormatted, + style: TextStyles.bodyBlack, + ), + ), Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ + Visibility( + visible: widget.userType == "admin" ? true : false, + child: widget.node.isHidden + ? SizedBox( + width: 110, + child: AppButton( + text: "Show", + height: 40, + icon: const Icon( + CupertinoIcons.eye, + size: 16, + color: Colors.white, + ), + type: "grey", + onTap: widget.onTap, + ), + ) + : SizedBox( + width: 110, + child: AppButton( + text: "Hide", + height: 40, + icon: const Icon( + CupertinoIcons.eye_slash, + size: 16, + color: Colors.white, + ), + type: "danger", + onTap: widget.onTap, + ), + ), + ), + const SizedBox(width: 10), SizedBox( - width: 110, + width: 135, child: AppButton( - text: "Graph", + text: "Relations", height: 40, icon: const Icon( CupertinoIcons.square_grid_3x2, @@ -123,9 +195,9 @@ class _NodeDetailsState extends State { ) ], ), - ]), - ], - )), + ], + ), + ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), @@ -144,24 +216,42 @@ class _NodeDetailsState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + width: 180, + child: AppButton( + text: showAnnotations ? "Show Text" : "Show Annotations", + height: 40, + onTap: () { + setState(() { + showAnnotations = !showAnnotations; + }); + })), + ], + ), + const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: TeXView( - renderingEngine: const TeXViewRenderingEngine.katex(), - child: TeXViewDocument( - NodeHelper.getNodeContentLatex(widget.node, "long")))), + child: showAnnotations + ? AnnotationText( + NodeHelper.getNodeContentLatex(widget.node, "long"), + "${Constants.appUrl}/node/${widget.node.nodeId}#theorem", + widget.node.contributors + .map((user) => "${Constants.appUrl}/profile/${user.email}") + .toList(), + ) + : TeXView( + renderingEngine: const TeXViewRenderingEngine.katex(), + child: TeXViewDocument( + NodeHelper.getNodeContentLatex(widget.node, "long")))), SelectableText.rich( textAlign: TextAlign.start, - TextSpan(children: [ - const TextSpan( - text: "published on ", - style: TextStyles.bodyGrey, - ), - TextSpan( - text: widget.node.publishDateFormatted, - style: TextStyles.bodyBlack, - ) - ]), + TextSpan( + text: widget.node.publishDateFormatted, + style: TextStyles.bodyBlack, + ), ), ], )), @@ -170,7 +260,10 @@ class _NodeDetailsState extends State { //proofs Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: ProofListView(proof: widget.node.proof), + child: ProofListView( + proof: widget.node.proof, + contributors: widget.node.contributors, + nodeID: widget.node.nodeId), ), if (currentIndex == 2) Padding( @@ -187,14 +280,27 @@ class _NodeDetailsState extends State { //Q/A Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: QuestionsView(questions: widget.node.questions), + child: QuestionsView( + questions: widget.node.questions, + nodeId: widget.node.nodeId, + canAnswer: canAnswerQuestions, + canAsk: canAskQuestions, + isAdmin: (widget.userType == "admin"), + ), ), if (currentIndex == 5) //contributors - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: Contributors( - contributors: widget.node.contributors, controller: widget.controller)), + Contributors( + contributors: widget.node.contributors, + controller: widget.controller, + semanticTags: const [], + ), + if (currentIndex == 6) + //contributors + SemantigTagsListView( + controller: widget.controller, + semanticTags: widget.node.semanticTags, + ), ], ), ), diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details_menu.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details_menu.dart new file mode 100644 index 00000000..4fac89d4 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details_menu.dart @@ -0,0 +1,60 @@ +import 'package:collaborative_science_platform/providers/auth.dart'; +import 'package:collaborative_science_platform/screens/page_with_appbar/widgets/app_bar_button.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class NodeDetailsMenu extends StatelessWidget { + final Function createNewWorkspacefromNode; + const NodeDetailsMenu({super.key, required this.createNewWorkspacefromNode}); + + @override + Widget build(BuildContext context) { + final auth = Provider.of(context); + if (auth.basicUser != null) { + return (auth.basicUser!.userType == "contributor" || auth.basicUser!.userType == "reviewer") + ? AuthenticatedNodeDetailsMenu( + createNewWorkspacefromNode: createNewWorkspacefromNode, + ) + : const SizedBox(); + } else { + return const SizedBox(); + } + + + } +} + +class AuthenticatedNodeDetailsMenu extends StatelessWidget { + final Function createNewWorkspacefromNode; + final GlobalKey> _popupNodeMenu = GlobalKey(); + AuthenticatedNodeDetailsMenu({super.key, required this.createNewWorkspacefromNode}); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + key: _popupNodeMenu, + position: PopupMenuPosition.under, + color: Colors.grey[200], + onSelected: (String result) async { + switch (result) { + case 'create': + // Create new node TODO + await createNewWorkspacefromNode(); + break; + default: + } + }, + child: AppBarButton( + icon: Icons.more_horiz, + text: Provider.of(context).user!.firstName, + onPressed: () => _popupNodeMenu.currentState!.showButtonMenu(), + ), + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: 'create', + child: Text("Create new workspace with this theorem."), + ), + ], + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details_nav_bar_item.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details_nav_bar_item.dart index 948e901e..6582336c 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details_nav_bar_item.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details_nav_bar_item.dart @@ -52,20 +52,22 @@ class _NavigationBarItemState extends State { if (!Responsive.isMobile(context)) Padding( padding: const EdgeInsets.only(top: 4.0), - child: Text( - widget.text, - style: TextStyle( - fontSize: 16, - fontWeight: widget.isSelected - ? FontWeight.w700 - : isHovering - ? FontWeight.w600 - : FontWeight.w500, - color: widget.isSelected - ? AppColors.secondaryColor - : isHovering - ? Colors.indigo[200] - : Colors.grey[700], + child: SelectionContainer.disabled( + child: Text( + widget.text, + style: TextStyle( + fontSize: 16, + fontWeight: widget.isSelected + ? FontWeight.w700 + : isHovering + ? FontWeight.w600 + : FontWeight.w500, + color: widget.isSelected + ? AppColors.secondaryColor + : isHovering + ? Colors.indigo[200] + : Colors.grey[700], + ), ), ), ), diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details_tab_bar.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details_tab_bar.dart index 21e2f77d..0f43debd 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details_tab_bar.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/node_details_tab_bar.dart @@ -73,6 +73,14 @@ class _NodeDetailsTabBarState extends State { isSelected: currentIndex == 5, text: "Contributors", ), + if (!Responsive.isDesktop(context)) + NavigationBarItem( + callback: updateIndex, + icon: Icons.tag, + index: 6, + isSelected: currentIndex == 6, + text: "Contributors", + ), ], )); } diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/proof_list_view.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/proof_list_view.dart index 148714fd..5ddde975 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/proof_list_view.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/proof_list_view.dart @@ -1,78 +1,139 @@ import 'package:collaborative_science_platform/models/node_details_page/proof.dart'; +import 'package:collaborative_science_platform/models/user.dart'; import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; import 'package:collaborative_science_platform/utils/text_styles.dart'; +import 'package:collaborative_science_platform/utils/constants.dart'; +import 'package:collaborative_science_platform/widgets/annotation_text.dart'; +import 'package:collaborative_science_platform/widgets/app_button.dart'; import 'package:collaborative_science_platform/widgets/card_container.dart'; import 'package:flutter/material.dart'; import 'package:flutter_tex/flutter_tex.dart'; import 'dart:convert'; -class ProofListView extends StatelessWidget { +class ProofListView extends StatefulWidget { final List proof; - const ProofListView({super.key, required this.proof}); + final List contributors; + final int nodeID; + const ProofListView( + {super.key, required this.proof, required this.contributors, required this.nodeID}); + @override + State createState() => _ProofListViewState(); +} + +class _ProofListViewState extends State { + bool showAnnotations = false; @override Widget build(BuildContext context) { return Container( - width: Responsive.desktopPageWidth, + width: Responsive.desktopPageWidth - 50, decoration: BoxDecoration(color: Colors.grey[200]), - child: ListView.builder( - scrollDirection: Axis.vertical, - shrinkWrap: true, - padding: const EdgeInsets.all(8), - itemCount: proof.length, - itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.all(5), - child: CardContainer( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Text( - // proof[index].isDisproof ? "Disproof" : "Proof", - // style: TextStyles.bodyGrey, - // textAlign: TextAlign.start, - // ), - // Text( - // proof[index].proofTitle, - // style: TextStyles.title4, - // textAlign: TextAlign.start, - // ), - TeXView( - renderingEngine: const TeXViewRenderingEngine.katex(), - child: TeXViewDocument(utf8.decode(proof[index].proofContent.codeUnits))), - - // Row( - // mainAxisAlignment: MainAxisAlignment.end, - // crossAxisAlignment: CrossAxisAlignment.end, - // children: [ - // Icon( - // proof[index].isValid ? Icons.check : Icons.clear, - // color: proof[index].isValid ? AppColors.successColor : AppColors.dangerColor, - // ), - // Text( - // proof[index].isValid ? "valid" : "invalid", - // style: TextStyles.bodyGrey, - // textAlign: TextAlign.end, - // ), - // ], - // ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - proof[index].publishDate.toString(), - style: TextStyles.bodyGrey, - textAlign: TextAlign.end, - ) - ], - ), - ], + child: SingleChildScrollView( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + width: 180, + child: AppButton( + text: showAnnotations ? "Show Text" : "Show Annotations", + height: 40, + onTap: () { + setState(() { + showAnnotations = !showAnnotations; + }); + }), ), + ], + ), + showAnnotations + ? ProofItemWidget( + proof: widget.proof, contributors: widget.contributors, nodeID: widget.nodeID) + : ProofTexView(proof: widget.proof), + ], + ), + ), + ); + } +} + +class ProofTexView extends StatelessWidget { + final List proof; + const ProofTexView({ + super.key, + required this.proof, + }); + + @override + Widget build(BuildContext context) { + return TeXView( + renderingEngine: const TeXViewRenderingEngine.katex(), + child: TeXViewColumn( + children: [ + for (int i = 0; i < proof.length; i++) + TeXViewContainer( + child: TeXViewDocument( + proof[i].proofContent, + ), + style: const TeXViewStyle( + margin: TeXViewMargin.all(10), + elevation: 5, + padding: TeXViewPadding.all(16), + borderRadius: TeXViewBorderRadius.all(5), + backgroundColor: Colors.white, + ), + ), + ], + ), + ); + } +} + +class ProofItemWidget extends StatelessWidget { + final List proof; + final List contributors; + final int nodeID; + + const ProofItemWidget( + {Key? key, required this.proof, required this.contributors, required this.nodeID}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + for (int i = 0; i < proof.length; i++) + Padding( + padding: const EdgeInsets.all(5), + child: CardContainer( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnnotationText( + utf8.decode(proof[i].proofContent.codeUnits), + "${Constants.appUrl}/node/$nodeID#proof#$i", + contributors + .map((user) => "${Constants.appUrl}/profile/${user.email}") + .toList(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + proof[i].publishDate.toString(), + style: TextStyles.bodyGrey, + textAlign: TextAlign.end, + ) + ], + ), + ], ), - ); - }), + ), + ), + ], ); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/question_box.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/question_box.dart new file mode 100644 index 00000000..849c1117 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/question_box.dart @@ -0,0 +1,128 @@ +import 'package:collaborative_science_platform/models/node_details_page/question.dart'; +import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/providers/auth.dart'; +import 'package:collaborative_science_platform/screens/node_details_page/widgets/answer_box.dart'; +import 'package:collaborative_science_platform/widgets/app_button.dart'; +import 'package:collaborative_science_platform/providers/admin_provider.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class QuestionBox extends StatefulWidget { + final Question question; + final bool canAnswer; + final bool isAdmin; + final Function() onTap; + + const QuestionBox({ + super.key, + required this.question, + required this.canAnswer, + required this.isAdmin, + required this.onTap, + }); + + @override + State createState() => _QuestionBoxState(); +} + +class _QuestionBoxState extends State { + bool isHidden = false; + bool isReplyVisible = false; + TextEditingController answerController = TextEditingController(); + + void changeQuestionStatus() async { + try { + final User? admin = Provider.of(context, listen: false).user; + final adminProvider = Provider.of(context, listen: false); + int response = + await adminProvider.hideQuestion(admin, widget.question, !widget.question.isHidden); + setState(() { + widget.question.isHidden = !widget.question.isHidden; + }); + } catch (e) { + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.all(16.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Q: ${widget.question.content}', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8.0), + Text('Asked by: ${widget.question.asker.email} at ${widget.question.createdAt}'), + if (widget.question.isAnswered) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8.0), + Text( + 'A: ${widget.question.answer}', + style: const TextStyle(fontSize: 16), + ), + ], + ), + if (widget.question.isAnswered == false && widget.canAnswer) + Column( + children: [ + const SizedBox(height: 8.0), + Align( + alignment: Alignment.centerLeft, + child: ElevatedButton( + onPressed: () { + setState(() { + isReplyVisible = !isReplyVisible; + }); + }, + child: Text(isReplyVisible ? 'Hide Reply' : 'Reply'), + ), + ), + if (isReplyVisible) + AnswerBox( + question: widget.question, + onQuestionAnswered: () => setState(() {}), + ), + ], + ), + Visibility( + visible: widget.isAdmin, // Visible only to admin + child: Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(right: 4.0), // Reduced right padding + child: SizedBox( + width: 100, // Further reduced width + height: 30, // Adjusted height if needed + child: AppButton( + text: widget.question.isHidden ? "Show" : "Hide", + height: 30, // Adjusted height + icon: Icon( + widget.question.isHidden ? CupertinoIcons.eye : CupertinoIcons.eye_slash, + size: 16, + color: Colors.white, + ), + type: widget.question.isHidden ? "grey" : "danger", + onTap: () { + changeQuestionStatus(); + widget.onTap(); + }, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/questions_list_view.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/questions_list_view.dart index c57d1069..c263a691 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/questions_list_view.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/node_details_page/widgets/questions_list_view.dart @@ -1,61 +1,92 @@ -import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; -import 'package:collaborative_science_platform/utils/text_styles.dart'; -import 'package:collaborative_science_platform/widgets/card_container.dart'; import 'package:flutter/material.dart'; +import 'package:collaborative_science_platform/models/node_details_page/question.dart'; +import 'package:collaborative_science_platform/screens/node_details_page/widgets/question_box.dart'; +import 'package:collaborative_science_platform/screens/node_details_page/widgets/ask_question_form.dart'; -import '../../../models/node_details_page/question.dart'; - -class QuestionsView extends StatelessWidget { +class QuestionsView extends StatefulWidget { + final int nodeId; final List questions; - const QuestionsView({super.key, required this.questions}); + final bool canAnswer; + final bool canAsk; + final bool isAdmin; + + const QuestionsView({ + Key? key, + required this.nodeId, + required this.questions, + required this.canAnswer, + required this.canAsk, + required this.isAdmin, + }) : super(key: key); + + @override + State createState() => _QuestionsViewState(); +} + +class _QuestionsViewState extends State { + List questions = []; + bool isVisible = true; + + @override + void initState() { + super.initState(); + setState(() { + questions = widget.questions; + }); + } + + void updateVisibility() { + setState(() { + isVisible = !isVisible; + questions = widget.questions; + }); + } @override Widget build(BuildContext context) { - return Container( - width: Responsive.desktopPageWidth, - decoration: BoxDecoration(color: Colors.grey[200]), - child: ListView.builder( - scrollDirection: Axis.vertical, - shrinkWrap: true, - padding: const EdgeInsets.all(8), - itemCount: questions.length, - itemBuilder: (BuildContext context, int index) { - if (Responsive.isDesktop(context)) { - return Padding( - padding: const EdgeInsets.all(5), - child: CardContainer( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - "Q: ${questions[index].content}", - style: TextStyles.title4black, - textAlign: TextAlign.start, - ), - SelectableText( - "asked by ${questions[index].asker} at ${questions[index].createdAt}", - style: TextStyles.bodyGrey, - textAlign: TextAlign.end, - ), - SelectableText( - "A: ${questions[index].answer}", - style: TextStyles.bodyBlack, - textAlign: TextAlign.start, - ), - SelectableText( - "answered by ${questions[index].answerer} at ${questions[index].answeredAt}", - style: TextStyles.bodyGrey, - textAlign: TextAlign.end, - ), - ], - ), - ), - ); - } else { - return const SizedBox(); - } - }), + List filteredQuestions = questions.where((question) { + return question.isAnswered || widget.canAnswer || widget.isAdmin; + }).toList(); + filteredQuestions + .sort((a, b) => DateTime.parse(b.createdAt).compareTo(DateTime.parse(a.createdAt))); + + return SingleChildScrollView( + child: Container( + width: double.infinity, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.canAsk) + AskQuestionForm( + nodeId: widget.nodeId, + onQuestionPosted: (Question newQuestion) { + setState(() { + questions.add(newQuestion); + }); + }, + ), + Flexible( + child: ListView.builder( + shrinkWrap: true, + itemCount: filteredQuestions.length, + itemBuilder: (BuildContext context, int index) { + return Visibility( + visible: + isVisible || widget.isAdmin, //visible if it is not hidden or user is admin + child: QuestionBox( + isAdmin: widget.isAdmin, + question: filteredQuestions[index], + canAnswer: widget.canAnswer, + onTap: updateVisibility, + ), + ); + }, + ), + ), + ], + ), + ), ); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/page_with_appbar/widgets/profile_menu.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/page_with_appbar/widgets/profile_menu.dart index e895fa44..99cb1e64 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/page_with_appbar/widgets/profile_menu.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/page_with_appbar/widgets/profile_menu.dart @@ -1,6 +1,7 @@ import 'package:collaborative_science_platform/providers/auth.dart'; import 'package:collaborative_science_platform/screens/auth_screens/login_page.dart'; import 'package:collaborative_science_platform/screens/auth_screens/signup_page.dart'; +import 'package:collaborative_science_platform/screens/home_page/home_page.dart'; import 'package:collaborative_science_platform/screens/profile_page/profile_page.dart'; import 'package:collaborative_science_platform/services/screen_navigation.dart'; import 'package:collaborative_science_platform/screens/page_with_appbar/widgets/app_bar_button.dart'; @@ -39,6 +40,7 @@ class AuthenticatedProfileMenu extends StatelessWidget { context.push('${ProfilePage.routeName}/$encodedEmail'); break; case 'logout': + context.go(HomePage.routeName); auth.logout(); break; default: diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/page_with_appbar/widgets/top_navigation_bar.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/page_with_appbar/widgets/top_navigation_bar.dart index 0907a802..d8a93af8 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/page_with_appbar/widgets/top_navigation_bar.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/page_with_appbar/widgets/top_navigation_bar.dart @@ -25,7 +25,7 @@ class TopNavigationBar extends StatelessWidget { NavigationBarItem( icon: Icons.graphic_eq, value: ScreenTab.graph, - text: "Graph", + text: "Relations", isSelected: screenNavigation.selectedTab == ScreenTab.graph, ), NavigationBarItem( @@ -34,13 +34,13 @@ class TopNavigationBar extends StatelessWidget { isSelected: screenNavigation.selectedTab == ScreenTab.workspaces, text: "Workspaces", ), - if (Responsive.isMobile(context)) - NavigationBarItem( - icon: Icons.notifications, - value: ScreenTab.notifications, - isSelected: screenNavigation.selectedTab == ScreenTab.notifications, - text: "Notifications", - ), + // if (Responsive.isMobile(context)) + // NavigationBarItem( + // icon: Icons.notifications, + // value: ScreenTab.notifications, + // isSelected: screenNavigation.selectedTab == ScreenTab.notifications, + // text: "Notifications", + // ), if (Responsive.isMobile(context)) NavigationBarItem( icon: Icons.person, @@ -119,20 +119,22 @@ class _NavigationBarItemState extends State { if (!Responsive.isMobile(context)) Padding( padding: const EdgeInsets.only(top: 4.0), - child: Text( - widget.text, - style: TextStyle( - fontSize: 16, - fontWeight: widget.isSelected - ? FontWeight.w700 - : isHovering - ? FontWeight.w600 - : FontWeight.w500, - color: widget.isSelected - ? Colors.indigo[600] - : isHovering - ? Colors.indigo[200] - : Colors.grey[700], + child: SelectionContainer.disabled( + child: Text( + widget.text, + style: TextStyle( + fontSize: 16, + fontWeight: widget.isSelected + ? FontWeight.w700 + : isHovering + ? FontWeight.w600 + : FontWeight.w500, + color: widget.isSelected + ? Colors.indigo[600] + : isHovering + ? Colors.indigo[200] + : Colors.grey[700], + ), ), ), ), diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/change_password_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/change_password_page.dart deleted file mode 100644 index c006ac87..00000000 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/change_password_page.dart +++ /dev/null @@ -1,20 +0,0 @@ -//import 'package:collaborative_science_platform/models/user.dart'; -//import 'package:collaborative_science_platform/screens/page_with_appbar/page_with_appbar.dart'; -//import 'package:collaborative_science_platform/screens/profile_page/widgets/change_password_form.dart'; -//import 'package:collaborative_science_platform/widgets/simple_app_bar.dart'; -//import 'package:flutter/material.dart'; -// -//class ChangePasswordPage extends StatelessWidget { -// final User user; -// static const routeName = '/change-password'; -// const ChangePasswordPage({super.key, required this.user}); -// -// @override -// Widget build(BuildContext context) { -// return const PageWithAppBar( -// appBar: SimpleAppBar(title: "Account Settings"), -// child: ChangePasswordForm(user: widget.user), -// ); -// } -//} - diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/profile_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/profile_page.dart index adc8b502..6fa30b2d 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/profile_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/profile_page.dart @@ -1,8 +1,11 @@ import 'package:collaborative_science_platform/exceptions/profile_page_exceptions.dart'; +import 'package:collaborative_science_platform/models/basic_user.dart'; import 'package:collaborative_science_platform/models/profile_data.dart'; import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/providers/admin_provider.dart'; import 'package:collaborative_science_platform/providers/auth.dart'; import 'package:collaborative_science_platform/providers/profile_data_provider.dart'; +import 'package:collaborative_science_platform/screens/error_page/error_page.dart'; import 'package:collaborative_science_platform/screens/home_page/widgets/home_page_appbar.dart'; import 'package:collaborative_science_platform/screens/node_details_page/node_details_page.dart'; import 'package:collaborative_science_platform/screens/page_with_appbar/page_with_appbar.dart'; @@ -18,6 +21,7 @@ import 'package:collaborative_science_platform/widgets/card_container.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:go_router/go_router.dart'; +import 'package:collaborative_science_platform/providers/wiki_data_provider.dart'; class ProfilePage extends StatefulWidget { static const routeName = '/profile'; @@ -29,18 +33,26 @@ class ProfilePage extends StatefulWidget { State createState() => _ProfilePageState(); } -// TODO: add optional parameter to ProfilePage to get others profileData class _ProfilePageState extends State { + ProfileDataProvider profileDataProvider = ProfileDataProvider(); ProfileData profileData = ProfileData(); + BasicUser basicUser = BasicUser(); int noWorks = 0; bool error = false; String errorMessage = ""; - bool isLoading = false; + bool isLoading = false; + bool isAuthLoading = false; bool _isFirstTime = true; - int currentIndex = 0; + int response = -1; // response status code of demote/promote + String newUserType = ""; //new user type according to response + + bool isAdmin = false; + int response_isBanned = -1; + bool isBanned = false; + void updateIndex(int index) { setState(() { currentIndex = index; @@ -51,6 +63,7 @@ class _ProfilePageState extends State { void didChangeDependencies() { if (_isFirstTime) { try { + getAuthUser(); getUserData(); } catch (e) { setState(() { @@ -63,10 +76,10 @@ class _ProfilePageState extends State { super.didChangeDependencies(); } - void getUserData() async { + Future getUserData() async { try { if (widget.email != "") { - final profileDataProvider = Provider.of(context); + profileDataProvider = Provider.of(context); setState(() { isLoading = true; }); @@ -74,14 +87,18 @@ class _ProfilePageState extends State { setState(() { profileData = (profileDataProvider.profileData ?? {} as ProfileData); noWorks = profileData.nodes.length; + newUserType = profileData.userType; + isBanned = profileData.isBanned; }); } else { final User user = Provider.of(context).user!; - final profileDataProvider = Provider.of(context); + profileDataProvider = Provider.of( + context); //This is wrong? It seems like we are taking current users data await profileDataProvider.getData(user.email); setState(() { profileData = (profileDataProvider.profileData ?? {} as ProfileData); noWorks = profileData.nodes.length; + newUserType = profileData.userType; }); } } on ProfileDoesNotExist { @@ -101,238 +118,427 @@ class _ProfilePageState extends State { } } + void getAuthUser() async { + final User? user = Provider.of(context).user; + if (user != null) { + try { + final auth = Provider.of(context); + basicUser = (auth.basicUser ?? {} as BasicUser); + isAuthLoading = true; + if (basicUser.userType == "admin") { + isAdmin = true; + } + // user = (auth.user ?? {} as User); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + rethrow; + } finally { + setState(() { + isAuthLoading = false; + }); + } + } + } + + void addUserSemanticTag(String wikiId, String label) async { + try { + final auth = Provider.of(context, listen: false); + final wikiDataProvider = Provider.of(context, listen: false); + final profileDataProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await wikiDataProvider.addSemanticTag(wikiId, label, auth.basicUser!.basicUserId, 'user', auth.user!.token); + await profileDataProvider.getData(widget.email); + setState(() { + profileData = (profileDataProvider.profileData ?? {} as ProfileData); + noWorks = profileData.nodes.length; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = e.toString(); + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void removeUserSemanticTag(int tagId) async { + try { + final auth = Provider.of(context, listen: false); + final wikiDataProvider = Provider.of(context, listen: false); + final profileDataProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await wikiDataProvider.removeUserSemanticTag(tagId, auth.user!.token); + await profileDataProvider.getData(widget.email); + setState(() { + profileData = (profileDataProvider.profileData ?? {} as ProfileData); + noWorks = profileData.nodes.length; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = e.toString(); + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + Future changeProfileStatus() async { + try { + final User? admin = Provider.of(context, listen: false).user; + final adminProvider = Provider.of(context, listen: false); + + adminProvider.banUser(admin, profileData.id, !profileData.isBanned); + setState(() { + profileData.isBanned = !profileData.isBanned; + isBanned = !isBanned; + }); + error = false; + var message = "Profile status updated."; + print(message); + } catch (e) { + setState(() { + error = true; + var message = "Something went wrong!"; + print(message); + }); + } + } + + void handleButtonIsBanned() async { + await changeProfileStatus(); + } + + Future promoteUser() async { + try { + final User? admin = Provider.of(context, listen: false).user; + final adminProvider = Provider.of(context, listen: false); + response = await adminProvider.promoteUser(admin, profileData.id); + + error = false; + } catch (e) { + setState(() { + error = true; + var message = "Something went wrong!"; + print(message); + }); + } + } + + Future demoteUser() async { + try { + final User? admin = Provider.of(context, listen: false).user; + final adminProvider = Provider.of(context, listen: false); + response = await adminProvider.demoteUser(admin, profileData.id); + + error = false; + } catch (e) { + setState(() { + error = true; + var message = "Something went wrong!"; + print(message); + }); + } + } + + void handleButtonIsReviewer() async { + if (newUserType == "contributor") { + await promoteUser(); + if (response == 201) { + setState(() { + newUserType = "reviewer"; + }); + } + } else if (newUserType == "reviewer") { + await demoteUser(); + if (response == 204) { + setState(() { + newUserType = "contributor"; + }); + } + } + response = -1; + } + @override Widget build(BuildContext context) { final User? user = Provider.of(context).user; + var asked = profileData.askedQuestions.where((element) => element.isAnswered).toList(); + var answered = profileData.answeredQuestions.where((element) => element.isAnswered).toList(); + var questionList = asked + answered; if (user == null) { // guest can see profile pages } else if (user.email == profileData.email) { // own profile page, should be editible - return PageWithAppBar( - appBar: const HomePageAppBar(), - pageColor: Colors.grey.shade200, - child: Responsive( - mobile: SingleChildScrollView( - child: SizedBox( - width: Responsive.getGenericPageWidth(context), - child: isLoading - ? Container( - decoration: const BoxDecoration(color: Colors.white), - child: const Center( - child: CircularProgressIndicator(), - ), - ) - : error - ? SelectableText( - errorMessage, - style: const TextStyle(color: Colors.red), - textAlign: TextAlign.center, - ) - : Column( - children: [ - AboutMe( - aboutMe: profileData.aboutMe, - email: profileData.email, - name: profileData.name, - surname: profileData.surname, - noWorks: noWorks, + return (!profileData.isBanned || isAdmin) + ? PageWithAppBar( + appBar: const HomePageAppBar(), + pageColor: Colors.grey.shade200, + child: Responsive( + mobile: SingleChildScrollView( + child: SizedBox( + width: Responsive.getGenericPageWidth(context), + child: isLoading && isAuthLoading + ? Container( + decoration: const BoxDecoration(color: Colors.white), + child: const Center( + child: CircularProgressIndicator(), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10), - child: Row( + ) + : error + ? SelectableText( + errorMessage, + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ) + : Column( children: [ - const Expanded(child: MobileEditProfileButton()), - Expanded(child: LogOutButton()) + AboutMe( + profileData: profileData, + tags: profileData.tags, + addUserSemanticTag: addUserSemanticTag, + removeUserSemanticTag: removeUserSemanticTag, + noWorks: noWorks, + userType: basicUser.userType, + newUserType: newUserType, + onTap: handleButtonIsBanned, + onTapReviewerButton: handleButtonIsReviewer, + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 30, vertical: 10), + child: Row( + children: [ + const Expanded(child: MobileEditProfileButton()), + Expanded(child: LogOutButton()) + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: ProfileActivityTabBar( + callback: updateIndex, + ), + ), + if (currentIndex == 0) + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: CardContainer( + child: ListView.builder( + padding: const EdgeInsets.all(0), + scrollDirection: Axis.vertical, + shrinkWrap: true, + itemCount: profileData.nodes.length, + itemBuilder: (context, index) { + return ProfileNodeCard( + profileNode: profileData.nodes.elementAt(index), + onTap: () { + context.push( + '${NodeDetailsPage.routeName}/${profileData.nodes.elementAt(index).id}'); + }, + ); + }, + ), + ), + ), + if (currentIndex == 1) + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: CardContainer( + child: SizedBox( + height: 400, + child: QuestionActivity( + isAdmin: isAdmin, + isHidden: false, + questions: questionList), + ), + ), + ), ], ), + ), + ), + desktop: SingleChildScrollView( + child: SizedBox( + width: Responsive.getGenericPageWidth(context), + child: isLoading + ? Container( + decoration: const BoxDecoration(color: Colors.white), + padding: const EdgeInsets.only(top: 20), + child: const Center( + child: CircularProgressIndicator(), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: ProfileActivityTabBar( - callback: updateIndex, - ), - ), - if (currentIndex == 0) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: CardContainer( - child: ListView.builder( - padding: const EdgeInsets.all(0), - scrollDirection: Axis.vertical, - shrinkWrap: true, - itemCount: profileData.nodes.length, - itemBuilder: (context, index) { - return ProfileNodeCard( - profileNode: profileData.nodes.elementAt(index), - onTap: () { - context.push( - '${NodeDetailsPage.routeName}/${profileData.nodes.elementAt(index).id}'); - }, - ); - }, + ) + : error + ? SelectableText( + errorMessage, + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ) + : Column( + children: [ + AboutMe( + profileData: profileData, + tags: profileData.tags, + addUserSemanticTag: addUserSemanticTag, + removeUserSemanticTag: removeUserSemanticTag, + noWorks: noWorks, + userType: basicUser.userType, + newUserType: newUserType, + onTap: handleButtonIsBanned, + onTapReviewerButton: handleButtonIsReviewer, ), - ), - ), - if (currentIndex == 1) - const Padding( - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: CardContainer( - child: SizedBox( - height: 400, - child: QuestionActivity(), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: DesktopEditProfileButton(), ), - ), - ), - ], - ), - ), - ), - desktop: SingleChildScrollView( - child: SizedBox( - width: Responsive.getGenericPageWidth(context), - child: isLoading - ? Container( - decoration: const BoxDecoration(color: Colors.white), - padding: const EdgeInsets.only(top: 20), - child: const Center( - child: CircularProgressIndicator(), - ), - ) - : error - ? SelectableText( - errorMessage, - style: const TextStyle(color: Colors.red), - textAlign: TextAlign.center, - ) - : Column( - children: [ - AboutMe( - aboutMe: profileData.aboutMe, - email: profileData.email, - name: profileData.name, - surname: profileData.surname, - noWorks: noWorks, - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: DesktopEditProfileButton(), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: ProfileActivityTabBar( - callback: updateIndex, - ), - ), - if (currentIndex == 0) - CardContainer( - child: ListView.builder( - padding: const EdgeInsets.all(0), - scrollDirection: Axis.vertical, - shrinkWrap: true, - itemCount: profileData.nodes.length, - itemBuilder: (context, index) { - return ProfileNodeCard( - profileNode: profileData.nodes.elementAt(index), - onTap: () { - context.push( - '${NodeDetailsPage.routeName}/${profileData.nodes.elementAt(index).id}'); - }, - ); - }, - ), - ), - if (currentIndex == 1) - const Padding( - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: CardContainer( - child: SizedBox( - height: 400, - child: QuestionActivity(), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: ProfileActivityTabBar( + callback: updateIndex, + ), ), - ), + if (currentIndex == 0) + CardContainer( + child: ListView.builder( + padding: const EdgeInsets.all(0), + scrollDirection: Axis.vertical, + shrinkWrap: true, + itemCount: profileData.nodes.length, + itemBuilder: (context, index) { + return ProfileNodeCard( + profileNode: profileData.nodes.elementAt(index), + onTap: () { + context.push( + '${NodeDetailsPage.routeName}/${profileData.nodes.elementAt(index).id}'); + }, + ); + }, + ), + ), + if (currentIndex == 1) + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: CardContainer( + child: SizedBox( + height: 400, + child: QuestionActivity( + isAdmin: isAdmin, + isHidden: false, + questions: questionList), + ), + ), + ), + ], ), - ], - ), - ), - ), - ), - ); + ), + ), + ), + ) + : const ErrorPage(); } // others profile page, will be same both on desktop and mobile - return PageWithAppBar( - appBar: const HomePageAppBar(), - pageColor: Colors.grey.shade200, - child: SingleChildScrollView( - child: SizedBox( - width: Responsive.getGenericPageWidth(context), - child: isLoading - ? Container( - decoration: const BoxDecoration(color: Colors.white), - padding: const EdgeInsets.only(top: 20), - child: const Center( - child: CircularProgressIndicator(), - ), - ) - : error - ? SelectableText( - errorMessage, - style: const TextStyle(color: Colors.red), - textAlign: TextAlign.center, - ) - : Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - AboutMe( - aboutMe: profileData.aboutMe, - email: profileData.email, - name: profileData.name, - surname: profileData.surname, - noWorks: noWorks, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: ProfileActivityTabBar( - callback: updateIndex, - ), + return (!profileData.isBanned || isAdmin) + ? PageWithAppBar( + appBar: const HomePageAppBar(), + pageColor: Colors.grey.shade200, + child: SingleChildScrollView( + child: SizedBox( + width: Responsive.getGenericPageWidth(context), + child: isLoading && isAuthLoading + ? Container( + decoration: const BoxDecoration(color: Colors.white), + padding: const EdgeInsets.only(top: 20), + child: const Center( + child: CircularProgressIndicator(), ), - if (currentIndex == 0) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: CardContainer( - child: ListView.builder( - padding: const EdgeInsets.all(0), - physics: - const NeverScrollableScrollPhysics(), // Prevents a conflict with SingleChildScrollView - scrollDirection: Axis.vertical, - shrinkWrap: true, - itemCount: profileData.nodes.length, - itemBuilder: (context, index) { - return ProfileNodeCard( - profileNode: profileData.nodes.elementAt(index), - onTap: () { - context.push( - '${NodeDetailsPage.routeName}/${profileData.nodes.elementAt(index).id}'); - }, - ); - }, - ), - ), - ), - if (currentIndex == 1) - const Padding( - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: CardContainer( - child: SizedBox( - height: 400, - child: QuestionActivity(), + ) + : error + ? SelectableText( + errorMessage, + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ) + : Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + AboutMe( + profileData: profileData, + tags: profileData.tags, + addUserSemanticTag: addUserSemanticTag, + removeUserSemanticTag: removeUserSemanticTag, + noWorks: noWorks, + userType: basicUser.userType, + newUserType: newUserType, + onTap: handleButtonIsBanned, + onTapReviewerButton: handleButtonIsReviewer), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: ProfileActivityTabBar( + callback: updateIndex, + ), ), - ), + if (currentIndex == 0) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: CardContainer( + child: ListView.builder( + padding: const EdgeInsets.all(0), + physics: + const NeverScrollableScrollPhysics(), // Prevents a conflict with SingleChildScrollView + scrollDirection: Axis.vertical, + shrinkWrap: true, + itemCount: profileData.nodes.length, + itemBuilder: (context, index) { + return ProfileNodeCard( + profileNode: profileData.nodes.elementAt(index), + onTap: () { + context.push( + '${NodeDetailsPage.routeName}/${profileData.nodes.elementAt(index).id}'); + }, + ); + }, + ), + ), + ), + if (currentIndex == 1) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: CardContainer( + child: SizedBox( + height: 400, + child: QuestionActivity( + isAdmin: isAdmin, + isHidden: false, + questions: questionList), + ), + ), + ), + ], ), - ], - ), - ), - ), - ); + ), + ), + ) + : const ErrorPage(); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/about_me.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/about_me.dart index 77c7e73b..74b18d40 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/about_me.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/about_me.dart @@ -1,21 +1,40 @@ +import 'package:collaborative_science_platform/screens/profile_page/widgets/profile_semantic_tag_list_view.dart'; +import 'package:collaborative_science_platform/models/profile_data.dart'; import 'package:collaborative_science_platform/utils/colors.dart'; import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; +import 'package:collaborative_science_platform/widgets/app_button.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:collaborative_science_platform/models/workspace_semantic_tag.dart'; -class AboutMe extends StatelessWidget { - final String email; - final String name; - final String surname; +class AboutMe extends StatefulWidget { + final ProfileData profileData; final int noWorks; - final String aboutMe; - const AboutMe( - {super.key, - required this.email, - required this.name, - required this.surname, - required this.noWorks, - required this.aboutMe}); + final String userType; + final String newUserType; + final List tags; + final Function addUserSemanticTag; + final Function removeUserSemanticTag; + final Function() onTap; + final Function() onTapReviewerButton; + const AboutMe({ + super.key, + required this.profileData, + required this.noWorks, + required this.userType, + required this.newUserType, + required this.tags, + required this.addUserSemanticTag, + required this.removeUserSemanticTag, + required this.onTap, + required this.onTapReviewerButton, + }); + @override + State createState() => _AboutMeState(); +} + +class _AboutMeState extends State { @override Widget build(BuildContext context) { return SizedBox( @@ -27,15 +46,89 @@ class AboutMe extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SelectableText( - "$name $surname", + "${widget.profileData.name} ${widget.profileData.surname}", style: const TextStyle( color: AppColors.primaryDarkColor, fontWeight: FontWeight.bold, fontSize: 40, ), ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Visibility( + visible: ((widget.userType == "admin" ? true : false) && + (widget.newUserType == "reviewer" || + widget.newUserType == "contributor")), + child: widget.newUserType == "contributor" + ? SizedBox( + width: 220, + child: AppButton( + text: "Give Reviewer Status", + height: 40, + icon: const Icon( + CupertinoIcons.add_circled_solid, + size: 16, + color: Colors.white, + ), + type: "safe", + onTap: widget.onTapReviewerButton, + ), + ) + : SizedBox( + width: 220, + child: AppButton( + text: "Remove Reviewer Status", + height: 40, + icon: const Icon( + CupertinoIcons.minus_circle_fill, + size: 16, + color: Colors.white, + ), + type: "danger", + onTap: widget.onTapReviewerButton, + ), + ), + ), + const SizedBox(height: 10.0), + Visibility( + visible: (widget.userType == "admin" ? true : false), + child: widget.profileData.isBanned + ? SizedBox( + width: 220, + child: AppButton( + text: "Unban User", + height: 40, + icon: const Icon( + CupertinoIcons.lock_open, + size: 16, + color: Colors.white, + ), + type: "grey", + onTap: widget.onTap, + ), + ) + : SizedBox( + width: 220, + child: AppButton( + text: "Ban User", + height: 40, + icon: const Icon( + CupertinoIcons.lock, + size: 16, + color: Colors.white, + ), + type: "danger", + onTap: widget.onTap, + ), + ), + ), + ], + ), ], ), const SizedBox( @@ -48,7 +141,7 @@ class AboutMe extends StatelessWidget { ? MediaQuery.of(context).size.width * 0.9 : MediaQuery.of(context).size.width * 0.5, child: SelectableText( - aboutMe, + widget.profileData.aboutMe, style: const TextStyle( fontWeight: FontWeight.normal, fontSize: 20, @@ -71,7 +164,7 @@ class AboutMe extends StatelessWidget { width: 10, ), SelectableText( - email, + widget.profileData.email, style: const TextStyle( fontSize: 20, ), @@ -84,7 +177,7 @@ class AboutMe extends StatelessWidget { Row( children: [ SelectableText( - "Published works: $noWorks", + "Published works: ${widget.noWorks}", style: const TextStyle( fontWeight: FontWeight.normal, fontSize: 20, @@ -97,6 +190,11 @@ class AboutMe extends StatelessWidget { ), const Divider( color: AppColors.tertiaryColor, + ), + ProfileSemanticTagListView( + tags: widget.tags, + addUserSemanticTag: widget.addUserSemanticTag, + removeUserSemanticTag: widget.removeUserSemanticTag, ) ], ), diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/account_settings_form.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/account_settings_form.dart index e0165940..8b1df55e 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/account_settings_form.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/account_settings_form.dart @@ -1,3 +1,4 @@ +import 'package:collaborative_science_platform/models/basic_user.dart'; import 'package:collaborative_science_platform/models/profile_data.dart'; import 'package:collaborative_science_platform/models/user.dart'; import 'package:collaborative_science_platform/providers/auth.dart'; @@ -7,6 +8,7 @@ import 'package:collaborative_science_platform/screens/profile_page/widgets/chan import 'package:collaborative_science_platform/utils/colors.dart'; import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; import 'package:collaborative_science_platform/utils/text_styles.dart'; +import 'package:collaborative_science_platform/widgets/app_text_field.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -19,17 +21,24 @@ class AccountSettingsForm extends StatefulWidget { class _AccountSettingsFormState extends State { ProfileData profileData = ProfileData(); + BasicUser basicUser = BasicUser(); + final passwordController = TextEditingController(); final aboutMeController = TextEditingController(); + final orcidController = TextEditingController(); final passwordFocusNode = FocusNode(); final aboutMeFocusNode = FocusNode(); + final orcidFocusNode = FocusNode(); bool isSwitched = false; bool isSwitched2 = false; bool error = false; String message = ""; + bool isLoading = false; + bool _isFirstTime = true; + @override void dispose() { passwordController.dispose(); @@ -39,12 +48,52 @@ class _AccountSettingsFormState extends State { super.dispose(); } + @override + void didChangeDependencies() { + if (_isFirstTime) { + try { + getBasicUser(); + } catch (e) { + setState(() { + error = true; + message = "Something went wrong!"; + }); + } + _isFirstTime = false; + } + super.didChangeDependencies(); + } + + void getBasicUser() async { + final User? user = Provider.of(context).user; + if (user != null) { + try { + final auth = Provider.of(context); //for token + basicUser = (auth.basicUser ?? {} as BasicUser); + isLoading = true; + // user = (auth.user ?? {} as User); + } catch (e) { + setState(() { + error = true; + message = "Something went wrong!"; + }); + rethrow; + } finally { + setState(() { + isSwitched = basicUser.emailNotificationPreference; + isSwitched2 = basicUser.showActivity; + isLoading = false; + }); + } + } + } + void changePreff() async { try { final User? user = Provider.of(context, listen: false).user; final settingsProvider = Provider.of(context, listen: false); await settingsProvider.changePreferences( - user, aboutMeController.text, isSwitched, isSwitched2); + user, aboutMeController.text, isSwitched, isSwitched2, orcidController.text); error = false; message = "Changed Successfully."; } catch (e) { @@ -60,19 +109,45 @@ class _AccountSettingsFormState extends State { final User? user = Provider.of(context).user; return Container( padding: const EdgeInsets.symmetric(horizontal: 16.0), - width: Responsive.getGenericPageWidth(context), + width: Responsive.getGenericPageWidth(context) / 1.2, child: Column( children: [ - const SizedBox(height: 10), + const SizedBox(height: 14), const Row( mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SelectableText('About', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), + SelectableText('About', + style: + TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 16.0)), ], ), - const SizedBox(height: 10), + const SizedBox(height: 8), AboutMeEdit(aboutMeController), - const Divider(height: 40.0), + const SizedBox(height: 14.0), + // const Divider(height: 40.0), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Add your ORCID", + style: + TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 16.0)), + const SizedBox(height: 10.0), + AppTextField( + controller: orcidController, + focusNode: orcidFocusNode, + hintText: 'Example: 0000-0002-4940-348X', + obscureText: false, + color: error && orcidController.text.isEmpty + ? AppColors.dangerColor + : AppColors.primaryColor, + prefixIcon: const Icon(Icons.edit), + height: 64.0, + onChanged: (_) {}, + ), + ], + ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -133,10 +208,12 @@ class _AccountSettingsFormState extends State { ), ), ), - const SizedBox(height: 10), Text(message), const Divider(height: 40.0), - Container( + const Text("or", + style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 16.0)), + const SizedBox(height: 10.0), + SizedBox( width: 400, child: MouseRegion( cursor: SystemMouseCursors.click, diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/change_password_form.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/change_password_form.dart index c7eccec6..5e2d3cdd 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/change_password_form.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/change_password_form.dart @@ -1,6 +1,5 @@ import 'package:collaborative_science_platform/models/user.dart'; import 'package:collaborative_science_platform/providers/settings_provider.dart'; -import 'package:collaborative_science_platform/utils/colors.dart'; import 'package:collaborative_science_platform/widgets/app_button.dart'; import 'package:flutter/material.dart'; import 'package:collaborative_science_platform/models/profile_data.dart'; @@ -28,7 +27,7 @@ class _ChangePasswordFormState extends State { final newPassFocusNode = FocusNode(); final double x = 300; -bool buttonState = false; + bool buttonState = false; String errorMessage = ""; bool error = false; @@ -67,8 +66,7 @@ bool buttonState = false; } void controllerCheck() { - if (newPassController.text.isNotEmpty && - oldPassController.text.isNotEmpty) { + if (newPassController.text.isNotEmpty && oldPassController.text.isNotEmpty) { setState(() { buttonState = true; }); @@ -111,16 +109,19 @@ bool buttonState = false; ], ), const SizedBox(height: 20.0), - AppButton( - onTap: () => oldPassController.text.isNotEmpty && newPassController.text.isNotEmpty ? changePass() : {}, - text: "Save", - height: 50, - isActive: buttonState, - // isLoading: isLoading, - ), + onTap: () => oldPassController.text.isNotEmpty && newPassController.text.isNotEmpty + ? changePass() + : {}, + text: "Save", + height: 50, + isActive: buttonState, + ), const SizedBox(height: 10.0), - Text(errorMessage, style: const TextStyle(fontSize: 16.0),), + Text( + errorMessage, + style: const TextStyle(fontSize: 16.0), + ), ], ), ); diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/profile_activity_tabbar.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/profile_activity_tabbar.dart index fee7c83d..7033b6de 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/profile_activity_tabbar.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/profile_activity_tabbar.dart @@ -29,7 +29,7 @@ class _ProfileActivityTabBar extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Center( + const Center( child: Text( "Activities", style: TextStyles.bodyBlack, diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/profile_semantic_tag_list_view.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/profile_semantic_tag_list_view.dart new file mode 100644 index 00000000..5942838f --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/profile_semantic_tag_list_view.dart @@ -0,0 +1,56 @@ +import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/subsection_title.dart'; +import 'package:collaborative_science_platform/widgets/semantic_search_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:collaborative_science_platform/models/workspace_semantic_tag.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/semantic_tag_card.dart'; +import 'package:collaborative_science_platform/utils/text_styles.dart'; + +class ProfileSemanticTagListView extends StatefulWidget { + final List tags; + final Function addUserSemanticTag; + final Function removeUserSemanticTag; + + const ProfileSemanticTagListView({ + required this.tags, + required this.addUserSemanticTag, + required this.removeUserSemanticTag, + super.key, + }); + + @override + State createState() => _ProfileSemanticTagListViewState(); +} + +class _ProfileSemanticTagListViewState extends State { + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SubSectionTitle(title: "Semantic Tags"), + SemanticSearchBar(addSemanticTag: widget.addUserSemanticTag), + Text( + (widget.tags.isNotEmpty) ? "Added Tags" : "You haven't added any tag yet!", + style: TextStyles.bodySecondary, + ), + ListView.builder( + padding: const EdgeInsets.all(0.0), + shrinkWrap: true, + itemCount: widget.tags.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return SemanticTagCard( + finalized: false, + tag: widget.tags[index], + backgroundColor: const Color.fromARGB(255, 220, 235, 220), + onDelete: () async { + await widget.removeUserSemanticTag(widget.tags[index].tagId); + }, + ); + }, + ), + ], + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/question_activity.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/question_activity.dart index 84e537e0..886dedd5 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/question_activity.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/profile_page/widgets/question_activity.dart @@ -1,8 +1,16 @@ +import 'package:collaborative_science_platform/models/node_details_page/question.dart'; +import 'package:collaborative_science_platform/screens/node_details_page/node_details_page.dart'; +import 'package:collaborative_science_platform/screens/node_details_page/widgets/question_box.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; -// TODO: currently no API to get questions class QuestionActivity extends StatelessWidget { - const QuestionActivity({super.key}); + final bool isAdmin; + final bool isHidden; + final List questions; + const QuestionActivity( + {Key? key, required this.isAdmin, required this.isHidden, required this.questions}) + : super(key: key); @override Widget build(BuildContext context) { @@ -10,48 +18,17 @@ class QuestionActivity extends StatelessWidget { padding: const EdgeInsets.all(0), scrollDirection: Axis.vertical, shrinkWrap: true, - itemCount: 10, + itemCount: questions.length, itemBuilder: (BuildContext context, int index) { - return Card( - elevation: 4.0, - margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - child: InkWell( - // onTap: Navigate to the screen of the question/answer - customBorder: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - "question/answer $index", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18.0, - ), - ), - const SizedBox(height: 8.0), - const SizedBox(height: 8.0), - const Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SelectableText( - 'some date', - style: TextStyle( - color: Colors.grey, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ], - ), - ), + return GestureDetector( + onTap: () { + context.push("${NodeDetailsPage.routeName}/${questions[index].nodeId}"); + }, + child: QuestionBox( + isAdmin: isAdmin, + question: questions[index], + canAnswer: false, + onTap: () {}, ), ); }, diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/mobile_workspace_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/mobile_workspace_page.dart index 26c6d0a8..d25f6565 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/mobile_workspace_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/mobile_workspace_page.dart @@ -3,18 +3,78 @@ import 'package:collaborative_science_platform/models/workspaces_page/workspace. import 'package:collaborative_science_platform/models/workspaces_page/workspaces.dart'; import 'package:collaborative_science_platform/models/workspaces_page/workspaces_object.dart'; import 'package:collaborative_science_platform/screens/page_with_appbar/page_with_appbar.dart'; +import 'package:collaborative_science_platform/screens/page_with_appbar/widgets/app_bar_button.dart'; import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/app_alert_dialog.dart'; import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/mobile_workspace_content.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/comments_sidebar.dart'; import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/create_workspace_form.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/workspaces_side_bar.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/workspaces_page.dart'; import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; +import 'package:collaborative_science_platform/utils/text_styles.dart'; import 'package:flutter/material.dart'; -import '../../../widgets/app_button.dart'; -import '../../home_page/widgets/home_page_appbar.dart'; +import 'package:go_router/go_router.dart'; +import 'package:collaborative_science_platform/widgets/app_button.dart'; +import 'package:collaborative_science_platform/screens/home_page/widgets/home_page_appbar.dart'; class MobileWorkspacePage extends StatefulWidget { final Workspace? workspace; final Workspaces? workspaces; - const MobileWorkspacePage({super.key, required this.workspace, required this.workspaces}); + final Function createNewWorkspace; + final Function createNewEntry; + final Function editEntry; + final Function deleteEntry; + final Function addReference; + final Function deleteReference; + final Function editTitle; + final Function sendCollaborationRequest; + final Function finalizeWorkspace; + final Function addSemanticTag; + final Function removeSemanticTag; + final Function sendWorkspaceToReview; + final Function addReview; + final Function updateReviewRequest; + final Function updateCollaborationRequest; + + final Function resetWorkspace; + + final Function setProof; + final Function setDisproof; + final Function setTheorem; + final Function removeDisproof; + final Function removeTheorem; + final Function removeProof; + + + const MobileWorkspacePage({ + super.key, + required this.workspace, + required this.workspaces, + required this.createNewWorkspace, + required this.createNewEntry, + required this.editEntry, + required this.deleteEntry, + required this.addReference, + required this.deleteReference, + required this.editTitle, + required this.addSemanticTag, + required this.removeSemanticTag, + required this.finalizeWorkspace, + required this.sendCollaborationRequest, + required this.sendWorkspaceToReview, + required this.addReview, + required this.updateReviewRequest, + required this.updateCollaborationRequest, + required this.resetWorkspace, + + required this.removeDisproof, + required this.removeProof, + required this.removeTheorem, + required this.setDisproof, + required this.setProof, + required this.setTheorem, + + }); @override State createState() => _MobileWorkspacesPageState(); @@ -22,57 +82,19 @@ class MobileWorkspacePage extends StatefulWidget { class _MobileWorkspacesPageState extends State { final CarouselController controller = CarouselController(); - - Workspaces workspacesData = Workspaces( - workspaces: [], - pendingWorkspaces: [], - ); + TextEditingController textController = TextEditingController(); + ScrollController controller1 = ScrollController(); + ScrollController controller2 = ScrollController(); bool isLoading = false; bool error = false; String errorMessage = ""; - int yourWorkLength = 0; - int pendingLength = 0; - int totalLength = 0; int current = 1; int workspaceIndex = 0; - @override - void initState() { - super.initState(); - getWorkspacesData(); - } - - void getWorkspacesData() { - setState(() { - isLoading = true; - }); - for (int i = 0; i < 4; i++) { - workspacesData.workspaces.add( - WorkspacesObject( - workspaceId: i+1, - workspaceTitle: "Workspace Title xxxxxxxxxxxxxxxxxx ${i+1}", - pending: false, - ), - ); - } - for (int i = 0; i < 2; i++) { - workspacesData.pendingWorkspaces.add( - WorkspacesObject( - workspaceId: i+workspacesData.workspaces.length+1, - workspaceTitle: "Pending Workspace Title ${i+1}", - pending: true, - ), - ); - } - yourWorkLength = workspacesData.workspaces.length; - pendingLength = workspacesData.pendingWorkspaces.length; - totalLength = yourWorkLength + pendingLength; - setState(() { - isLoading = false; - }); - } + bool showSidebar = false; + bool showCommentSidebar = false; Widget mobileAddNewWorkspaceIcon() { return CircleAvatar( @@ -86,13 +108,17 @@ class _MobileWorkspacesPageState extends State { context: context, builder: (context) => AppAlertDialog( text: "Create Workspace", - content: const CreateWorkspaceForm(), + content: CreateWorkspaceForm( + titleController: textController, + ), actions: [ AppButton( text: "Create New Workspace", height: 50, - onTap: () { - // Create Workspace + onTap: () async { + await widget.createNewWorkspace(textController.text); + textController.text = ""; + // ignore: use_build_context_synchronously Navigator.of(context).pop(); }, ), @@ -109,6 +135,7 @@ class _MobileWorkspacesPageState extends State { padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), child: SizedBox( height: 80.0, + width: MediaQuery.of(context).size.width * 0.9, child: Card( elevation: 4.0, shape: RoundedRectangleBorder( @@ -118,50 +145,8 @@ class _MobileWorkspacesPageState extends State { customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0), ), - onTap: pending ? () { // accept or reject the review - showDialog( - context: context, - builder: (context) => AppAlertDialog( - text: "Do you accept the work?", - actions: [ - AppButton( - text: "Accept", - height: 40, - onTap: () { - /* Send to review */ - Navigator.of(context).pop(); - }, - ), - AppButton( - text: "Reject", - height: 40, - onTap: () { Navigator.of(context).pop(); }, - ), - ], - ), - ); - } : () { // send to review - showDialog( - context: context, - builder: (context) => AppAlertDialog( - text: "Do you want to send it to review?", - actions: [ - AppButton( - text: "Yes", - height: 40, - onTap: () { - /* Send to review */ - Navigator.of(context).pop(); - }, - ), - AppButton( - text: "No", - height: 40, - onTap: () { Navigator.of(context).pop(); }, - ), - ], - ), - ); + onTap: () { + context.push("${WorkspacesPage.routeName}/${workspacesObject.workspaceId}"); }, child: Center( child: Padding( @@ -171,7 +156,8 @@ class _MobileWorkspacesPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(width: 2.0), - Expanded( + SizedBox( + width: MediaQuery.of(context).size.width * 0.55, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, @@ -179,8 +165,7 @@ class _MobileWorkspacesPageState extends State { Text( pending ? "Pending" : "Your Work", style: TextStyle( - color: pending ? Colors.red.shade800 - : Colors.green.shade800, + color: pending ? Colors.red.shade800 : Colors.green.shade800, fontWeight: FontWeight.w500, fontSize: 15.0, ), @@ -197,11 +182,6 @@ class _MobileWorkspacesPageState extends State { ], ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 3.0), - child: pending ? const Icon(Icons.keyboard_arrow_right) - : const Icon(Icons.send), - ), ], ), ), @@ -216,58 +196,97 @@ class _MobileWorkspacesPageState extends State { return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CarouselSlider( - carouselController: controller, - items: List.generate( - totalLength+1, - (index) => (index == 0) ? mobileAddNewWorkspaceIcon() - : (index <= yourWorkLength) ? mobileWorkspaceCard( - workspacesData.workspaces[index-1], - false, - ) : mobileWorkspaceCard( - workspacesData.pendingWorkspaces[index-yourWorkLength-1], - true, - ), - ), - options: CarouselOptions( - scrollPhysics: const ScrollPhysics(), - height: 100, - autoPlay: false, - viewportFraction: 0.8, - enableInfiniteScroll: false, - initialPage: current, - enlargeCenterPage: true, - enlargeStrategy: CenterPageEnlargeStrategy.zoom, - enlargeFactor: 0.3, - onPageChanged: (index, reason) { - // I added this conditional to reduce the number - // of build operation for the workspace. - // Going to slide 1 from slide 2 or vice versa does not affect the - // workspace content below. So it shouldn't be reloaded again. - // However, it doesn't work. One that solves this problem wins a chukulat. - if (index != 0 && current != 0) { - setState(() { - workspaceIndex = index-1; - }); - } - setState(() { - current = index; - }); - }, - ), - ), - Text( - "${current+1}/${totalLength+1}", - style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - ), - ), - ], + children: widget.workspaces != null + ? [ + SizedBox( + width: MediaQuery.of(context).size.width, + height: 110, + child: CarouselSlider( + carouselController: controller, + items: List.generate( + widget.workspaces!.workspaces.length + + widget.workspaces!.pendingWorkspaces.length + + 1, + (index) => (index == 0) + ? mobileAddNewWorkspaceIcon() + : (index <= widget.workspaces!.workspaces.length) + ? mobileWorkspaceCard( + widget.workspaces!.workspaces[index - 1], + false, + ) + : mobileWorkspaceCard( + widget.workspaces!.pendingWorkspaces[ + index - widget.workspaces!.workspaces.length - 1], + true, + ), + ), + options: CarouselOptions( + scrollPhysics: const ScrollPhysics(), + height: 100, + autoPlay: false, + viewportFraction: 0.8, + enableInfiniteScroll: false, + initialPage: current, + enlargeCenterPage: true, + enlargeStrategy: CenterPageEnlargeStrategy.zoom, + enlargeFactor: 0.3, + onPageChanged: (index, reason) { + if (index != 0 && current != 0) { + setState(() { + workspaceIndex = index - 1; + }); + } + setState(() { + current = index; + }); + }, + ), + ), + ), + Text( + "${current + 1}/${widget.workspaces!.workspaces.length + widget.workspaces!.pendingWorkspaces.length + 1}", + style: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + ), + ), + ] + : [ + SizedBox( + width: MediaQuery.of(context).size.width, + height: 100, + //child: mobileAddNewWorkspaceIcon(), + ) + ], ); } + @override + void dispose() { + controller1.dispose(); + controller2.dispose(); + super.dispose(); + } + + hideSideBar() { + setState(() { + showSidebar = false; + }); + } + + hideCommentsSideBar() { + setState(() { + showCommentSidebar = false; + }); + } + + displayCommentSidebar() { + setState(() { + showCommentSidebar = true; + showSidebar = false; + }); + } + @override Widget build(BuildContext context) { if (isLoading || error) { @@ -285,32 +304,121 @@ class _MobileWorkspacesPageState extends State { return PageWithAppBar( appBar: const HomePageAppBar(), child: SizedBox( + height: MediaQuery.of(context).size.height, width: Responsive.getGenericPageWidth(context), child: ListView( physics: const ScrollPhysics(), padding: const EdgeInsets.only(top: 10.0), - children: [ - slidingWorkspaceList(), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: Divider(), - ), - (totalLength != 0) ? MobileWorkspaceContent( - workspaceId: (workspaceIndex < yourWorkLength) ? workspacesData.workspaces[workspaceIndex].workspaceId - : workspacesData.pendingWorkspaces[workspaceIndex-yourWorkLength].workspaceId, - pending: (workspaceIndex < yourWorkLength) ? false : true, - ) : const Padding( - padding: EdgeInsets.fromLTRB(16.0, 120.0, 16.0, 0.0), - child: Text( - "You haven't created any workspace yet!", - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 24.0, - ), - ), - ), - ], + children: showSidebar || showCommentSidebar + ? [ + if (showSidebar && !showCommentSidebar) + WorkspacesSideBar( + controller: controller1, + hideSidebar: hideSideBar, + height: MediaQuery.of(context).size.height, + workspaces: widget.workspaces, + createNewWorkspace: widget.createNewWorkspace, + updateReviewRequest: widget.updateReviewRequest, + updateCollaborationRequest: widget.updateCollaborationRequest, + ), + if (showCommentSidebar && !showSidebar) + CommentsSideBar( + controller: controller2, + height: MediaQuery.of(context).size.height, + comments: widget.workspace!.comments, + hideSidebar: hideCommentsSideBar, + ) + ] + : [ + // slidingWorkspaceList(), + // const Padding( + // padding: EdgeInsets.symmetric(horizontal: 12.0), + // child: Divider(), + // ), + Row(mainAxisAlignment: MainAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.all(8), + child: AppBarButton( + onPressed: () { + setState(() { + showSidebar = true; + showCommentSidebar = false; + }); + }, + icon: Icons.menu, + text: "hide workspaces", + ), + ), + const Text( + "Workspaces", + style: TextStyles.bodyGrey, + ) + ]), + (widget.workspaces != null && widget.workspace == null) + ? ((widget.workspaces!.workspaces.length + + widget.workspaces!.pendingWorkspaces.length != + 0) + ? const Padding( + padding: EdgeInsets.fromLTRB(16.0, 120.0, 16.0, 0.0), + child: Text( + "Select a workspace to see details.", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 24.0, + ), + ), + ) + : const Padding( + padding: EdgeInsets.fromLTRB(16.0, 120.0, 16.0, 0.0), + child: Text( + "You haven't created any workspace yet!", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 24.0, + ), + ), + )) + : (widget.workspaces != null && widget.workspace != null) + ? MobileWorkspaceContent( + workspace: widget.workspace!, + pending: widget.workspace!.pending, + + createNewEntry: widget.createNewEntry, + editEntry: widget.editEntry, + deleteEntry: widget.deleteEntry, + addReference: widget.addReference, + deleteReference: widget.deleteReference, + editTitle: widget.editTitle, + addSemanticTag: widget.addSemanticTag, + removeSemanticTag: widget.removeSemanticTag, + finalizeWorkspace: widget.finalizeWorkspace, + sendCollaborationRequest: widget.sendCollaborationRequest, + updateRequest: widget.updateCollaborationRequest, + sendWorkspaceToReview: widget.sendWorkspaceToReview, + addReview: widget.addReview, + + resetWorkspace: widget.resetWorkspace, + + setProof: widget.setProof, + setDisproof: widget.setDisproof, + setTheorem: widget.setTheorem, + removeProof: widget.removeProof, + removeDisproof: widget.removeDisproof, + removeTheorem: widget.removeTheorem, + + displayCommentSidebar: displayCommentSidebar, + + ) + : const SizedBox( + width: 100, + height: 100, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ], ), ), ); diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/contributor_card.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/contributor_card.dart index 3de14739..f65e8ea2 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/contributor_card.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/contributor_card.dart @@ -1,15 +1,24 @@ +import 'package:collaborative_science_platform/models/workspaces_page/workspace.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import '../../../../models/user.dart'; -import '../../../profile_page/profile_page.dart'; +import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/utils/colors.dart'; +import 'package:collaborative_science_platform/screens/profile_page/profile_page.dart'; class ContributorCard extends StatelessWidget { final User contributor; + final bool pending; + final bool workspacePending; + final Function updateRequest; const ContributorCard({ super.key, required this.contributor, + required this.pending, + required this.workspacePending, + required this.updateRequest, }); @override @@ -34,30 +43,47 @@ class ContributorCard extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - "${contributor.firstName} ${contributor.lastName}", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 18.0, - ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${contributor.firstName} ${contributor.lastName}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18.0, + ), + ), + const SizedBox(height: 2.0), + Text( + contributor.email, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.0, + color: Colors.grey, + ), + ), + ], ), - const SizedBox(height: 2.0), - Text( - contributor.email, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16.0, - color: Colors.grey, + if (pending && !workspacePending) + IconButton( + icon: const Icon( + CupertinoIcons.clear_circled, + color: AppColors.warningColor, + ), + onPressed: () async { + // function to delete collaboration request + await updateRequest(contributor.requestId, RequestStatus.rejected); + }, ), - ), ], ), ), diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/entry_card.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/entry_card.dart deleted file mode 100644 index 2de702c4..00000000 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/entry_card.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/app_alert_dialog.dart'; -import 'package:collaborative_science_platform/widgets/app_button.dart'; -import 'package:flutter/material.dart'; - -import '../../../../models/workspaces_page/entry.dart'; -import '../../../../utils/colors.dart'; - - -class EntryCard extends StatefulWidget { - final Entry entry; - final void Function() onDelete; - final bool pending; - - const EntryCard({ - super.key, - required this.entry, - required this.onDelete, - required this.pending, - }); - - @override - State createState() => _EntryCardState(); -} - -class _EntryCardState extends State { - final entryController = TextEditingController(); - final entryFocusNode = FocusNode(); - - final double shrunkHeight = 120.0; - final double extendHeight = 450.0; - - bool extended = false; - bool readOnly = true; - - @override - void initState() { - super.initState(); - entryController.text = widget.entry.content; - } - - @override - void dispose() { - entryController.dispose(); - entryFocusNode.dispose(); - super.dispose(); - } - - Widget entryHeader() { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - widget.entry.isTheoremEntry ? "Theorem" - : widget.entry.isProofEntry ? "Proof" - : "", - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16.0, - color: widget.entry.isTheoremEntry ? Colors.green.shade800 - : widget.entry.isProofEntry ? Colors.yellow.shade800 - : Colors.blue.shade800, - ), - ), - if (widget.entry.isFinalEntry) Text( - " (Final)", - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16.0, - color: Colors.red.shade800, - ), - ), - ], - ); - } - - Widget iconRow(bool pending) { - return !pending ? Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - readOnly ? IconButton( - onPressed: () { // Make editable - setState(() { - readOnly = false; - }); - }, - icon: const Icon(Icons.edit), - ) : Row( - children: [ - IconButton( - onPressed: () { - // Save the changes - setState(() { - readOnly = true; - }); - }, - icon: const Icon(Icons.check), - ), - IconButton( - onPressed: () { - // Do not save the changes - setState(() { - readOnly = true; - }); - }, - icon: const Icon(Icons.close), - ), - ], - ), - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => - AppAlertDialog( - text: "Do you want to delete the entry?", - actions: [ - AppButton( - text: "Yes", - height: 40, - onTap: () { - setState(() { // delete the entry - widget.onDelete(); - }); - Navigator.of(context).pop(); - }, - ), - AppButton( - text: "No", - height: 40, - onTap: () { Navigator.of(context).pop(); }, - ), - ], - ), - ); - }, - icon: const Icon(Icons.delete), - ), - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => AppAlertDialog( - text: "Do you want to finalize the entry?", - actions: [ - AppButton( - text: "Yes", - height: 40, - onTap: () { - /* Finalize the entry */ - Navigator.of(context).pop(); - }, - ), - AppButton( - text: "No", - height: 40, - onTap: () { Navigator.of(context).pop(); }, - ), - ], - ), - ); - }, - icon: const Icon(Icons.stop), - ), - const Expanded(child: SizedBox()), - Text( - widget.entry.publishDateFormatted, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16.0, - color: Colors.grey, - ), - ), - const SizedBox(width: 10.0), - ], - ) : Padding( - padding: const EdgeInsets.only(top: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - widget.entry.publishDateFormatted, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16.0, - color: Colors.grey, - ), - ), - const SizedBox(width: 10.0), - ], - ), - ); - } - - Widget fullEntryContent() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - readOnly: readOnly, - controller: entryController, - focusNode: entryFocusNode, - cursorColor: Colors.grey.shade700, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16.0, - ), - maxLines: 10, - onChanged: (text) { /* What will happen when the text changes? */ }, - decoration: InputDecoration( - enabledBorder: OutlineInputBorder( - borderSide: const BorderSide(color: AppColors.primaryColor), - borderRadius: BorderRadius.circular(4.0), - ), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: AppColors.secondaryDarkColor), - borderRadius: BorderRadius.circular(4.0), - ), - ), - ), - ), - const SizedBox(height: 10.0), - iconRow(widget.pending), - ], - ); - } - - Widget headerOfContent() { - return Text( - widget.entry.content, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - ), - ); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - height: extended ? extendHeight : shrunkHeight, - child: Card( - elevation: 4.0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 0.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - entryHeader(), - extended ? fullEntryContent() : headerOfContent(), - const Expanded(child: SizedBox()), - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - onPressed: () { - setState(() { - extended = !extended; - }); - }, - icon: extended - ? const Icon(Icons.keyboard_arrow_up) - : const Icon(Icons.keyboard_arrow_down), - ), - ], - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/entry_header.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/entry_header.dart new file mode 100644 index 00000000..ca8a94e7 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/entry_header.dart @@ -0,0 +1,48 @@ + +import 'package:collaborative_science_platform/models/workspaces_page/entry.dart'; +import 'package:flutter/material.dart'; + +class EntryHeader extends StatelessWidget { + final Entry entry; + const EntryHeader({ + super.key, + required this.entry, + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + entry.isTheoremEntry + ? "Theorem" + : entry.isProofEntry + ? "Proof" + : entry.isDisproofEntry + ? "Disproof" + : "", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + color: entry.isTheoremEntry + ? Colors.green.shade800 + : entry.isProofEntry + ? Colors.yellow.shade800 + : Colors.blue.shade800, + ), + ), + if (entry.isFinalEntry) + Text( + " (Final)", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + color: Colors.red.shade800, + ), + ), + ], + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/mobile_entry_card.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/mobile_entry_card.dart new file mode 100644 index 00000000..5c97b005 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/mobile_entry_card.dart @@ -0,0 +1,287 @@ +import 'dart:convert'; + +import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/entry_menu.dart'; +import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_tex/flutter_tex.dart'; + +import 'package:collaborative_science_platform/models/workspaces_page/entry.dart'; +import 'package:collaborative_science_platform/utils/colors.dart'; +import 'package:collaborative_science_platform/widgets/card_container.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/entry_header.dart'; + +class MobileEntryCard extends StatefulWidget { + final Entry entry; + final void Function() onDelete; + final Function editEntry; + final Color backgroundColor; + final bool finalized; + final Function setProof; + final Function setDisproof; + final Function setTheorem; + final Function removeDisproof; + final Function removeTheorem; + final Function removeProof; + final Function deleteEntry; + final bool fromNode; + + const MobileEntryCard({ + super.key, + required this.entry, + required this.onDelete, + required this.editEntry, + this.backgroundColor = const Color.fromARGB(255, 220, 235, 220), + required this.finalized, + required this.removeDisproof, + required this.removeProof, + required this.removeTheorem, + required this.setDisproof, + required this.setProof, + required this.setTheorem, + required this.deleteEntry, + required this.fromNode, + }); + + @override + State createState() => _MobileEntryCardState(); +} + +class _MobileEntryCardState extends State { + final entryController = TextEditingController(); + final entryFocusNode = FocusNode(); + + bool editMode = false; + bool edited = false; + String buffer = ""; + + @override + void initState() { + super.initState(); + entryController.text = widget.entry.content; + } + + @override + void dispose() { + entryController.dispose(); + entryFocusNode.dispose(); + super.dispose(); + } + + Widget upperIconRow() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + setState(() { + editMode = false; + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Preview", + style: TextStyle( + color: (editMode) ? Colors.grey : Colors.indigo[600], + fontSize: 16.0, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(width: 4.0), + Icon( + Icons.visibility, + color: (editMode) ? Colors.grey : Colors.indigo[600], + ) + ], + ), + ), + ), + const SizedBox(width: 12.0), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + setState(() { + if (!edited) { + buffer = entryController.text; + } + editMode = true; + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Edit", + style: TextStyle( + color: (!editMode) ? Colors.grey : Colors.indigo[600], + fontSize: 16.0, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(width: 4.0), + Icon( + Icons.edit, + color: (!editMode) ? Colors.grey : Colors.indigo[600], + ) + ], + ), + ), + ), + ], + ), + (editMode) + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () async { + await widget.editEntry(entryController.text, widget.entry.entryId); + setState(() { + editMode = false; + edited = false; + }); + }, + icon: const Icon( + Icons.check, + color: Colors.green, + ), + ), + IconButton( + onPressed: () { + setState(() { + editMode = false; + edited = false; + entryController.text = buffer; + }); + }, + icon: const Icon( + Icons.close, + color: Colors.red, + ), + ), + EntryMenu( + removeDisproof: widget.removeDisproof, + removeProof: widget.removeProof, + removeTheorem: widget.removeTheorem, + setDisproof: widget.setDisproof, + setProof: widget.setProof, + setTheorem: widget.setTheorem, + entry: widget.entry, + deleteEntry: widget.deleteEntry, + fromNode: widget.fromNode, + ), + ], + ) + : Container(), + + ], + ); + } + + Widget lowerIconRow() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // (widget.entry.isEditable) + // ? IconButton( + // onPressed: () { + // widget.onDelete(); + // setState(() {}); + // }, + // icon: const Icon( + // Icons.delete, + // color: Colors.grey, + // ), + // ) + // : Container(), + Text( + widget.entry.publishDateFormatted, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.0, + color: Colors.grey, + ), + ), + + ], + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + child: CardContainer( + backgroundColor: widget.backgroundColor, + child: Padding( + padding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + EntryHeader(entry: widget.entry), + if (!widget.finalized && widget.entry.isEditable) upperIconRow(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: (editMode) + ? TextField( + controller: entryController, + focusNode: entryFocusNode, + cursorColor: Colors.grey.shade700, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.0, + ), + maxLines: 10, + onChanged: (text) { + edited = true; + }, + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: AppColors.primaryColor), + borderRadius: BorderRadius.circular(4.0), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: AppColors.secondaryDarkColor), + borderRadius: BorderRadius.circular(4.0), + ), + ), + ) + : Container( + constraints: BoxConstraints( + minHeight: 100.0, // Set the minimum height here + maxHeight: (Responsive.isMobile(context)) ? double.infinity : 400, + ), + child: TeXView( + loadingWidgetBuilder: (context) => + const Center(child: CircularProgressIndicator()), + renderingEngine: const TeXViewRenderingEngine.katex(), + child: TeXViewDocument( + utf8.decode(entryController.text.codeUnits), + ), + ), + ), + ), + if (!widget.finalized) lowerIconRow(), + ], + ), + ), + ), + ), + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/mobile_workspace_content.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/mobile_workspace_content.dart index 9934580c..af2d8894 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/mobile_workspace_content.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/mobile_workspace_content.dart @@ -1,31 +1,78 @@ +import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/new_entry.dart'; import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/reference_card.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/semantic_tag_card.dart'; import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/subsection_title.dart'; +import 'package:collaborative_science_platform/utils/colors.dart'; +import 'package:collaborative_science_platform/utils/text_styles.dart'; +import 'package:collaborative_science_platform/widgets/app_button.dart'; +import 'package:collaborative_science_platform/widgets/app_text_field.dart'; +import 'package:collaborative_science_platform/widgets/semantic_search_bar.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - - -import '../../../../models/node.dart'; -import '../../../../models/user.dart'; -import '../../../../models/workspaces_page/entry.dart'; -import '../../../../models/workspaces_page/workspace.dart'; -import '../../../../providers/auth.dart'; -import '../../../../utils/lorem_ipsum.dart'; -import '../../../../utils/responsive/responsive.dart'; -import '../../../../widgets/app_button.dart'; -import '../../web_workspace_page/widgets/add_reference_form.dart'; -import '../../web_workspace_page/widgets/entry_form.dart'; -import '../../web_workspace_page/widgets/send_collaboration_request_form.dart'; +import 'package:collaborative_science_platform/models/workspaces_page/workspace.dart'; +import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/add_reference_form.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/send_collaboration_request_form.dart'; import 'app_alert_dialog.dart'; import 'contributor_card.dart'; -import 'entry_card.dart'; +import 'mobile_entry_card.dart'; class MobileWorkspaceContent extends StatefulWidget { - final int workspaceId; + final Workspace workspace; final bool pending; + final Function createNewEntry; + final Function editEntry; + final Function deleteEntry; + final Function deleteReference; + final Function addReference; + final Function editTitle; + final Function updateRequest; + final Function sendCollaborationRequest; + final Function finalizeWorkspace; + final Function addSemanticTag; + final Function removeSemanticTag; + final Function sendWorkspaceToReview; + final Function addReview; + final Function resetWorkspace; + + final Function setProof; + final Function setDisproof; + final Function setTheorem; + final Function removeDisproof; + final Function removeTheorem; + final Function removeProof; + + final Function displayCommentSidebar; + + const MobileWorkspaceContent({ super.key, - required this.workspaceId, required this.pending, + required this.workspace, + required this.createNewEntry, + required this.editEntry, + required this.deleteEntry, + required this.addReference, + required this.deleteReference, + required this.editTitle, + required this.addSemanticTag, + required this.removeSemanticTag, + required this.finalizeWorkspace, + required this.sendCollaborationRequest, + required this.updateRequest, + required this.sendWorkspaceToReview, + required this.addReview, + + required this.resetWorkspace, + + required this.removeDisproof, + required this.removeProof, + required this.removeTheorem, + required this.setDisproof, + required this.setProof, + required this.setTheorem, + required this.displayCommentSidebar, + + }); @override @@ -36,276 +83,228 @@ class _MobileWorkspaceContentState extends State { bool isLoading = false; bool error = false; String errorMessage = ""; + bool entryLoading = false; - Workspace workspaceData = Workspace( - workspaceId: 0, - workspaceTitle: "workspaceTitle", - entries: [], - status: WorkspaceStatus.workable, - numApprovals: 0, - contributors: [], - pendingContributors: [], - references: [], - ); + bool titleReadOnly = true; + final TextEditingController titleController = TextEditingController(); + final FocusNode titleFocusNode = FocusNode(); - @override - void didChangeDependencies() { - getWorkspaceData(); - super.didChangeDependencies(); - } + bool newEntryOpen = false; + final FocusNode reviewFocusNode = FocusNode(); + final TextEditingController reviewController = TextEditingController(); - void getWorkspaceData() { - setState(() { - isLoading = true; - }); - workspaceData = Workspace( - workspaceId: 0, - workspaceTitle: "workspaceTitle", - entries: [ - Entry( - content: getLongLoremIpsum(), - entryDate: DateTime.now(), - entryId: 1, - entryNumber: 1, - index: 1, - isEditable: false, - isFinalEntry: false, - isProofEntry: false, - isTheoremEntry: true, - ), - Entry( - content: getLongLoremIpsum(2), - entryDate: DateTime.now(), - entryId: 2, - entryNumber: 2, - index: 2, - isEditable: false, - isFinalEntry: false, - isProofEntry: true, - isTheoremEntry: false, - ), - Entry( - content: getLongLoremIpsum(3), - entryDate: DateTime.now(), - entryId: 2, - entryNumber: 2, - index: 2, - isEditable: false, - isFinalEntry: true, - isProofEntry: true, - isTheoremEntry: false, - ), - ], - status: WorkspaceStatus.workable, - numApprovals: 0, - contributors: [ - // Automatically add the user to the list of contributors - // It will be deleted once the providers are implemented - if (!widget.pending) Provider.of(context).user as User, - User( - email: "dummy1@mail.com", - firstName: "dummy 1", - lastName: "jackson", - ), - User( - email: "dummy2@mail.com", - firstName: "dummy 2", - lastName: "jackson", - ), - ], - pendingContributors: [ - User( - email: "dummy3@mail.com", - firstName: "dummy 3", - lastName: "jackson", - ), - ], - references: [ - Node( - contributors: [ - User( - email: "dummy1@mail.com", - firstName: "dummy 1", - lastName: "jackson", - ), - User( - email: "dummy2@mail.com", - firstName: "dummy 2", - lastName: "jackson", - ), - ], - id: 1, - nodeTitle: "Awesome Node Title", - publishDate: DateTime.now(), - ), - ], - ); - setState(() { - isLoading = false; - }); + @override + void dispose() { + titleController.dispose(); + titleFocusNode.dispose(); + reviewController.dispose(); + reviewFocusNode.dispose(); + super.dispose(); } Widget addIcon(Function() onPressed) { return Center( child: IconButton( iconSize: 40.0, - onPressed: onPressed, - icon: const Icon(Icons.add), + onPressed: (widget.workspace.status != WorkspaceStatus.workable || widget.workspace.pending) + ? () {} + : onPressed, + icon: Icon( + Icons.add, + color: (widget.workspace.status != WorkspaceStatus.workable || widget.workspace.pending) + ? Colors.grey[200] + : Colors.grey[800], + ), ), ); } Widget firstAddition(String message, Function() onPressed) { - return ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - Center( - child: Text( - message, - style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, + return SizedBox( + height: 300, + child: ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + Center( + child: Text( + message, + style: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + ), ), ), - ), - addIcon(onPressed), - ], + addIcon(onPressed), + ], + ), ); } - Widget entryList() { - int length = workspaceData.entries.length; - Widget alertDialog = AppAlertDialog( - text: 'New Entry', - content: const EntryForm(newEntry: true), - actions: [ - AppButton( - text: "Create New Entry", - height: 40, - onTap: () { - /* Create Entry */ - Navigator.of(context).pop(); - }, - ), - ], + Widget semanticTagList() { + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: ListView.builder( + padding: const EdgeInsets.all(0.0), + shrinkWrap: true, + itemCount: widget.workspace.tags.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return SemanticTagCard( + + finalized: + widget.workspace.status != WorkspaceStatus.workable || widget.workspace.pending, + tag: widget.workspace.tags[index], + + backgroundColor: const Color.fromARGB(255, 220, 235, 220), + onDelete: () async { + await widget.removeSemanticTag(widget.workspace.tags[index].tagId); + }, + ); + }, + ), ); + } - return workspaceData.entries.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: ListView.builder( - padding: const EdgeInsets.all(0.0), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: length + 1, - itemBuilder: (context, index) => (index < length) - ? EntryCard( - entry: workspaceData.entries[index], - onDelete: () { - setState(() { - workspaceData.entries.removeAt(index); - }); + Widget entryList() { + int length = widget.workspace.entries.length; + return widget.workspace.entries.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: ListView.builder( + padding: const EdgeInsets.all(0.0), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: length + 1, + itemBuilder: (context, index) { + return (index < length) + ? MobileEntryCard( + finalized: widget.workspace.status != WorkspaceStatus.workable || + widget.workspace.pending, + entry: widget.workspace.entries[index], + onDelete: () async { + await widget.deleteEntry(widget.workspace.entries[index].entryId); + }, + editEntry: widget.editEntry, + setProof: widget.setProof, + setDisproof: widget.setDisproof, + setTheorem: widget.setTheorem, + removeProof: widget.removeProof, + removeDisproof: widget.removeDisproof, + removeTheorem: widget.removeTheorem, + deleteEntry: widget.deleteEntry, + fromNode: widget.workspace.fromNodeId != -1, + ) + : NewEntry( + onCreate: widget.createNewEntry, + backgroundColor: const Color.fromARGB(255, 220, 220, 240), + isMobile: true, + finalized: widget.workspace.status != WorkspaceStatus.workable || + widget.workspace.pending, + ); }, - pending: widget.pending, - ) : addIcon(() { - showDialog( - context: context, - builder: (context) => alertDialog - ); - }), - ), - ) : firstAddition( - "Add Your First Entry!", - () { - showDialog( - context: context, - builder: (context) => alertDialog - ); - }, - ); + ), + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + "Add Your First Entry!", + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + ), + NewEntry( + onCreate: widget.createNewEntry, + backgroundColor: const Color.fromARGB(255, 220, 220, 240), + isMobile: true, + finalized: + widget.workspace.status != WorkspaceStatus.workable || widget.workspace.pending, + ), + ], + ); } Widget contributorList() { - int length = workspaceData.contributors.length; - Widget alertDialog = const AppAlertDialog( + int length = widget.workspace.contributors.length; + int pendingLength = widget.workspace.pendingContributors.length; + + Widget alertDialog = AppAlertDialog( text: "Send Collaboration Request", - content: SendCollaborationRequestForm(), + content: + SendCollaborationRequestForm(sendCollaborationRequest: widget.sendCollaborationRequest), ); - return workspaceData.contributors.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: ListView.builder( - padding: const EdgeInsets.all(0.0), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: length + 1, - itemBuilder: (context, index) => (index < length) - ? ContributorCard(contributor: workspaceData.contributors[index]) - : addIcon(() { - showDialog( - context: context, - builder: (context) => alertDialog - ); - } - ), - ), - ) : firstAddition( - "Add The First Contributor!", - () { - showDialog( - context: context, - builder: (context) => alertDialog - ); - }, + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: ListView.builder( + padding: const EdgeInsets.all(0.0), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: length + pendingLength + 1, + itemBuilder: (context, index) => (index < length) + ? ContributorCard( + contributor: widget.workspace.contributors[index], + pending: false, + workspacePending: widget.workspace.pending, + updateRequest: widget.updateRequest, + ) + : (index < length + pendingLength) + ? ContributorCard( + contributor: widget.workspace.pendingContributors[index - length], + pending: true, + workspacePending: widget.workspace.pending, + updateRequest: widget.updateRequest, + + ) + : addIcon(() { + showDialog(context: context, builder: (context) => alertDialog); + }), + ), ); } Widget referenceList() { - int length = workspaceData.references.length; - Widget alertDialog = const AppAlertDialog - (text: "Add Reference", - content: AddReferenceForm(), + int length = widget.workspace.references.length; + Widget alertDialog = AppAlertDialog( + text: "Add Reference", + content: AddReferenceForm(onAdd: widget.addReference), ); - - return (workspaceData.references.isNotEmpty) - ? Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: ListView.builder( - padding: const EdgeInsets.all(0.0), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: length + 1, - itemBuilder: (context, index) => (index < length) - ? ReferenceCard(reference: workspaceData.references[index]) - : addIcon(() { - showDialog( - context: context, - builder: (context) => alertDialog - ); - }), + return widget.workspace.references.isNotEmpty + ? (Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: ListView.builder( + padding: const EdgeInsets.all(0.0), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: length + 1, + itemBuilder: (context, index) => (index < length) + ? ReferenceCard(reference: widget.workspace.references[index]) + : addIcon(() { + showDialog(context: context, builder: (context) => alertDialog); + }), ), - ) : firstAddition( - "Add Your First Reference!", - () { - showDialog( - context: context, - builder: (context) => alertDialog - ); - }, - ); + )) + : firstAddition( + "Add Your First Reference!", + () { + showDialog(context: context, builder: (context) => alertDialog); + }, + ); } @override Widget build(BuildContext context) { - print("Created ${widget.workspaceId}"); if (isLoading || error) { return Center( - child: isLoading ? const CircularProgressIndicator() - : error ? SelectableText(errorMessage) - : const SelectableText("Something went wrong!") - ); + child: isLoading + ? const CircularProgressIndicator() + : error + ? SelectableText(errorMessage) + : const SelectableText("Something went wrong!")); } else { return SizedBox( width: Responsive.getGenericPageWidth(context), @@ -315,22 +314,273 @@ class _MobileWorkspaceContentState extends State { padding: const EdgeInsets.all(0.0), // It needs to be nested scrollable in the future children: [ - const SizedBox(height: 10.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Column(children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: titleReadOnly + ? [ + SizedBox( + width: Responsive.getGenericPageWidth(context) - 150, + child: Text( + widget.workspace.workspaceTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyles.title2, + ), + ), + if (widget.workspace.status == WorkspaceStatus.workable && + !widget.workspace.pending) + IconButton( + onPressed: () { + setState(() { + titleController.text = widget.workspace.workspaceTitle; + titleReadOnly = false; + }); + }, + icon: const Icon(Icons.edit)), + ] + : [ + SizedBox( + width: 300, + height: 80, + child: AppTextField( + controller: titleController, + focusNode: titleFocusNode, + hintText: "", + obscureText: false, + height: 200), + ), + if (!widget.workspace.pending) + SizedBox( + width: 50, + height: 50, + child: IconButton( + onPressed: () { + widget.editTitle(titleController.text); + widget.workspace.workspaceTitle = titleController.text; + setState(() { + titleReadOnly = true; + }); + }, + icon: const Icon(Icons.save), + ), + ) + ], + ), + if (!widget.workspace.pending && widget.workspace.requestId == -1) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 4.0), + child: AppButton( + height: 40, + isActive: widget.workspace.status == WorkspaceStatus.workable || + widget.workspace.status == WorkspaceStatus.finalized || + widget.workspace.status == WorkspaceStatus.rejected, + text: widget.pending + ? "Accept Workspace" + : widget.workspace.status == WorkspaceStatus.workable + ? "Finalize Workspace" + : widget.workspace.status == WorkspaceStatus.finalized + ? "Send to Review" + : (widget.workspace.status == WorkspaceStatus.rejected + ? "Reset Workspace" + : (widget.workspace.status == WorkspaceStatus.inReview + ? "In Review" + : "Published")), + + onTap: widget.pending + ? () { + // accept or reject the review + showDialog( + context: context, + builder: (context) => AppAlertDialog( + text: "Do you accept the work?", + actions: [ + AppButton( + text: "Accept", + height: 40, + onTap: () { + /* Send to review */ + Navigator.of(context).pop(); + }, + ), + AppButton( + text: "Reject", + height: 40, + onTap: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + } + : () { + showDialog( + context: context, + builder: (context) => AppAlertDialog( + text: widget.workspace.status == WorkspaceStatus.workable + ? "Do you want to finalize the workspace?" + + : (widget.workspace.status == WorkspaceStatus.finalized + ? "Do you want to send it to review?" + : "Do you want to reset the workspace?"), + + actions: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: AppButton( + text: "Yes", + height: 40, + onTap: () { + if (widget.workspace.status == WorkspaceStatus.workable) { + widget.finalizeWorkspace(); + Navigator.of(context).pop(); + } else if (widget.workspace.status == + WorkspaceStatus.finalized) { + /* Send to review */ + widget.sendWorkspaceToReview(); + Navigator.of(context).pop(); + } else if (widget.workspace.status == + WorkspaceStatus.rejected) { + widget.resetWorkspace(); + Navigator.of(context).pop(); + + } + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: AppButton( + text: "No", + height: 40, + onTap: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ); + }, + ), + ), + if (!widget.workspace.pending && + widget.workspace.requestId != -1 && + widget.workspace.status == WorkspaceStatus.inReview) + + /** adjust it to check if the user is reviewer of this workspace */ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 4.0), + child: AppButton( + isActive: widget.workspace.status == WorkspaceStatus.inReview, + text: "Review Workspace", + height: 40, + onTap: () { + showDialog( + context: context, + builder: (context) => AppAlertDialog( + text: "Review Workspace", + content: AppTextField( + controller: reviewController, + focusNode: reviewFocusNode, + hintText: "Your Review", + obscureText: false, + height: 200, + maxLines: 10, + ), + actions: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: AppButton( + text: "Approve Workspace", + height: 40, + onTap: () { + /** Approve workspace*/ + widget.addReview(widget.workspace.requestId, + RequestStatus.approved, reviewController.text); + Navigator.of(context).pop(); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: AppButton( + text: "Reject Workspace", + height: 40, + type: "outlined", + onTap: () { + /** Reject workspace*/ + widget.addReview(widget.workspace.requestId, + RequestStatus.rejected, reviewController.text); + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ); + }, + type: "primary", + ), + ), + if (widget.workspace.comments.isNotEmpty && widget.workspace.requestId == -1) + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + widget.displayCommentSidebar(); + }, + child: const Text( + "See Review Comments", + style: + TextStyle(fontWeight: FontWeight.bold, color: AppColors.hyperTextColor), + ), + ), + ), + ]), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: Divider(), + ), const SubSectionTitle(title: "Entries"), entryList(), const Padding( padding: EdgeInsets.symmetric(horizontal: 12.0), child: Divider(), ), + const SubSectionTitle(title: "Semantic Tags"), + if (widget.workspace.status == WorkspaceStatus.workable) Padding( + padding: const EdgeInsets.all(8.0), + child: SemanticSearchBar(addSemanticTag: widget.addSemanticTag), + ), + Center( + child: Text( + (widget.workspace.tags.isNotEmpty) ? "Added Tags" : "You haven't added any tag yet!", + style: TextStyles.bodySecondary, + ), + ), + semanticTagList(), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: Divider(), + ), + if (widget.workspace.requestId == -1 || widget.workspace.pendingContributor) const SubSectionTitle(title: "Contributors"), + if (widget.workspace.requestId == -1 || widget.workspace.pendingContributor) contributorList(), + if (widget.workspace.requestId == -1 || widget.workspace.pendingContributor) const Padding( padding: EdgeInsets.symmetric(horizontal: 12.0), child: Divider(), ), const SubSectionTitle(title: "References"), referenceList(), - const SizedBox(height: 20.0), + const SizedBox(height: 100.0), ], ), ); diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/new_entry.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/new_entry.dart new file mode 100644 index 00000000..e2539786 --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/new_entry.dart @@ -0,0 +1,255 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_tex/flutter_tex.dart'; + +import 'package:collaborative_science_platform/utils/colors.dart'; +import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; +import 'package:collaborative_science_platform/widgets/app_button.dart'; +import 'package:collaborative_science_platform/widgets/card_container.dart'; + +class NewEntry extends StatefulWidget { + final Function onCreate; + final Color backgroundColor; + final bool isMobile; + final bool finalized; + + const NewEntry({ + super.key, + required this.onCreate, + required this.backgroundColor, + required this.isMobile, + required this.finalized, + }); + + @override + State createState() => _NewEntryState(); +} + +class _NewEntryState extends State { + final controller = TextEditingController(); + final focusNode = FocusNode(); + + bool open = false; + bool editMode = true; + bool entryLoading = false; + + @override + void dispose() { + controller.dispose(); + focusNode.dispose(); + super.dispose(); + } + + Widget upperIconRow() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + setState(() { + editMode = false; + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Preview", + style: TextStyle( + color: (editMode) ? Colors.grey : Colors.indigo[600], + fontSize: 16.0, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(width: 6.0), + Icon( + Icons.visibility, + color: (editMode) ? Colors.grey : Colors.indigo[600], + ) + ], + ), + ), + ), + const SizedBox(width: 10.0), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + setState(() { + editMode = true; + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Write", + style: TextStyle( + color: (!editMode) ? Colors.grey : Colors.indigo[600], + fontSize: 16.0, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(width: 6.0), + Icon( + Icons.edit, + color: (!editMode) ? Colors.grey : Colors.indigo[600], + ) + ], + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () async { + setState(() { + entryLoading = true; + }); + await widget.onCreate(controller.text); + setState(() { + open = false; + entryLoading = false; + }); + }, + icon: const Icon( + Icons.check, + color: Colors.green, + ), + ), + IconButton( + onPressed: () { + setState(() { + open = false; + }); + }, + icon: const Icon( + Icons.close, + color: Colors.red, + ), + ), + ], + ), + ], + ); + } + + Widget button() { + return (widget.isMobile) + ? Center( + child: IconButton( + iconSize: 40.0, + onPressed: widget.finalized + ? () {} + : () { + setState(() { + open = true; + editMode = true; + }); + }, + icon: Icon( + Icons.add, + color: widget.finalized ? Colors.grey[200] : Colors.grey[800], + ), + ), + ) + : Center( + child: SizedBox( + width: 300.0, + child: AppButton( + isActive: !widget.finalized, + text: "New Entry", + height: 40, + type: "outlined", + onTap: () { + setState(() { + open = true; + editMode = true; + }); + }, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return entryLoading + ? const Center(child: CircularProgressIndicator()) + : !open + ? button() + : Padding( + padding: const EdgeInsets.all(12.0), + child: CardContainer( + backgroundColor: widget.backgroundColor, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Proof or Theorem + upperIconRow(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: (editMode) + ? TextField( + controller: controller, + focusNode: focusNode, + cursorColor: Colors.grey.shade700, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.0, + ), + maxLines: 10, + decoration: InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: AppColors.primaryColor), + borderRadius: BorderRadius.circular(4.0), + ), + focusedBorder: OutlineInputBorder( + borderSide: + const BorderSide(color: AppColors.secondaryDarkColor), + borderRadius: BorderRadius.circular(4.0), + ), + ), + ) + : Container( + constraints: BoxConstraints( + minHeight: 100.0, // Set the minimum height here + maxHeight: + (Responsive.isMobile(context)) ? double.infinity : 600, + ), + child: SingleChildScrollView( + child: TeXView( + renderingEngine: const TeXViewRenderingEngine.katex(), + child: TeXViewDocument( + utf8.decode(controller.text.codeUnits), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/reference_card.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/reference_card.dart index 8d86161b..0df4eaab 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/reference_card.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/reference_card.dart @@ -1,7 +1,8 @@ import 'package:collaborative_science_platform/models/node.dart'; +import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import '../../../node_details_page/node_details_page.dart'; +import 'package:collaborative_science_platform/screens/node_details_page/node_details_page.dart'; class ReferenceCard extends StatelessWidget { final Node reference; @@ -14,35 +15,34 @@ class ReferenceCard extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - child: SizedBox( - height: 100.0, - child: Card( - elevation: 4.0, - //shadowColor: AppColors.primaryColor, - //color: AppColors.primaryLightColor, - shape: RoundedRectangleBorder( + child: Card( + elevation: 4.0, + //shadowColor: AppColors.primaryColor, + //color: AppColors.primaryLightColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: InkWell( + onTap: () { + // Navigate to the node page of the theorem + context.push('${NodeDetailsPage.routeName}/${reference.id}'); + }, + customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0), ), - child: InkWell( - onTap: () { - // Navigate to the node page of the theorem - context.push('${NodeDetailsPage.routeName}/${reference.id}'); - }, - customBorder: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(width: 4.0), - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: Responsive.getGenericPageWidth(context)-100, + child: Text( reference.nodeTitle, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -51,42 +51,50 @@ class ReferenceCard extends StatelessWidget { fontSize: 18.0, ), ), - const SizedBox(height: 2.0), - Text( - "By ${reference.contributors.map( - (user) => "${user.firstName} ${user.lastName}" - ).join(", ")}", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16.0, - color: Colors.grey, - ), + ), + const SizedBox(height: 2.0), + SizedBox( + width: MediaQuery.of(context).size.width-100, + child: ListView.builder( + scrollDirection: Axis.vertical, + shrinkWrap: true, + itemCount: reference.contributors.length, + itemBuilder: (context, index) => Text( + "${reference.contributors[index].firstName} ${reference.contributors[index].lastName}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.0, + color: Colors.grey, + ), + ), ), - const SizedBox(height: 2.0), - Text( - reference.publishDateFormatted, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16.0, - color: Colors.grey, - ), - // textAlign: TextAlign.start, + ), + const SizedBox(height: 2.0), + Text( + reference.publishDateFormatted, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.0, + color: Colors.grey, ), - ], - ), - const Expanded(child: SizedBox(width: 4.0)), - IconButton( - onPressed: () { //remove reference + // textAlign: TextAlign.start, + ), + ], + ), + IconButton( + onPressed: () { //remove reference - }, - icon: const Icon(Icons.delete), - ) - ], - ), + }, + icon: const Icon( + Icons.delete, + color: Colors.grey, + ), + ) + ], ), ), ), diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/semantic_tag_card.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/semantic_tag_card.dart new file mode 100644 index 00000000..6331d60c --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/mobile_workspace_page/widget/semantic_tag_card.dart @@ -0,0 +1,65 @@ +import 'package:collaborative_science_platform/models/workspace_semantic_tag.dart'; +import 'package:flutter/material.dart'; + +class SemanticTagCard extends StatefulWidget { + final WorkspaceSemanticTag tag; + final Function() onDelete; + final Color backgroundColor; + final bool finalized; + + const SemanticTagCard({ + required this.tag, + required this.onDelete, + required this.backgroundColor, + required this.finalized, + super.key, + }); + + @override + State createState() => _SemanticTagCardState(); +} + +class _SemanticTagCardState extends State { + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Card( + elevation: 4.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget.tag.label, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18.0, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (!widget.finalized) + IconButton( + onPressed: () { + widget.onDelete(); + setState(() { }); + }, + icon: const Icon( + Icons.delete, + color: Colors.grey, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/web_workspace_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/web_workspace_page.dart index c1aaaf86..33f79e76 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/web_workspace_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/web_workspace_page.dart @@ -3,23 +3,78 @@ import 'package:collaborative_science_platform/models/workspaces_page/workspaces import 'package:collaborative_science_platform/screens/home_page/widgets/home_page_appbar.dart'; import 'package:collaborative_science_platform/screens/page_with_appbar/page_with_appbar.dart'; import 'package:collaborative_science_platform/screens/page_with_appbar/widgets/app_bar_button.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/app_alert_dialog.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/comments_sidebar.dart'; import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/contributors_list_view.dart'; import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/entries_list_view.dart'; import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/references_list_view.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/semantic_tag_list_view.dart'; import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/workspaces_side_bar.dart'; +import 'package:collaborative_science_platform/utils/colors.dart'; import 'package:collaborative_science_platform/utils/text_styles.dart'; import 'package:collaborative_science_platform/widgets/app_button.dart'; +import 'package:collaborative_science_platform/widgets/app_text_field.dart'; +import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; import 'package:flutter/cupertino.dart'; - import 'package:flutter/material.dart'; class WebWorkspacePage extends StatefulWidget { final Workspace? workspace; final Workspaces? workspaces; final bool isLoading; + final Function createNewWorkspace; + final Function createNewEntry; + final Function editEntry; + final Function deleteEntry; + final Function addReference; + final Function deleteReference; + final Function editTitle; + final Function sendCollaborationRequest; + final Function finalizeWorkspace; + final Function addSemanticTag; + final Function removeSemanticTag; + final Function sendWorkspaceToReview; + final Function addReview; + final Function updateReviewRequest; + final Function updateCollaborationRequest; + + final Function resetWorkspace; + + final Function setProof; + final Function setDisproof; + final Function setTheorem; + final Function removeDisproof; + final Function removeTheorem; + final Function removeProof; - const WebWorkspacePage( - {super.key, required this.workspace, required this.workspaces, required this.isLoading}); + const WebWorkspacePage({ + super.key, + required this.workspace, + required this.workspaces, + required this.isLoading, + required this.createNewWorkspace, + required this.createNewEntry, + required this.editEntry, + required this.deleteEntry, + required this.addReference, + required this.deleteReference, + required this.editTitle, + required this.addSemanticTag, + required this.removeSemanticTag, + required this.finalizeWorkspace, + required this.sendCollaborationRequest, + required this.sendWorkspaceToReview, + required this.addReview, + required this.updateReviewRequest, + required this.updateCollaborationRequest, + required this.removeDisproof, + required this.removeProof, + required this.removeTheorem, + required this.setDisproof, + required this.setProof, + required this.setTheorem, + required this.resetWorkspace, + }); @override State createState() => _WebWorkspacePageState(); @@ -30,21 +85,34 @@ class _WebWorkspacePageState extends State { ScrollController controller2 = ScrollController(); ScrollController controller3 = ScrollController(); ScrollController controller4 = ScrollController(); - + ScrollController controller5 = ScrollController(); bool _isFirstTime = true; bool error = false; String errorMessage = ""; bool showSidebar = true; + bool showCommentSidebar = false; double minHeight = 750; + bool titleReadOnly = true; + TextEditingController titleController = TextEditingController(); + TextEditingController reviewController = TextEditingController(); + + FocusNode titleFocusNode = FocusNode(); + FocusNode reviewFocusNode = FocusNode(); + @override void dispose() { controller1.dispose(); controller2.dispose(); controller3.dispose(); controller4.dispose(); + controller5.dispose(); + titleController.dispose(); + titleFocusNode.dispose(); + reviewController.dispose(); + reviewFocusNode.dispose(); super.dispose(); } @@ -68,6 +136,12 @@ class _WebWorkspacePageState extends State { }); } + hideCommentsSideBar() { + setState(() { + showCommentSidebar = false; + }); + } + @override Widget build(BuildContext context) { return PageWithAppBar( @@ -93,6 +167,9 @@ class _WebWorkspacePageState extends State { hideSidebar: hideSideBar, height: minHeight, workspaces: widget.workspaces, + createNewWorkspace: widget.createNewWorkspace, + updateReviewRequest: widget.updateReviewRequest, + updateCollaborationRequest: widget.updateCollaborationRequest, ), if (!showSidebar) Container( @@ -118,6 +195,7 @@ class _WebWorkspacePageState extends State { onPressed: () { setState(() { showSidebar = true; + showCommentSidebar = false; }); }, icon: CupertinoIcons.forward, @@ -144,48 +222,278 @@ class _WebWorkspacePageState extends State { height: 100, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(widget.workspace!.workspaceTitle, style: TextStyles.title2), - SizedBox( - width: MediaQuery.of(context).size.width / 5, - child: AppButton( - text: "Send Workspace to Review", - height: 45, - onTap: () {}, - type: "primary", + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: titleReadOnly + ? [ + Text(widget.workspace!.workspaceTitle, + style: TextStyles.title2), + if (widget.workspace!.status == + WorkspaceStatus.workable && + !widget.workspace!.pending) + IconButton( + onPressed: () { + setState(() { + titleController.text = + widget.workspace!.workspaceTitle; + + titleReadOnly = false; + }); + }, + icon: const Icon(Icons.edit)), + ] + : [ + SizedBox( + width: 300, + height: 80, + child: AppTextField( + controller: titleController, + focusNode: titleFocusNode, + hintText: "", + obscureText: false, + height: 200), + ), + if (!widget.workspace!.pending && + !widget.workspace!.pendingContributor && + !widget.workspace!.pendingReviewer) + SizedBox( + width: 50, + height: 50, + child: IconButton( + onPressed: () { + widget.editTitle(titleController.text); + widget.workspace!.workspaceTitle = + titleController.text; + setState(() { + titleReadOnly = true; + }); + }, + icon: const Icon(Icons.save)), + ) + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (!widget.workspace!.pending && + widget.workspace!.requestId == -1 && + (widget.workspace!.status == WorkspaceStatus.workable || + widget.workspace!.status == + WorkspaceStatus.finalized || + widget.workspace!.status == WorkspaceStatus.inReview)) + SizedBox( + width: MediaQuery.of(context).size.width / 5, + child: AppButton( + isActive: widget.workspace!.status == + WorkspaceStatus.workable || + widget.workspace!.status == WorkspaceStatus.finalized || + widget.workspace!.status == WorkspaceStatus.rejected, + text: + widget.workspace!.status == WorkspaceStatus.workable + ? ((MediaQuery.of(context).size.width > + Responsive.desktopPageWidth) + ? "Finalize Workspace" + : "Finalize") + : widget.workspace!.status == WorkspaceStatus.finalized + ? ((MediaQuery.of(context).size.width > + Responsive.desktopPageWidth) + ? "Send to Review" + : "Send") + : (widget.workspace!.status == + WorkspaceStatus.rejected + ? ((MediaQuery.of(context).size.width > + Responsive.desktopPageWidth) + ? "Reset Workspace" + : "Workspace") + : (widget.workspace!.status == + WorkspaceStatus.inReview + ? "In Review" + : "Published")), + height: 45, + onTap: () { + /* finalize workspace*/ + if (widget.workspace!.status == + WorkspaceStatus.workable) { + widget.finalizeWorkspace(); + } + /*send workspace to review */ + else if (widget.workspace!.status == + WorkspaceStatus.finalized) { + widget.sendWorkspaceToReview(); + } else if (widget.workspace!.status == + WorkspaceStatus.rejected) { + widget.resetWorkspace(); + } + }, + type: "primary", + ), ), + if (!widget.workspace!.pending && + widget.workspace!.requestId != -1 && + widget.workspace!.status == WorkspaceStatus.inReview) + SizedBox( + width: MediaQuery.of(context).size.width / 5, + child: AppButton( + isActive: widget.workspace!.status == + WorkspaceStatus.inReview, + text: (MediaQuery.of(context).size.width > + Responsive.desktopPageWidth) + ? "Review Workspace" + : "Review", + height: 45, + onTap: () { + showDialog( + context: context, + builder: (context) => AppAlertDialog( + text: "Review Workspace", + content: AppTextField( + controller: reviewController, + focusNode: reviewFocusNode, + hintText: "Your Review", + obscureText: false, + height: 200, + maxLines: 10, + ), + actions: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: AppButton( + text: "Approve Workspace", + height: 40, + onTap: () { + /** Approve workspace*/ + widget.addReview( + widget.workspace!.requestId, + RequestStatus.approved, + reviewController.text); + Navigator.of(context).pop(); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: AppButton( + text: "Reject Workspace", + height: 40, + type: "outlined", + onTap: () { + /** Reject workspace*/ + widget.addReview( + widget.workspace!.requestId, + RequestStatus.rejected, + reviewController.text); + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ); + }, + type: "primary", + ), + ), + if (widget.workspace!.comments.isNotEmpty && + !showCommentSidebar && + widget.workspace!.requestId == -1) + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + setState(() { + showSidebar = false; + showCommentSidebar = true; + }); + }, + child: const Text( + "See Review Comments", + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppColors.hyperTextColor), + ), + ), + ), + ], ), + ], ), ), Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ EntriesListView( entries: widget.workspace!.entries, controller: controller2, - showSidebar: showSidebar, + showSidebar: showSidebar || showCommentSidebar, height: minHeight, + createNewEntry: widget.createNewEntry, + editEntry: widget.editEntry, + deleteEntry: widget.deleteEntry, + finalized: widget.workspace!.status != WorkspaceStatus.workable || + widget.workspace!.pending, + + setProof: widget.setProof, + setDisproof: widget.setDisproof, + setTheorem: widget.setTheorem, + removeProof: widget.removeProof, + removeDisproof: widget.removeDisproof, + removeTheorem: widget.removeTheorem, + fromNode: widget.workspace!.fromNodeId != -1, ), Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ + SemanticTagListView( + finalized: widget.workspace!.status != WorkspaceStatus.workable, + tags: widget.workspace!.tags, + addSemanticTag: widget.addSemanticTag, + removeSemanticTag: widget.removeSemanticTag, + height: minHeight, + ), + if (widget.workspace!.requestId == -1 || + widget.workspace!.pendingContributor) ContributorsListView( - contributors: widget.workspace!.contributors, + finalized: + widget.workspace!.status != WorkspaceStatus.workable || + widget.workspace!.pending, + contributors: widget.workspace!.contributors, pendingContributors: widget.workspace!.pendingContributors, controller: controller3, - height: minHeight / 2, + height: minHeight / 3, + sendCollaborationRequest: widget.sendCollaborationRequest, + updateRequest: widget.updateCollaborationRequest, ), ReferencesListView( references: widget.workspace!.references, controller: controller4, - height: minHeight / 2, + height: (widget.workspace!.requestId == -1) + ? minHeight / 3 + : minHeight / 2, + addReference: widget.addReference, + deleteReference: widget.deleteReference, + finalized: + widget.workspace!.status != WorkspaceStatus.workable || + widget.workspace!.pending, ), ], ) ], ) ], - ) - else + ), + if (showCommentSidebar && widget.workspace != null) + CommentsSideBar( + controller: controller5, + height: minHeight, + hideSidebar: hideCommentsSideBar, + comments: widget.workspace!.comments), + if (widget.workspace == null) SizedBox( width: showSidebar ? MediaQuery.of(context).size.width * 0.75 diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/add_reference_form.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/add_reference_form.dart index 4f32b5d3..1e14e75b 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/add_reference_form.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/add_reference_form.dart @@ -6,7 +6,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class AddReferenceForm extends StatefulWidget { - const AddReferenceForm({super.key}); + final Function onAdd; + const AddReferenceForm({super.key, required this.onAdd}); @override State createState() => _AddReferenceFormState(); @@ -113,7 +114,8 @@ class _AddReferenceFormState extends State { overflow: TextOverflow.ellipsis, ), Text( - nodeProvider.searchNodeResult[index].publishDateFormatted, + nodeProvider + .searchNodeResult[index].publishDateFormatted, style: TextStyles.bodyGrey, textAlign: TextAlign.start, maxLines: 1, @@ -124,7 +126,9 @@ class _AddReferenceFormState extends State { ), IconButton( onPressed: () { - //add reference + widget.onAdd(nodeProvider.searchNodeResult[index].id); + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); }, icon: Icon( Icons.add, diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/comments_sidebar.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/comments_sidebar.dart new file mode 100644 index 00000000..3d7097fa --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/comments_sidebar.dart @@ -0,0 +1,108 @@ +import 'package:collaborative_science_platform/models/workspaces_page/comment.dart'; +import 'package:collaborative_science_platform/models/workspaces_page/workspace.dart'; +import 'package:collaborative_science_platform/screens/page_with_appbar/widgets/app_bar_button.dart'; +import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; +import 'package:collaborative_science_platform/utils/text_styles.dart'; +import 'package:collaborative_science_platform/widgets/card_container.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class CommentsSideBar extends StatefulWidget { + final ScrollController controller; + final Function? hideSidebar; + final double height; + final List comments; + + const CommentsSideBar({ + super.key, + required this.controller, + this.hideSidebar, + required this.height, + required this.comments, + }); + + @override + State createState() => _CommentsSideBarState(); +} + +class _CommentsSideBarState extends State { + @override + Widget build(BuildContext context) { + return Container( + height: widget.height + 100, + width: MediaQuery.of(context).size.width * 0.2, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(5), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.8), + blurRadius: 7, + offset: const Offset(0, 2), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + AppBarButton( + onPressed: () { + widget.hideSidebar!(); + }, + icon: CupertinoIcons.forward, + text: "hide comments", + ), + SizedBox( + width: Responsive.isDesktop(context) + ? MediaQuery.of(context).size.width * 0.10 + : MediaQuery.of(context).size.width * 0.8, + child: (MediaQuery.of(context).size.width >= Responsive.desktopPageWidth) + ? const Text("Reviewer comments", style: TextStyles.title4secondary) + : const Text("Reviewer comments", style: TextStyles.bodySecondary), + ) + ], + ), + SizedBox( + height: widget.height * 0.9, + child: ListView.builder( + scrollDirection: Axis.vertical, + shrinkWrap: true, + padding: const EdgeInsets.all(8), + itemCount: (widget.comments.length), + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(5), + child: CardContainer( + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.18, + child: Column( + children: [ + Text( + widget.comments[index].comment, + ), + if (widget.comments[index].response == RequestStatus.approved) + const Text( + "Approved", + style: TextStyle(color: Colors.green), + textAlign: TextAlign.end, + ), + if (widget.comments[index].response == RequestStatus.rejected) + const Text( + "Rejected", + style: TextStyle(color: Colors.red), + textAlign: TextAlign.end, + ) + ], + ), + ), + ), + ); + })), + ], + ))); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/contributors_list_view.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/contributors_list_view.dart index b47bfb13..28b083ab 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/contributors_list_view.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/contributors_list_view.dart @@ -1,32 +1,40 @@ import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/models/workspaces_page/workspace.dart'; import 'package:collaborative_science_platform/screens/profile_page/profile_page.dart'; import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/send_collaboration_request_form.dart'; import 'package:collaborative_science_platform/utils/colors.dart'; +import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; import 'package:collaborative_science_platform/utils/text_styles.dart'; import 'package:collaborative_science_platform/widgets/app_button.dart'; import 'package:collaborative_science_platform/widgets/card_container.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; - -import '../../mobile_workspace_page/widget/app_alert_dialog.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/app_alert_dialog.dart'; class ContributorsListView extends StatelessWidget { final List contributors; final List pendingContributors; final ScrollController controller; final double height; - const ContributorsListView( - {super.key, - required this.contributors, - required this.pendingContributors, - required this.controller, - required this.height}); + final Function updateRequest; + final Function sendCollaborationRequest; + final bool finalized; + const ContributorsListView({ + super.key, + required this.contributors, + required this.pendingContributors, + required this.controller, + required this.height, + required this.sendCollaborationRequest, + required this.updateRequest, + required this.finalized, + }); @override Widget build(BuildContext context) { return Container( - height: height, + // height: height, width: MediaQuery.of(context).size.width / 4, decoration: BoxDecoration(color: Colors.grey[100]), child: Padding( @@ -34,37 +42,40 @@ class ContributorsListView extends StatelessWidget { child: Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ const Text("Contributors", style: TextStyles.title4secondary), SizedBox( - height: (height * 2) / 3, + //height: (height * 3) / 5, child: ListView.builder( controller: controller, scrollDirection: Axis.vertical, shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.all(3), itemCount: contributors.length + pendingContributors.length, itemBuilder: (BuildContext context, int index) { if (index < contributors.length) { return Padding( - padding: const EdgeInsets.all(3), - child: CardContainer( - onTap: () { - final String email = contributors[index].email; - final String encodedEmail = Uri.encodeComponent(email); - context.push('${ProfilePage.routeName}/$encodedEmail'); - }, - child: Column( - children: [ - Text( - "${contributors[index].firstName} ${contributors[index].lastName}", - style: TextStyles.bodyBold, - ), - Text( - contributors[index].email, - style: TextStyles.bodyGrey, - ) - ], + padding: const EdgeInsets.all(3), + child: CardContainer( + onTap: () { + final String email = contributors[index].email; + final String encodedEmail = Uri.encodeComponent(email); + context.push('${ProfilePage.routeName}/$encodedEmail'); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${contributors[index].firstName} ${contributors[index].lastName}", + style: TextStyles.bodyBold, + ), + Text( + contributors[index].email, + style: TextStyles.bodyGrey, + ) + ], + ), ), - ), - ); + ); } else { return Padding( padding: const EdgeInsets.all(3), @@ -81,6 +92,8 @@ class ContributorsListView extends StatelessWidget { SizedBox( width: MediaQuery.of(context).size.width / 8, child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "${pendingContributors[index - contributors.length].firstName} ${pendingContributors[index - contributors.length].lastName}", @@ -93,37 +106,44 @@ class ContributorsListView extends StatelessWidget { ], ), ), - Column(children: [ - IconButton( - icon: const Icon( - CupertinoIcons.clear_circled, - color: AppColors.warningColor, + if (!finalized) + Column(children: [ + IconButton( + icon: const Icon( + CupertinoIcons.clear_circled, + color: AppColors.warningColor, + ), + onPressed: () async { + // function to delete collaboration request + //TODO - requests id's are absent for now. + await updateRequest( + pendingContributors[index - contributors.length] + .requestId, + RequestStatus.rejected); + }, ), - onPressed: () { - // function to delete collaboration request - }, - ), - ]) + ]) ], ), ), ); } - }), ), SizedBox( width: MediaQuery.of(context).size.width / 6, child: AppButton( - text: "Send Collaboration Request", + isActive: !finalized, + text: "Collaborate", height: 40, type: "outlined", onTap: () { showDialog( context: context, - builder: (context) => const AppAlertDialog( + builder: (context) => AppAlertDialog( text: "Send Collaboration Request", - content: SendCollaborationRequestForm(), + content: SendCollaborationRequestForm( + sendCollaborationRequest: sendCollaborationRequest), ), ); }, diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/create_workspace_form.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/create_workspace_form.dart index 1b4d187b..7de29c40 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/create_workspace_form.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/create_workspace_form.dart @@ -2,20 +2,19 @@ import 'package:collaborative_science_platform/widgets/app_text_field.dart'; import 'package:flutter/material.dart'; class CreateWorkspaceForm extends StatefulWidget { - const CreateWorkspaceForm({super.key}); + final TextEditingController titleController; + const CreateWorkspaceForm({super.key, required this.titleController}); @override State createState() => _CreateWorkspaceFormState(); } class _CreateWorkspaceFormState extends State { - final titleController = TextEditingController(); final titleFocusNode = FocusNode(); @override void dispose() { - titleController.dispose(); titleFocusNode.dispose(); super.dispose(); @@ -30,11 +29,11 @@ class _CreateWorkspaceFormState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ AppTextField( - controller: titleController, - focusNode: titleFocusNode, - hintText: 'Workspace Title', - obscureText: false, - height: 64, + controller: widget.titleController, + focusNode: titleFocusNode, + hintText: 'Workspace Title', + obscureText: false, + height: 64, ), ], )); diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/entries_list_view.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/entries_list_view.dart index 8029fddf..6ca5df38 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/entries_list_view.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/entries_list_view.dart @@ -1,162 +1,122 @@ import 'package:collaborative_science_platform/models/workspaces_page/entry.dart'; -import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/entry_form.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/mobile_entry_card.dart'; import 'package:collaborative_science_platform/utils/text_styles.dart'; -import 'package:collaborative_science_platform/widgets/app_button.dart'; -import 'package:collaborative_science_platform/widgets/card_container.dart'; import 'package:flutter/material.dart'; -import '../../mobile_workspace_page/widget/app_alert_dialog.dart'; +import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/new_entry.dart'; -class EntriesListView extends StatelessWidget { +class EntriesListView extends StatefulWidget { final List entries; final ScrollController controller; final bool showSidebar; final double height; - const EntriesListView( - {super.key, - required this.entries, - required this.controller, - required this.showSidebar, - required this.height}); + final Function createNewEntry; + final Function editEntry; + final Function deleteEntry; + final bool finalized; + final Function setProof; + final Function setDisproof; + final Function setTheorem; + final Function removeDisproof; + final Function removeTheorem; + final Function removeProof; + final bool fromNode; + const EntriesListView({ + super.key, + required this.entries, + required this.controller, + required this.showSidebar, + required this.height, + required this.createNewEntry, + required this.editEntry, + required this.deleteEntry, + required this.finalized, + required this.removeDisproof, + required this.removeProof, + required this.removeTheorem, + required this.setDisproof, + required this.setProof, + required this.setTheorem, + required this.fromNode, + }); + @override + State createState() => _EntriesListViewState(); +} + +class _EntriesListViewState extends State { + TextEditingController contentController = TextEditingController(); + + bool entryLoading = false; @override Widget build(BuildContext context) { + int length = widget.entries.length; return Container( - height: height, - width: showSidebar + height: widget.height, + width: widget.showSidebar ? MediaQuery.of(context).size.width / 2 : MediaQuery.of(context).size.width * 0.7, decoration: BoxDecoration(color: Colors.grey.withOpacity(0.1)), child: Padding( padding: const EdgeInsets.all(16), - child: (Column( + child: (ListView( + shrinkWrap: true, + //mainAxisAlignment: MainAxisAlignment.center, + //crossAxisAlignment: CrossAxisAlignment.center, children: [ - Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - const Text( + const Center( + child: Text( "Entries", style: TextStyles.title3secondary, ), - SizedBox( - width: MediaQuery.of(context).size.width / 6, - child: AppButton( - text: "Create New Entry", - height: 40, - onTap: () { - showDialog( - context: context, - builder: (context) => AppAlertDialog( - text: "New Entry", - content: const EntryForm(newEntry: true), - actions: [ - AppButton( - text: "Create New Entry", - height: 40, - onTap: () { /* Create new Entry */ }, - ), - ], - ), - ); - }, - type: "outlined", - ), - ), - ]), - if (entries.length > 0) + ), ListView.builder( - controller: controller, + controller: widget.controller, scrollDirection: Axis.vertical, shrinkWrap: true, padding: const EdgeInsets.all(8), - itemCount: entries.length, + itemCount: length + 1, itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.all(5), - child: CardContainer( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - entries[index].isProofEntry - ? "Proof" - : (entries[index].isTheoremEntry ? "Theorem" : ""), - style: TextStyles.bodyGrey, - textAlign: TextAlign.start, - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (entries[index].isEditable) - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => AppAlertDialog( - text: "Edit Entry", - content: EntryForm(id: entries[index].entryId), - actions: [ - AppButton( - text: "Save Entry", - height: 40, - onTap: () { /* Edit the Entry */ }, - ), - ], - ), - ); - }, - icon: Icon( - Icons.edit, - color: Colors.grey[600], - )), - if (entries[index].isEditable) - IconButton( - onPressed: () { - //edit entry - }, - icon: Icon( - Icons.delete, - color: Colors.grey[600], - )) - ], - ) - ], - ), - Text( - entries[index].content, - style: TextStyles.bodyBlack, - textAlign: TextAlign.start, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - entries[index].isFinalEntry ? "Finalized" : "", - style: TextStyles.bodyGrey, - ), - Text( - entries[index].publishDateFormatted, - style: TextStyles.bodyGrey, - textAlign: TextAlign.end, - ), - ], - ), - ], - ), + return (index != 0) + ? MobileEntryCard( + finalized: widget.finalized, + entry: widget.entries[index - 1], + onDelete: () async { + await widget.deleteEntry(widget.entries[index - 1].entryId); + }, + editEntry: widget.editEntry, + backgroundColor: Colors.white, + setProof: widget.setProof, + setDisproof: widget.setDisproof, + setTheorem: widget.setTheorem, + removeProof: widget.removeProof, + removeDisproof: widget.removeDisproof, + removeTheorem: widget.removeTheorem, + deleteEntry: widget.deleteEntry, + fromNode: widget.fromNode, + ) + : NewEntry( + onCreate: widget.createNewEntry, + backgroundColor: const Color.fromARGB(255, 220, 220, 240), + isMobile: false, + finalized: widget.finalized, + ); + }), + if (widget.entries.isEmpty) + const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: Text( + "Add Your First Entry!", + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + color: Colors.grey, ), - ); - }) - else - const Padding( - padding: EdgeInsets.all(16), - child: Text( - "No Entries Yet!", - style: TextStyles.bodyGrey, + ), ), - ) - + ), + const SizedBox(height: 60.0), ], )), ), diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/entry_form.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/entry_form.dart index 30bc0590..8c291581 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/entry_form.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/entry_form.dart @@ -4,19 +4,22 @@ import 'package:flutter/material.dart'; class EntryForm extends StatefulWidget { final int id; final bool newEntry; - const EntryForm({super.key, this.id = 0, this.newEntry = false}); + final TextEditingController contentController; + const EntryForm({ + super.key, + this.id = 0, + this.newEntry = false, + required this.contentController, + }); @override State createState() => _EntryFormState(); } class _EntryFormState extends State { - final contentController = TextEditingController(); - final contentFocusNode = FocusNode(); @override void dispose() { - contentController.dispose(); contentFocusNode.dispose(); super.dispose(); } @@ -26,23 +29,24 @@ class _EntryFormState extends State { return SizedBox( height: 300, child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 10.0), - Expanded( - child: SizedBox( - child: AppTextField( - controller: contentController, - focusNode: contentFocusNode, - hintText: "Content", - obscureText: false, - height: 200, - maxLines: 10, - ), + const SizedBox(height: 10.0), + SizedBox( + width: double.infinity, + child: SizedBox( + child: AppTextField( + controller: widget.contentController, + focusNode: contentFocusNode, + hintText: "Content", + obscureText: false, + height: 200, + maxLines: 10, ), - ) - ], + ), + ) + ], ), ); } diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/entry_menu.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/entry_menu.dart new file mode 100644 index 00000000..e7ed8b2d --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/entry_menu.dart @@ -0,0 +1,153 @@ +import 'package:collaborative_science_platform/models/workspaces_page/entry.dart'; +import 'package:collaborative_science_platform/providers/auth.dart'; +import 'package:collaborative_science_platform/screens/page_with_appbar/widgets/app_bar_button.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class EntryMenu extends StatelessWidget { + final Function setProof; + final Function setDisproof; + final Function setTheorem; + final Function removeDisproof; + final Function removeTheorem; + final Function removeProof; + final Function deleteEntry; + final Entry entry; + final bool fromNode; + const EntryMenu({ + super.key, + required this.removeDisproof, + required this.removeProof, + required this.removeTheorem, + required this.setDisproof, + required this.setProof, + required this.setTheorem, + required this.entry, + required this.deleteEntry, + required this.fromNode, + }); + + @override + Widget build(BuildContext context) { + return (entry.isEditable && !entry.isFinalEntry) + ? AuthenticatedEntryMenu( + setProof: setProof, + setDisproof: setDisproof, + setTheorem: setTheorem, + removeProof: removeProof, + removeDisproof: removeDisproof, + removeTheorem: removeTheorem, + deleteEntry: deleteEntry, + entry: entry, + fromNode: fromNode, + ) + : const SizedBox(); + } +} + +class AuthenticatedEntryMenu extends StatelessWidget { + final Function setProof; + final Function setDisproof; + final Function setTheorem; + final Function removeDisproof; + final Function removeTheorem; + final Function removeProof; + final Function deleteEntry; + final bool fromNode; + final Entry entry; + final GlobalKey> _popupNodeMenu = GlobalKey(); + AuthenticatedEntryMenu({ + super.key, + required this.removeDisproof, + required this.removeProof, + required this.removeTheorem, + required this.setDisproof, + required this.setProof, + required this.setTheorem, + required this.entry, + required this.deleteEntry, + required this.fromNode, + }); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + key: _popupNodeMenu, + position: PopupMenuPosition.under, + color: Colors.grey[200], + onSelected: (String result) async { + switch (result) { + case 'setTheorem': + await setTheorem(entry.entryId); + break; + case 'setProof': + await setProof(entry.entryId); + break; + case 'setDisproof': + await setDisproof(entry.entryId); + break; + case 'removeEntry': + await deleteEntry(entry.entryId); + break; + case 'removeDisproof': + await removeDisproof(); + break; + case 'removeTheorem': + await removeTheorem(); + break; + case 'removeProof': + await removeProof(); + break; + default: + } + }, + child: AppBarButton( + icon: Icons.more_horiz, + text: Provider.of(context).user!.firstName, + onPressed: () => _popupNodeMenu.currentState!.showButtonMenu(), + ), + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: 'removeEntry', + child: Text("Remove Entry"), + ), + if (!entry.isFinalEntry && + !entry.isTheoremEntry && + !entry.isProofEntry && + !entry.isDisproofEntry) + const PopupMenuItem( + value: 'setTheorem', + child: Text("Set this entry as theorem"), + ), + if (!entry.isFinalEntry && + !entry.isTheoremEntry && + !entry.isProofEntry && + !entry.isDisproofEntry) + const PopupMenuItem( + value: 'setProof', + child: Text("Set this entry as proof"), + ), + if (!entry.isTheoremEntry && !entry.isProofEntry && !entry.isDisproofEntry && fromNode) + const PopupMenuItem( + value: 'setDisproof', + child: Text("Set this entry as disproof"), + ), + if (entry.isDisproofEntry) + const PopupMenuItem( + value: 'removeDisproof', + child: Text("Unset Disproof"), + ), + if (entry.isTheoremEntry) + const PopupMenuItem( + value: 'removeTheorem', + child: Text("Unset Theorem"), + ), + if (entry.isProofEntry) + const PopupMenuItem( + value: 'removeProof', + child: Text("Unset Proof"), + ), + ], + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/references_list_view.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/references_list_view.dart index 5000684f..dfc3ec35 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/references_list_view.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/references_list_view.dart @@ -2,6 +2,7 @@ import 'package:collaborative_science_platform/models/node.dart'; import 'package:collaborative_science_platform/screens/node_details_page/node_details_page.dart'; import 'package:collaborative_science_platform/screens/workspace_page/mobile_workspace_page/widget/app_alert_dialog.dart'; import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/widgets/add_reference_form.dart'; +import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; import 'package:collaborative_science_platform/utils/text_styles.dart'; import 'package:collaborative_science_platform/widgets/app_button.dart'; import 'package:collaborative_science_platform/widgets/card_container.dart'; @@ -12,13 +13,23 @@ class ReferencesListView extends StatelessWidget { final List references; final ScrollController controller; final double height; - const ReferencesListView( - {super.key, required this.references, required this.controller, required this.height}); + final Function addReference; + final Function deleteReference; + final bool finalized; + const ReferencesListView({ + super.key, + required this.references, + required this.controller, + required this.height, + required this.addReference, + required this.deleteReference, + required this.finalized, + }); @override Widget build(BuildContext context) { return Container( - height: height, + // height: height, width: MediaQuery.of(context).size.width / 4, decoration: BoxDecoration(color: Colors.grey[100]), child: Padding( @@ -31,27 +42,31 @@ class ReferencesListView extends StatelessWidget { SizedBox( width: MediaQuery.of(context).size.width / 6, child: AppButton( - text: "Add References", + isActive: !finalized, + text: (MediaQuery.of(context).size.width > Responsive.desktopPageWidth) + ? "Add References" + : "Add", height: 40, type: "outlined", onTap: () { showDialog( context: context, - builder: (context) => const AppAlertDialog( - text: "Add References", - content: AddReferenceForm(), - ), + builder: (context) => AppAlertDialog( + text: "Add References", + content: AddReferenceForm(onAdd: addReference), + ), ); }, ), ), SizedBox( - height: (height * 2) / 3, + height: (height * 3) / 5, child: ListView.builder( controller: controller, scrollDirection: Axis.vertical, shrinkWrap: true, padding: const EdgeInsets.all(3), + physics: const NeverScrollableScrollPhysics(), itemCount: references.length, itemBuilder: (BuildContext context, int index) { return Padding( @@ -70,19 +85,25 @@ class ReferencesListView extends StatelessWidget { SizedBox( width: MediaQuery.of(context).size.width / 8, child: Text( - references[index].nodeTitle, - style: TextStyles.bodyBold, - textAlign: TextAlign.start, - ), + references[index].nodeTitle, + style: TextStyles.bodyBold, + textAlign: TextAlign.start, + ), ), - IconButton( - onPressed: () { + if (!finalized && + MediaQuery.of(context).size.width > Responsive.desktopPageWidth) + IconButton( + onPressed: () async { //remove reference + await deleteReference(references[index].id); + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); }, icon: Icon( Icons.delete, color: Colors.grey[600], - )) + ), + ) ], ), Text( @@ -104,7 +125,26 @@ class ReferencesListView extends StatelessWidget { ); }), ), - ]), - )); + // SizedBox( + // width: MediaQuery.of(context).size.width / 6, + // child: AppButton( + // text: (MediaQuery.of(context).size.width > Responsive.desktopPageWidth) ? "Add References" : "Add", + // height: 40, + // type: "outlined", + // onTap: () { + // showDialog( + // context: context, + // builder: (context) => AppAlertDialog( + // text: "Add References", + // content: AddReferenceForm(onAdd: addReference), + // ), + // ); + // }, + // ), + // ), + ], + ), + ), + ); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/semantic_tag_list_view.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/semantic_tag_list_view.dart new file mode 100644 index 00000000..a0a82d2c --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/semantic_tag_list_view.dart @@ -0,0 +1,91 @@ +import 'package:collaborative_science_platform/models/workspace_semantic_tag.dart'; +import 'package:collaborative_science_platform/widgets/semantic_search_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:collaborative_science_platform/widgets/card_container.dart'; +import 'package:collaborative_science_platform/utils/text_styles.dart'; + +class SemanticTagListView extends StatefulWidget { + final List tags; + final Function addSemanticTag; + final Function removeSemanticTag; + final double height; + final bool finalized; + + const SemanticTagListView({ + super.key, + required this.tags, + required this.addSemanticTag, + required this.removeSemanticTag, + required this.height, + required this.finalized, + }); + + @override + State createState() => _SemanticTagListViewState(); +} + +class _SemanticTagListViewState extends State { + @override + Widget build(BuildContext context) { + return Container( + // height: widget.height, + width: MediaQuery.of(context).size.width / 4, + decoration: BoxDecoration(color: Colors.grey[100]), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("Semantic Tags", style: TextStyles.title4secondary), + Padding( + padding: const EdgeInsets.all(8.0), + child: SemanticSearchBar(addSemanticTag: widget.addSemanticTag), + ), + SizedBox( + // height: (widget.height * 3) / 5, + child: ListView.builder( + itemCount: widget.tags.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(3.0), + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(3.0), + child: CardContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + widget.tags[index].label, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18.0, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + if (!widget.finalized) + IconButton( + onPressed: () async { + await widget.removeSemanticTag(widget.tags[index].tagId); + setState(() { }); + }, + icon: const Icon( + Icons.delete, + color: Colors.grey, + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/send_collaboration_request_form.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/send_collaboration_request_form.dart index f218788d..db9b709c 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/send_collaboration_request_form.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/web_workspace_page/widgets/send_collaboration_request_form.dart @@ -1,27 +1,46 @@ +import 'package:collaborative_science_platform/models/profile_data.dart'; import 'package:collaborative_science_platform/providers/user_provider.dart'; import 'package:collaborative_science_platform/utils/text_styles.dart'; +import 'package:collaborative_science_platform/widgets/app_button.dart'; import 'package:collaborative_science_platform/widgets/app_search_bar.dart'; +import 'package:collaborative_science_platform/widgets/app_text_field.dart'; import 'package:collaborative_science_platform/widgets/card_container.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:collaborative_science_platform/utils/colors.dart'; class SendCollaborationRequestForm extends StatefulWidget { - const SendCollaborationRequestForm({super.key}); + final Function sendCollaborationRequest; + const SendCollaborationRequestForm({ + super.key, + required this.sendCollaborationRequest, + }); @override State createState() => _SendCollaborationRequestFormState(); } class _SendCollaborationRequestFormState extends State { + final messageTitleController = TextEditingController(); + final messageTitleFocusNode = FocusNode(); + final messageBodyController = TextEditingController(); + final messageBodyFocusNode = FocusNode(); final searchBarFocusNode = FocusNode(); bool isLoading = false; bool firstSearch = false; + ProfileData user = ProfileData(); bool error = false; String errorMessage = ""; + int page = 0; + bool sendingRequest = false; @override void dispose() { + messageTitleController.dispose(); + messageTitleFocusNode.dispose(); + messageBodyController.dispose(); + messageBodyFocusNode.dispose(); searchBarFocusNode.dispose(); super.dispose(); } @@ -51,12 +70,13 @@ class _SendCollaborationRequestFormState extends State(context); + return SizedBox( height: 600, child: SingleChildScrollView( primary: false, scrollDirection: Axis.vertical, - child: Column( + child: (page == 0) ? Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -113,11 +133,14 @@ class _SendCollaborationRequestFormState extends State createState() => _WorkspacesSideBarState(); } class _WorkspacesSideBarState extends State { + TextEditingController textController = TextEditingController(); @override Widget build(BuildContext context) { + final auth = Provider.of(context); return Container( height: widget.height + 100, width: MediaQuery.of(context).size.width / 4, @@ -51,7 +64,7 @@ class _WorkspacesSideBarState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - const Text("My Workspaces", style: TextStyles.title3secondary), + const Text("Workspaces", style: TextStyles.title4secondary), AppBarButton( onPressed: () { widget.hideSidebar!(); @@ -66,7 +79,9 @@ class _WorkspacesSideBarState extends State { child: Padding( padding: const EdgeInsets.symmetric(vertical: 15), child: AppButton( - text: "Create New Workspace", + text: (MediaQuery.of(context).size.width < Responsive.desktopPageWidth) + ? "New" + : "New Workspace", height: 40, onTap: () { showDialog( @@ -83,10 +98,17 @@ class _WorkspacesSideBarState extends State { ), backgroundColor: Colors.white, surfaceTintColor: Colors.white, - content: const CreateWorkspaceForm(), + content: CreateWorkspaceForm(titleController: textController), actions: [ AppButton( - text: "Create New Workspace", height: 50, onTap: () {}) + text: "Create New Workspace", + height: 50, + onTap: () async { + await widget.createNewWorkspace(textController.text); + textController.text = ""; + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + }) ], )); }, @@ -94,77 +116,316 @@ class _WorkspacesSideBarState extends State { )), ), SizedBox( - height: (widget.workspaces != null) ? widget.height * 0.9 : 40, + height: (widget.workspaces != null) + ? (auth.basicUser!.userType != "reviewer" + ? widget.height * 0.9 + : widget.height * 0.40) + : 40, child: (widget.workspaces != null) ? ListView.builder( - scrollDirection: Axis.vertical, - shrinkWrap: true, - padding: const EdgeInsets.all(8), + scrollDirection: Axis.vertical, + shrinkWrap: true, + padding: const EdgeInsets.all(8), itemCount: (widget.workspaces!.workspaces.length + widget.workspaces!.pendingWorkspaces.length), - itemBuilder: (BuildContext context, int index) { + itemBuilder: (BuildContext context, int index) { if (index < widget.workspaces!.workspaces.length) { - return Padding( - padding: const EdgeInsets.all(5), - child: CardContainer( - onTap: () { - context.push( + return Padding( + padding: const EdgeInsets.all(5), + child: CardContainer( + onTap: () { + context.push( "${WorkspacesPage.routeName}/${widget.workspaces!.workspaces[index].workspaceId}"); - }, - child: Text( + widget.hideSidebar!(); + }, + child: Text( widget.workspaces!.workspaces[index].workspaceTitle, - style: TextStyles.title4, - textAlign: TextAlign.start, + style: TextStyles.title4, + textAlign: TextAlign.start, + ), ), - ), - ); + ); } else if (index >= widget.workspaces!.workspaces.length) { - return Padding( - padding: const EdgeInsets.all(5), - child: CardContainer( + return Padding( + padding: const EdgeInsets.all(5), + child: CardContainer( onTap: () { context.push( - "${WorkspacesPage.routeName}/${widget.workspaces!.pendingWorkspaces[index - widget.workspaces!.workspaces.length].workspaceId}"); + "${WorkspacesPage.routeName}/${widget.workspaces!.pendingWorkspaces[index - widget.workspaces!.workspaces.length].workspaceId}"); + widget.hideSidebar!(); }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text( - widget - .workspaces! - .pendingWorkspaces[ - index - widget.workspaces!.workspaces.length] - .workspaceTitle, - style: TextStyles.bodyBold, - textAlign: TextAlign.start, - ), - Column(children: [ - IconButton( - icon: const Icon(CupertinoIcons.check_mark_circled, - color: AppColors.infoColor), - onPressed: () { - // function to accept collaboration request - }, - ), - IconButton( - icon: const Icon( - CupertinoIcons.clear_circled, - color: AppColors.warningColor, - ), - onPressed: () { - // function to reject collaboration request - }, + child: (MediaQuery.of(context).size.width > + Responsive.desktopPageWidth) + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 7, + child: Text( + widget + .workspaces! + .pendingWorkspaces[ + index - widget.workspaces!.workspaces.length] + .workspaceTitle, + style: TextStyles.bodyBold, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ), + Column(children: [ + IconButton( + icon: const Icon(CupertinoIcons.check_mark_circled, + color: AppColors.infoColor), + onPressed: () async { + // function to accept review request + await widget.updateCollaborationRequest( + widget + .workspaces! + .pendingWorkspaces[index - + widget.workspaces!.workspaces.length] + .requestId, + RequestStatus.approved); + }, + ), + IconButton( + icon: const Icon( + CupertinoIcons.clear_circled, + color: AppColors.warningColor, + ), + onPressed: () async { + // function to reject review request + await widget.updateCollaborationRequest( + widget + .workspaces! + .pendingWorkspaces[index - + widget.workspaces!.workspaces.length] + .requestId, + RequestStatus.rejected); + }, + ), + ]) + ], + ) + : Column( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + child: + Text( + widget + .workspaces! + .pendingWorkspaces[ + index - widget.workspaces!.workspaces.length] + .workspaceTitle, + style: TextStyles.bodyBold, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: const Icon( + CupertinoIcons.check_mark_circled, + color: AppColors.infoColor), + onPressed: () { + // function to accept review request + widget.updateCollaborationRequest( + widget + .workspaces! + .pendingWorkspaces[index - + widget + .workspaces!.workspaces.length] + .requestId, + RequestStatus.approved); + }, + ), + IconButton( + icon: const Icon( + CupertinoIcons.clear_circled, + color: AppColors.warningColor, + ), + onPressed: () async { + // function to reject review request + await widget.updateCollaborationRequest( + widget + .workspaces! + .pendingWorkspaces[index - + widget + .workspaces!.workspaces.length] + .requestId, + RequestStatus.rejected); + }, + ), + ], + ) + ], ), - ]) - ], - )), - ); - } else { - return const SizedBox(); - } + ), + ); + } else { + return const SizedBox(); + } }) : const CircularProgressIndicator(), ), + if (auth.basicUser!.userType == "reviewer") + const Padding( + padding: EdgeInsets.symmetric(vertical: 5), + child: Text("Review Workspaces", style: TextStyles.title4secondary), + ), + if (auth.basicUser!.userType == "reviewer") + SizedBox( + height: (widget.workspaces != null) ? widget.height * 0.40 : 40, + child: (widget.workspaces != null) + ? ListView.builder( + scrollDirection: Axis.vertical, + shrinkWrap: true, + padding: const EdgeInsets.all(8), + itemCount: (widget.workspaces!.reviewWorkspaces.length + + widget.workspaces!.pendingReviewWorkspaces.length), + itemBuilder: (BuildContext context, int index) { + if (index < widget.workspaces!.reviewWorkspaces.length) { + return Padding( + padding: const EdgeInsets.all(5), + child: CardContainer( + onTap: () { + context.push( + "${WorkspacesPage.routeName}/${widget.workspaces!.reviewWorkspaces[index].workspaceId}"); + widget.hideSidebar!(); + }, + child: Text( + widget.workspaces!.reviewWorkspaces[index].workspaceTitle, + style: TextStyles.title4, + textAlign: TextAlign.start, + ), + ), + ); + } else if (index >= widget.workspaces!.reviewWorkspaces.length) { + return Padding( + padding: const EdgeInsets.all(5), + child: CardContainer( + onTap: () { + context.push( + "${WorkspacesPage.routeName}/${widget.workspaces!.pendingReviewWorkspaces[index - widget.workspaces!.reviewWorkspaces.length].workspaceId}"); + widget.hideSidebar!(); + }, + child: (MediaQuery.of(context).size.width > + Responsive.desktopPageWidth) + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text( + widget + .workspaces! + .pendingReviewWorkspaces[index - + widget.workspaces!.reviewWorkspaces.length] + .workspaceTitle, + style: TextStyles.bodyBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + Column(children: [ + IconButton( + icon: const Icon( + CupertinoIcons.check_mark_circled, + color: AppColors.infoColor), + onPressed: () async { + // function to accept review request + await widget.updateReviewRequest( + widget + .workspaces! + .pendingReviewWorkspaces[index - + widget.workspaces!.reviewWorkspaces + .length] + .requestId, + RequestStatus.approved); + }, + ), + IconButton( + icon: const Icon( + CupertinoIcons.clear_circled, + color: AppColors.warningColor, + ), + onPressed: () async { + // function to reject review request + await widget.updateReviewRequest( + widget + .workspaces! + .pendingReviewWorkspaces[index - + widget.workspaces!.reviewWorkspaces + .length] + .requestId, + RequestStatus.rejected); + }, + ), + ]) + ], + ) + : Column( + children: [ + Text( + widget + .workspaces! + .pendingReviewWorkspaces[index - + widget.workspaces!.reviewWorkspaces.length] + .workspaceTitle, + style: TextStyles.bodyBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + Row( + children: [ + IconButton( + icon: const Icon( + CupertinoIcons.check_mark_circled, + color: AppColors.infoColor), + onPressed: () async { + // function to accept review request + await widget.updateReviewRequest( + widget + .workspaces! + .pendingReviewWorkspaces[index - + widget.workspaces! + .reviewWorkspaces.length] + .requestId, + RequestStatus.approved); + }, + ), + IconButton( + icon: const Icon( + CupertinoIcons.clear_circled, + color: AppColors.warningColor, + ), + onPressed: () async { + // function to accept review request + await widget.updateReviewRequest( + widget + .workspaces! + .pendingReviewWorkspaces[index - + widget.workspaces! + .reviewWorkspaces.length] + .requestId, + RequestStatus.rejected); + }, + ), + ], + ) + ], + ), + ), + ); + } else { + return const SizedBox(); + } + }) + : const CircularProgressIndicator(), + ) ], ))); } diff --git a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/workspaces_page.dart b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/workspaces_page.dart index a9ada4e1..879f7e79 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/workspaces_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/screens/workspace_page/workspaces_page.dart @@ -2,12 +2,13 @@ import 'package:collaborative_science_platform/exceptions/workspace_exceptions.d import 'package:collaborative_science_platform/models/workspaces_page/workspace.dart'; import 'package:collaborative_science_platform/models/workspaces_page/workspaces.dart'; import 'package:collaborative_science_platform/providers/auth.dart'; +import 'package:collaborative_science_platform/providers/wiki_data_provider.dart'; import 'package:collaborative_science_platform/providers/workspace_provider.dart'; import 'package:collaborative_science_platform/screens/workspace_page/web_workspace_page/web_workspace_page.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../../utils/responsive/responsive.dart'; +import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; import 'mobile_workspace_page/mobile_workspace_page.dart'; class WorkspacesPage extends StatefulWidget { @@ -30,6 +31,7 @@ class _WorkspacesPageState extends State { void getWorkspaceById(int id) async { try { + print("Workspace Id: $id"); final workspaceProvider = Provider.of(context); final auth = Provider.of(context); setState(() { @@ -86,6 +88,662 @@ class _WorkspacesPageState extends State { } } + void createNewWorkspace(String title) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.createWorkspace(title, auth.user!.token); + await workspaceProvider.getUserWorkspaces(auth.basicUser!.basicUserId, auth.user!.token); + setState(() { + workspaces = (workspaceProvider.workspaces ?? {} as Workspaces); + }); + } on CreateWorkspaceException { + setState(() { + error = true; + errorMessage = CreateWorkspaceException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + void createNewEntry(String content) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.addEntry(content, widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on AddEntryException { + setState(() { + error = true; + errorMessage = AddEntryException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void editEntry(String content, int entryId) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.editEntry(content, entryId, widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on EditEntryException { + setState(() { + error = true; + errorMessage = EditEntryException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void deleteEntry(int entryId) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.deleteEntry(entryId, widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on EditEntryException { + setState(() { + error = true; + errorMessage = EditEntryException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void addReference(int nodeId) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.addReference(widget.workspaceId, nodeId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on AddReferenceException { + setState(() { + error = true; + errorMessage = AddReferenceException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void deleteReference(int nodeId) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.deleteReference(widget.workspaceId, nodeId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on DeleteReferenceException { + setState(() { + error = true; + errorMessage = DeleteReferenceException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void editWorkspaceTitle(String title) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + }); + await workspaceProvider.updateWorkspaceTitle(widget.workspaceId, auth.user!.token, title); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + await workspaceProvider.getUserWorkspaces(auth.basicUser!.basicUserId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + workspaces = (workspaceProvider.workspaces ?? {} as Workspaces); + }); + } on WorkspacePermissionException { + setState(() { + error = true; + errorMessage = WorkspacePermissionException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void finalizeWorkspace() async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.finalizeWorkspace(widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on FinalizeWorkspaceException { + setState(() { + error = true; + errorMessage = FinalizeWorkspaceException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void sendWorkspaceToReview() async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.sendWorkspaceToReview( + widget.workspaceId, auth.basicUser!.basicUserId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on FinalizeWorkspaceException { + setState(() { + error = true; + errorMessage = FinalizeWorkspaceException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void sendCollaborationRequest(int receiverId, String title, String body) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + }); + await workspaceProvider.sendCollaborationRequest(auth.basicUser!.basicUserId, receiverId, + title, body, widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on SendCollaborationRequestException { + setState(() { + error = true; + errorMessage = SendCollaborationRequestException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void updateCollaborationRequest(int id, RequestStatus status) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + }); + await workspaceProvider.updateCollaborationRequest(id, status, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + await workspaceProvider.getUserWorkspaces(auth.basicUser!.basicUserId, auth.user!.token); + setState(() { + workspaces = (workspaceProvider.workspaces ?? {} as Workspaces); + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on SendCollaborationRequestException { + setState(() { + error = true; + errorMessage = SendCollaborationRequestException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void updateReviewRequest(int id, RequestStatus status) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + }); + await workspaceProvider.updateReviewRequest(id, status, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + await workspaceProvider.getUserWorkspaces(auth.basicUser!.basicUserId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + workspaces = (workspaceProvider.workspaces ?? {} as Workspaces); + }); + } on SendCollaborationRequestException { + setState(() { + error = true; + errorMessage = SendCollaborationRequestException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void addSemanticTag(String wikiId, String label) async { + try { + final auth = Provider.of(context, listen: false); + final wikiDataProvider = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + }); + wikiDataProvider.addSemanticTag(wikiId, label, widget.workspaceId, 'workspace', auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on WorkspacePermissionException { + setState(() { + error = true; + errorMessage = WorkspacePermissionException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = e.toString(); + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void removeSemanticTag(int tagId) async { + try { + final auth = Provider.of(context, listen: false); + final wikiDataProvider = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + }); + await wikiDataProvider.removeSemanticTag(widget.workspaceId, tagId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on WorkspacePermissionException { + setState(() { + error = true; + errorMessage = WorkspacePermissionException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = e.toString(); + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void addReview(int id, RequestStatus status, String comment) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + }); + await workspaceProvider.addReview(id, status, comment, auth.user!.token); + await workspaceProvider.getUserWorkspaces(auth.basicUser!.basicUserId, auth.user!.token); + setState(() { + workspace = null; + workspaces = (workspaceProvider.workspaces ?? {} as Workspaces); + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void setProof(int entryId) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.setProof(entryId, widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on EditEntryException { + setState(() { + error = true; + errorMessage = EditEntryException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void setDisproof(int entryId) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.setDisproof(entryId, widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on EditEntryException { + setState(() { + error = true; + errorMessage = EditEntryException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void setTheorem(int entryId) async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.setTheorem(entryId, widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on EditEntryException { + setState(() { + error = true; + errorMessage = EditEntryException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void removeProof() async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.removeProof(widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on EditEntryException { + setState(() { + error = true; + errorMessage = EditEntryException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void removeDisproof() async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.removeDisproof(widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on EditEntryException { + setState(() { + error = true; + errorMessage = EditEntryException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + void removeTheorem() async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.removeTheorem(widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on EditEntryException { + setState(() { + error = true; + errorMessage = EditEntryException().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + void resetWorkspace() async { + try { + final auth = Provider.of(context, listen: false); + final workspaceProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await workspaceProvider.resetWorkspace(widget.workspaceId, auth.user!.token); + await workspaceProvider.getWorkspaceById(widget.workspaceId, auth.user!.token); + setState(() { + workspace = (workspaceProvider.workspace ?? {} as Workspace); + }); + } on WorkspaceDoesNotExist { + setState(() { + error = true; + errorMessage = WorkspaceDoesNotExist().message; + }); + } catch (e) { + setState(() { + error = true; + errorMessage = "Something went wrong!"; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } @override void didChangeDependencies() { if (_isFirstTime) { @@ -105,11 +763,60 @@ class _WorkspacesPageState extends State { mobile: MobileWorkspacePage( workspace: workspace, workspaces: workspaces, + createNewWorkspace: createNewWorkspace, + createNewEntry: createNewEntry, + editEntry: editEntry, + deleteEntry: deleteEntry, + addReference: addReference, + deleteReference: deleteReference, + editTitle: editWorkspaceTitle, + addSemanticTag: addSemanticTag, + removeSemanticTag: removeSemanticTag, + sendCollaborationRequest: sendCollaborationRequest, + finalizeWorkspace: finalizeWorkspace, + sendWorkspaceToReview: sendWorkspaceToReview, + addReview: addReview, + updateReviewRequest: updateReviewRequest, + updateCollaborationRequest: updateCollaborationRequest, + resetWorkspace: resetWorkspace, + + setProof: setProof, + setDisproof: setDisproof, + setTheorem: setTheorem, + removeProof: removeProof, + removeDisproof: removeDisproof, + removeTheorem: removeTheorem, + ), desktop: WebWorkspacePage( isLoading: isLoading, workspace: workspace, workspaces: workspaces, + createNewWorkspace: createNewWorkspace, + createNewEntry: createNewEntry, + editEntry: editEntry, + deleteEntry: deleteEntry, + addReference: addReference, + deleteReference: deleteReference, + editTitle: editWorkspaceTitle, + addSemanticTag: addSemanticTag, + removeSemanticTag: removeSemanticTag, + sendCollaborationRequest: sendCollaborationRequest, + finalizeWorkspace: finalizeWorkspace, + sendWorkspaceToReview: sendWorkspaceToReview, + addReview: addReview, + updateReviewRequest: updateReviewRequest, + updateCollaborationRequest: updateCollaborationRequest, + + resetWorkspace: resetWorkspace, + + setProof: setProof, + setDisproof: setDisproof, + setTheorem: setTheorem, + removeProof: removeProof, + removeDisproof: removeDisproof, + removeTheorem: removeTheorem, + ), ); } diff --git a/project/FrontEnd/collaborative_science_platform/lib/services/screen_navigation.dart b/project/FrontEnd/collaborative_science_platform/lib/services/screen_navigation.dart index 78dacf05..1e0c6ece 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/services/screen_navigation.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/services/screen_navigation.dart @@ -1,7 +1,6 @@ import 'package:collaborative_science_platform/screens/auth_screens/please_login_page.dart'; import 'package:collaborative_science_platform/screens/graph_page/graph_page.dart'; import 'package:collaborative_science_platform/screens/home_page/home_page.dart'; -import 'package:collaborative_science_platform/screens/notifications_page/notifications_page.dart'; import 'package:collaborative_science_platform/screens/profile_page/profile_page.dart'; import 'package:collaborative_science_platform/screens/workspace_page/workspaces_page.dart'; import 'package:flutter/material.dart'; @@ -15,7 +14,7 @@ enum ScreenTab { workspaces, workspace, createWorkspace, - notifications, + //notifications, profile, pleaseLogin, none @@ -49,9 +48,9 @@ class ScreenNavigation extends ChangeNotifier { case ScreenTab.createWorkspace: // Goes to the page where workspaces are created context.go(MobileCreateWorkspacePage.routeName); break; - case ScreenTab.notifications: - context.go(NotificationPage.routeName); - break; + // case ScreenTab.notifications: + // context.go(NotificationPage.routeName); + // break; case ScreenTab.profile: if (email == "") { context.go(ProfilePage.routeName); diff --git a/project/FrontEnd/collaborative_science_platform/lib/services/share_page.dart b/project/FrontEnd/collaborative_science_platform/lib/services/share_page.dart index 18942da9..fe9cbc18 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/services/share_page.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/services/share_page.dart @@ -6,6 +6,6 @@ import 'package:share_plus/share_plus.dart'; class SharePage { static void shareNodeView(NodeDetailed node) { Share.share( - 'Check out this on Collaborative Science Platform: ${node.nodeTitle} at ${Constants.appUrl}/${NodeDetailsPage.routeName}/${node.nodeId}'); + 'Check out this on Collaborative Science Platform: ${node.nodeTitle} at ${Constants.appUrl}${NodeDetailsPage.routeName}/${node.nodeId}'); } } diff --git a/project/FrontEnd/collaborative_science_platform/lib/utils/constants.dart b/project/FrontEnd/collaborative_science_platform/lib/utils/constants.dart index e5eb0ae5..aa00d179 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/utils/constants.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/utils/constants.dart @@ -1,5 +1,6 @@ class Constants { static const String appName = "Collaborative Science Platform"; - static const String appUrl = "http://"; + static const String appUrl = "http://13.51.205.39"; static const String apiUrl = "http://13.51.55.11:8000/api"; + static const String annotationUrl = "http://13.51.55.11:8001"; } diff --git a/project/FrontEnd/collaborative_science_platform/lib/utils/responsive/responsive.dart b/project/FrontEnd/collaborative_science_platform/lib/utils/responsive/responsive.dart index 40faca6b..0920a72c 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/utils/responsive/responsive.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/utils/responsive/responsive.dart @@ -16,6 +16,7 @@ class Responsive extends StatelessWidget { }) : super(key: key); static double desktopPageWidth = 1000; + static double desktopNodePageWidth = 1200; static double getGenericPageWidth(BuildContext context) { if (isDesktop(context)) { @@ -25,12 +26,15 @@ class Responsive extends StatelessWidget { } } - static bool isMobile(BuildContext context) => MediaQuery.of(context).size.width < tabletBreakpoint; + static bool isMobile(BuildContext context) => + MediaQuery.of(context).size.width < tabletBreakpoint; static bool isTablet(BuildContext context) => - MediaQuery.of(context).size.width >= tabletBreakpoint && MediaQuery.of(context).size.width < desktopBreakpoint; + MediaQuery.of(context).size.width >= tabletBreakpoint && + MediaQuery.of(context).size.width < desktopBreakpoint; - static bool isDesktop(BuildContext context) => MediaQuery.of(context).size.width >= desktopBreakpoint; + static bool isDesktop(BuildContext context) => + MediaQuery.of(context).size.width >= desktopBreakpoint; @override Widget build(BuildContext context) { diff --git a/project/FrontEnd/collaborative_science_platform/lib/utils/router.dart b/project/FrontEnd/collaborative_science_platform/lib/utils/router.dart index a866b5c0..bd05f554 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/utils/router.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/utils/router.dart @@ -6,7 +6,6 @@ import 'package:collaborative_science_platform/screens/auth_screens/signup_page. import 'package:collaborative_science_platform/screens/graph_page/graph_page.dart'; import 'package:collaborative_science_platform/screens/home_page/home_page.dart'; import 'package:collaborative_science_platform/screens/node_details_page/node_details_page.dart'; -import 'package:collaborative_science_platform/screens/notifications_page/notifications_page.dart'; import 'package:collaborative_science_platform/screens/profile_page/account_settings_page.dart'; import 'package:collaborative_science_platform/screens/profile_page/profile_page.dart'; import 'package:collaborative_science_platform/screens/workspace_page/workspaces_page.dart'; @@ -63,7 +62,9 @@ final router = GoRouter( redirect: (context, state) { Provider.of(context, listen: false) .changeSelectedTab(ScreenTab.workspaces); - if (!context.read().isSignedIn) { + if (!context.read().isSignedIn || + (context.read().basicUser!.userType != "contributor" && + context.read().basicUser!.userType != "reviewer")) { return '${PleaseLoginPage.routeName}${WorkspacesPage.routeName}'; } else { return null; @@ -106,24 +107,24 @@ final router = GoRouter( ), ], ), - GoRoute( - name: NotificationPage.routeName.substring(1), - path: NotificationPage.routeName, - builder: (context, state) { - Provider.of(context, listen: false) - .changeSelectedTab(ScreenTab.notifications); - return const NotificationPage(); - }, - redirect: (context, state) { - Provider.of(context, listen: false) - .changeSelectedTab(ScreenTab.notifications); - if (!context.read().isSignedIn) { - return '${PleaseLoginPage.routeName}${NotificationPage.routeName}'; - } else { - return null; - } - }, - ), + // GoRoute( + // name: NotificationPage.routeName.substring(1), + // path: NotificationPage.routeName, + // builder: (context, state) { + // Provider.of(context, listen: false) + // .changeSelectedTab(ScreenTab.notifications); + // return const NotificationPage(); + // }, + // redirect: (context, state) { + // Provider.of(context, listen: false) + // .changeSelectedTab(ScreenTab.notifications); + // if (!context.read().isSignedIn) { + // return '${PleaseLoginPage.routeName}${NotificationPage.routeName}'; + // } else { + // return null; + // } + // }, + // ), GoRoute( name: AccountSettingsPage.routeName.substring(1), path: AccountSettingsPage.routeName, diff --git a/project/FrontEnd/collaborative_science_platform/lib/utils/text_styles.dart b/project/FrontEnd/collaborative_science_platform/lib/utils/text_styles.dart index 8762e83b..6ec63fed 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/utils/text_styles.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/utils/text_styles.dart @@ -49,7 +49,7 @@ class TextStyles { ); static const TextStyle bodyBlack = TextStyle( - fontSize: 16, + fontSize: 10, ); static const TextStyle bodyGrey = TextStyle( fontSize: 10, diff --git a/project/FrontEnd/collaborative_science_platform/lib/widgets/annotation_text.dart b/project/FrontEnd/collaborative_science_platform/lib/widgets/annotation_text.dart index c09d1a15..b58bb51d 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/widgets/annotation_text.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/widgets/annotation_text.dart @@ -1,50 +1,271 @@ import 'dart:ui'; +import 'package:collaborative_science_platform/models/annotation.dart'; +import 'package:collaborative_science_platform/models/user.dart'; +import 'package:collaborative_science_platform/providers/annotation_provider.dart'; +import 'package:collaborative_science_platform/providers/auth.dart'; +import 'package:collaborative_science_platform/utils/colors.dart'; import 'package:collaborative_science_platform/utils/responsive/responsive.dart'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_tex/flutter_tex.dart'; +import 'package:provider/provider.dart'; -class AnnotationText extends StatelessWidget { +class AnnotationText extends StatefulWidget { final String text; + final String annotationSourceLocation; + final List annotationAuthors; final TextStyle? style; final TextAlign? textAlign; final int? maxLines; - const AnnotationText(this.text, {super.key, this.style, this.textAlign, this.maxLines}); + const AnnotationText( + this.text, + this.annotationSourceLocation, + this.annotationAuthors, { + super.key, + this.style, + this.textAlign, + this.maxLines, + }); + + @override + State createState() => _AnnotationTextState(); +} + +class _AnnotationTextState extends State { + bool _initState = true; + bool isLoading = true; + bool error = false; + List annotations = []; + List annotationIndices = []; + List annotationOwner = []; + List textSpans = []; + + @override + void didChangeDependencies() { + if (_initState) { + getAnnotations(); + _initState = false; + } + super.didChangeDependencies(); + } + + void getAnnotations() async { + try { + setState(() { + error = false; + isLoading = true; + }); + final annotationProvider = Provider.of(context); + final User? user = Provider.of(context, listen: false).user; + final email = user == null ? "" : user.email; + if (user != null) widget.annotationAuthors.add("http://13.51.205.39/profile/$email"); + await annotationProvider.getAnnotations( + widget.annotationSourceLocation, widget.annotationAuthors); + annotations = annotationProvider.annotations; +/* + for (var element in annotations) { + annotationIndices.add(element.startOffset); + annotationIndices.add(element.endOffset); + element.annotationAuthor = element.annotationAuthor + .substring(element.annotationAuthor.lastIndexOf("/") + 1) + .replaceAll("%40", "@"); + print(element.annotationAuthor); + annotationOwner.add(element.annotationAuthor == email); + } + print(annotationIndices); + print(annotationOwner); + + int textLeftCounter = 0; + int textRightCounter = 0; + int annotationCounter = 0; + while ( + textRightCounter < widget.text.length && annotationCounter < annotationIndices.length) { + print(annotationOwner[(annotationCounter / 2).round()].toString() + "aaaaa "); + if (textRightCounter == annotationIndices[annotationCounter]) { + if (textLeftCounter != textRightCounter) { + textSpans.add(TextSpan( + text: widget.text.substring(textLeftCounter, textRightCounter), + style: widget.style, + )); + } + print(annotationOwner[(annotationCounter / 2).round()]); + textSpans.add(TextSpan( + text: widget.text.substring(textRightCounter, annotationIndices[annotationCounter + 1]), + style: TextStyle( + backgroundColor: annotationOwner[(annotationCounter / 2).round()] + ? Colors.green.withOpacity(0.3) + : Colors.yellow.withOpacity(0.3)), + )); + textLeftCounter = annotationIndices[annotationCounter + 1]; + textRightCounter = annotationIndices[annotationCounter + 1]; + annotationCounter += 2; + } + textRightCounter++; + } + if (annotationCounter == annotationIndices.length) { + textSpans.add(TextSpan( + text: widget.text.substring(textLeftCounter, widget.text.length), + style: widget.style, + )); + } else if (textLeftCounter != textRightCounter) { + textSpans.add(TextSpan( + text: widget.text.substring(textLeftCounter, textRightCounter), + style: widget.style, + )); + } + if (annotationCounter == 0) { + textSpans.add(TextSpan( + text: widget.text, + style: widget.style, + )); + } +*/ +// Filter out older annotations in case of overlap + Map mostRecentAnnotations = {}; + for (var annotation in annotations) { + // Check if there is already an annotation that overlaps + bool overlap = mostRecentAnnotations.keys.any((key) => + (annotation.startOffset < mostRecentAnnotations[key]!.endOffset && + annotation.endOffset > mostRecentAnnotations[key]!.startOffset)); + + if (overlap) { + // Find the overlapping annotation + int overlappingKey = mostRecentAnnotations.keys.firstWhere((key) => + (annotation.startOffset < mostRecentAnnotations[key]!.endOffset && + annotation.endOffset > mostRecentAnnotations[key]!.startOffset)); + + // Keep the most recent annotation + if (annotation.dateCreated.isAfter(mostRecentAnnotations[overlappingKey]!.dateCreated)) { + mostRecentAnnotations[overlappingKey] = annotation; + } + } else { + mostRecentAnnotations[annotation.startOffset] = annotation; + } + } + + // Sort the filtered annotations by startOffset + List sortedAnnotations = mostRecentAnnotations.values.toList(); + sortedAnnotations.sort((a, b) => a.startOffset.compareTo(b.startOffset)); + + // Initialize counters + int textLeftCounter = 0; + + // Iterate over the sorted annotations + for (var annotation in sortedAnnotations) { + // Extract email for comparison + String annotationEmail = annotation.annotationAuthor + .substring(annotation.annotationAuthor.lastIndexOf("/") + 1) + .replaceAll("%40", "@"); + + // Add non-annotated text span if any + if (annotation.startOffset > textLeftCounter) { + textSpans.add(TextSpan( + text: widget.text.substring(textLeftCounter, annotation.startOffset), + style: widget.style, + )); + } + + // Add annotated text span + textSpans.add(TextSpan( + text: widget.text.substring(annotation.startOffset, annotation.endOffset), + style: TextStyle( + backgroundColor: annotationEmail == email + ? Colors.indigo[600]!.withOpacity(0.3) + : Colors.deepOrangeAccent.withOpacity(0.3), + ), + )); + + // Update the left counter + textLeftCounter = annotation.endOffset; + } + +// Add remaining text if any + if (textLeftCounter < widget.text.length) { + textSpans.add(TextSpan( + text: widget.text.substring(textLeftCounter), + style: widget.style, + )); + } + } catch (e) { + print(e); + setState(() { + error = true; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } @override Widget build(BuildContext context) { - return SelectableText( - text, - style: style, - maxLines: maxLines, - showCursor: true, - textAlign: textAlign, - contextMenuBuilder: (context, editableTextState) { - String selectedText = editableTextState.textEditingValue.selection.textInside(text); - return _MyContextMenu( - anchor: editableTextState.contextMenuAnchors.primaryAnchor, - selectedText: selectedText.trim(), - children: AdaptiveTextSelectionToolbar.getAdaptiveButtons( - context, - editableTextState.contextMenuButtonItems, - ).toList(), - ); - }, - ); + return isLoading + ? const Center(child: CircularProgressIndicator()) + : error + ? const Text( + "An error occured while initiating explanations! Please try again later.", + style: TextStyle(color: AppColors.dangerColor), + ) + : SelectableText.rich( + TextSpan( + children: textSpans, + ), + contextMenuBuilder: (context, editableTextState) { + var selectedIndices = editableTextState.textEditingValue.selection; + String selectedText = + editableTextState.textEditingValue.selection.textInside(widget.text); + for (var element in annotations) { + if (element.startOffset <= selectedIndices.baseOffset && + element.endOffset >= selectedIndices.extentOffset) { + return _MyContextMenu( + anchor: editableTextState.contextMenuAnchors.primaryAnchor, + selectedText: widget.text.substring(element.startOffset, element.endOffset), + annotation: element, + sourceLocation: widget.annotationSourceLocation, + children: AdaptiveTextSelectionToolbar.getAdaptiveButtons( + context, + editableTextState.contextMenuButtonItems, + ).toList(), + ); + } + } + return _MyContextMenu( + anchor: editableTextState.contextMenuAnchors.primaryAnchor, + selectedText: selectedText.trim(), + startOffset: selectedIndices.baseOffset, + endOffset: selectedIndices.extentOffset, + sourceLocation: widget.annotationSourceLocation, + children: AdaptiveTextSelectionToolbar.getAdaptiveButtons( + context, + editableTextState.contextMenuButtonItems, + ).toList(), //TODO + ); + }, + ); } } class _MyContextMenu extends StatelessWidget { + final Annotation? annotation; + final int? startOffset; + final int? endOffset; const _MyContextMenu({ required this.anchor, required this.children, required this.selectedText, + required this.sourceLocation, + this.annotation, + this.startOffset, + this.endOffset, }); final Offset anchor; final List children; final String selectedText; + final String sourceLocation; @override Widget build(BuildContext context) { @@ -63,10 +284,23 @@ class _MyContextMenu extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ //...children, - - AddAnnotationButton(text: selectedText), - const SizedBox(height: 2), - ShowAnnotationButton(text: selectedText), + if (Provider.of(context).isSignedIn) + AddAnnotationButton( + text: selectedText, + startOffset: startOffset, + endOffset: endOffset, + annotation: annotation, + sourceLocation: sourceLocation), + if (annotation != null) + Column( + children: [ + const SizedBox(height: 2), + ShowAnnotationButton( + text: selectedText, + annotation: annotation!, + ), + ], + ) ], ), ), @@ -78,18 +312,21 @@ class _MyContextMenu extends StatelessWidget { class ShowAnnotationButton extends StatelessWidget { final String text; - const ShowAnnotationButton({super.key, required this.text}); + final Annotation annotation; + const ShowAnnotationButton({super.key, required this.text, required this.annotation}); @override Widget build(BuildContext context) { return Responsive( - mobile: MobileShowAnnotationButton(text), desktop: DesktopShowAnnotationButton(text)); + mobile: MobileShowAnnotationButton(text, annotation), + desktop: DesktopShowAnnotationButton(text, annotation)); } } class MobileShowAnnotationButton extends StatefulWidget { final String text; - const MobileShowAnnotationButton(this.text, {super.key}); + final Annotation annotation; + const MobileShowAnnotationButton(this.text, this.annotation, {super.key}); @override State createState() => _MobileShowAnnotationButtonState(); @@ -128,14 +365,14 @@ class _MobileShowAnnotationButtonState extends State style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), - content: const SizedBox( + content: SizedBox( height: 200, width: 400, child: Column( children: [ SelectableText( - "Automaton is a relatively self-operating machine.", - style: TextStyle(color: Colors.white), + widget.annotation.annotationContent, + style: const TextStyle(color: Colors.white), maxLines: 5, ) ], @@ -153,7 +390,8 @@ class _MobileShowAnnotationButtonState extends State class DesktopShowAnnotationButton extends StatefulWidget { final String text; - const DesktopShowAnnotationButton(this.text, {super.key}); + final Annotation annotation; + const DesktopShowAnnotationButton(this.text, this.annotation, {super.key}); @override State createState() => _DesktopShowAnnotationButtonState(); @@ -207,11 +445,21 @@ class _DesktopShowAnnotationButtonState extends State setState(() { isPortalOpen = false; }), + // design better annotation popup child: Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5.0), - color: Colors.grey[900]!.withOpacity(0.9)), + borderRadius: BorderRadius.circular(10.0), + color: Colors.grey[850], + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), // Soft shadow for depth + spreadRadius: 1, + blurRadius: 10, + offset: Offset(0, 4), // changes position of shadow + ), + ], + ), width: 400, constraints: const BoxConstraints(maxWidth: 400), child: SingleChildScrollView( @@ -220,13 +468,27 @@ class _DesktopShowAnnotationButtonState extends State createState() => _AddAnnotationButtonState(); @@ -250,11 +525,71 @@ class AddAnnotationButton extends StatefulWidget { class _AddAnnotationButtonState extends State { bool isHovering = false; bool isPortalOpen = false; + bool isSaving = false; + bool change = false; final TextEditingController _textEditingController = TextEditingController(); - void _submit() { - print(_textEditingController.text); - ContextMenuController.removeAny(); + @override + void initState() { + if (widget.annotation != null) { + change = true; + _textEditingController.text = widget.annotation!.annotationContent; + } + super.initState(); + } + + _submit(BuildContext context) async { + var scaffoldMessenger = ScaffoldMessenger.of(context); + var provider = Provider.of(context, listen: false); + var navigator = Navigator.of(context); + if (mounted) { + setState(() { + isSaving = true; + }); + } + try { + // no update or remove mechanism implemented yet in backend + // DELETE this later if it won't be implemented + if (change) { + Annotation annotation = Annotation( + annotationContent: _textEditingController.text, + startOffset: widget.annotation!.startOffset, + endOffset: widget.annotation!.endOffset, + annotationAuthor: + "http://13.51.205.39/profile/${Provider.of(context, listen: false).user!.email}", + sourceLocation: widget.sourceLocation, + dateCreated: DateTime.now(), + ); + await provider.addAnnotation(annotation); + await provider.deleteAnnotation(widget.annotation!); + } else { + Annotation annotation = Annotation( + annotationContent: _textEditingController.text, + startOffset: widget.startOffset!, + endOffset: widget.endOffset!, + annotationAuthor: + "http://13.51.205.39/profile/${Provider.of(context, listen: false).user!.email}", + sourceLocation: widget.sourceLocation, + dateCreated: DateTime.now(), + ); + await provider.addAnnotation(annotation); + } + } catch (e) { + scaffoldMessenger.showSnackBar( + const SnackBar( + content: Text("Something went wrong!"), + ), + ); + } finally { + if (mounted) { + setState(() { + isSaving = false; + }); + } + navigator.pop(); + } + + //ContextMenuController.removeAny(); } @override @@ -273,49 +608,56 @@ class _AddAnnotationButtonState extends State { onTap: () => showDialog( context: context, builder: (BuildContext context) { - return BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: AlertDialog( - backgroundColor: Colors.grey[800]!.withOpacity(0.7), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6.0), - ), - elevation: 20, - title: Text( - widget.text, - style: const TextStyle( - color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), - ), - content: SizedBox( - height: 200, - width: 400, - child: Column(children: [ - TextField( - controller: _textEditingController, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)), - labelText: 'Annotation', - labelStyle: TextStyle(color: Colors.grey[500]), + return isSaving + ? const Center( + child: CircularProgressIndicator(), + ) + : BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: AlertDialog( + backgroundColor: Colors.grey[800]!.withOpacity(0.7), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6.0), + ), + elevation: 20, + title: TeXView( + child: TeXViewDocument( + widget.text, + style: TeXViewStyle( + contentColor: Colors.white, + fontStyle: TeXViewFontStyle(fontSize: 14), ), - maxLines: 5, - ) - ])), - contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - actions: [ - TextButton( - onPressed: () { - _submit(); - Navigator.of(context).pop(); - }, - child: const Text('Save', style: TextStyle(color: Colors.white)), - ), - ], - ), - ); + )), + content: SizedBox( + height: 200, + width: 400, + child: Column(children: [ + TextField( + controller: _textEditingController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)), + labelText: 'Annotation', + labelStyle: TextStyle(color: Colors.grey[500]), + ), + maxLines: 5, + ) + ])), + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + actions: [ + TextButton( + onPressed: () async { + await _submit(context); + }, + child: const Text('Save', style: TextStyle(color: Colors.white)), + ), + ], + ), + ); }, ), - child: AnnotationButtonItem(isHovering: isHovering, text: "Add Annotation"), + child: AnnotationButtonItem( + isHovering: isHovering, text: change ? "Change Annotation" : "Add Annotation"), ), ); } diff --git a/project/FrontEnd/collaborative_science_platform/lib/widgets/app_button.dart b/project/FrontEnd/collaborative_science_platform/lib/widgets/app_button.dart index 53ecd144..b0dfa549 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/widgets/app_button.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/widgets/app_button.dart @@ -25,12 +25,20 @@ class AppButton extends StatelessWidget { Widget build(BuildContext context) { return ElevatedButton( onPressed: isActive ? onTap : () {}, - style: type != "outlined" + style: (type != "outlined" || !isActive) ? ElevatedButton.styleFrom( backgroundColor: isActive ? (type == "primary" - ? AppColors.primaryColor - : (type == "secondary" ? AppColors.secondaryColor : Colors.grey[600])) + ? const Color.fromRGBO(8, 155, 171, 1) + : (type == "secondary" + ? AppColors.secondaryColor + : (type == "danger" + ? AppColors.dangerColor + : (type == "grey" + ? Colors.grey[600] + : (type == "safe" + ? Color.fromARGB(255, 141, 208, 141) + : Colors.grey[600]))))) : Colors.grey[600], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), @@ -57,7 +65,9 @@ class AppButton extends StatelessWidget { style: TextStyle( fontSize: height / 3.0, fontWeight: FontWeight.bold, - color: (type != "outlined" ? Colors.white : AppColors.primaryColor), + color: ((type != "outlined" || !isActive) + ? Colors.white + : AppColors.primaryColor), )), ], ), diff --git a/project/FrontEnd/collaborative_science_platform/lib/widgets/card_container.dart b/project/FrontEnd/collaborative_science_platform/lib/widgets/card_container.dart index e60960f2..28308625 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/widgets/card_container.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/widgets/card_container.dart @@ -3,14 +3,21 @@ import 'package:flutter/material.dart'; class CardContainer extends StatelessWidget { final Widget child; final Function? onTap; - const CardContainer({super.key, required this.child, this.onTap}); + final Color? backgroundColor; + + const CardContainer({ + super.key, + required this.child, + this.onTap, + this.backgroundColor, + }); @override Widget build(BuildContext context) { if (onTap == null) { return Container( decoration: BoxDecoration( - color: Colors.white, + color: backgroundColor ?? Colors.white, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( @@ -34,7 +41,7 @@ class CardContainer extends StatelessWidget { }, child: Container( decoration: BoxDecoration( - color: Colors.white, + color: backgroundColor ?? Colors.white, borderRadius: BorderRadius.circular(5), boxShadow: [ BoxShadow( diff --git a/project/FrontEnd/collaborative_science_platform/lib/widgets/search_bar_extended.dart b/project/FrontEnd/collaborative_science_platform/lib/widgets/search_bar_extended.dart index 9f68a7aa..32c0cc2e 100644 --- a/project/FrontEnd/collaborative_science_platform/lib/widgets/search_bar_extended.dart +++ b/project/FrontEnd/collaborative_science_platform/lib/widgets/search_bar_extended.dart @@ -11,7 +11,12 @@ import 'package:provider/provider.dart'; class SearchBarExtended extends StatefulWidget { final Function semanticSearch; final Function exactSearch; - const SearchBarExtended({super.key, required this.semanticSearch, required this.exactSearch}); + + const SearchBarExtended({ + super.key, + required this.semanticSearch, + required this.exactSearch, + }); @override State createState() => _SearchBarExtendedState(); diff --git a/project/FrontEnd/collaborative_science_platform/lib/widgets/semantic_search_bar.dart b/project/FrontEnd/collaborative_science_platform/lib/widgets/semantic_search_bar.dart new file mode 100644 index 00000000..6f15cfff --- /dev/null +++ b/project/FrontEnd/collaborative_science_platform/lib/widgets/semantic_search_bar.dart @@ -0,0 +1,268 @@ +import 'package:collaborative_science_platform/models/semantic_tag.dart'; +import 'package:collaborative_science_platform/providers/wiki_data_provider.dart'; +import 'package:collaborative_science_platform/utils/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:collaborative_science_platform/utils/text_styles.dart'; + +class SemanticSearchBar extends StatefulWidget { + final Function addSemanticTag; + + const SemanticSearchBar({ + super.key, + required this.addSemanticTag, + }); + + @override + State createState() => _SemanticSearchBarState(); +} + +class _SemanticSearchBarState extends State { + final TextEditingController searchController = TextEditingController(); + final FocusNode searchFocusNode = FocusNode(); + final TextEditingController labelController = TextEditingController(); + final FocusNode labelFocusNode = FocusNode(); + final int maxLength = 5; + + List tags = []; + bool isLoading = false; + bool error = false; + bool emptySearch = false; + String errorMessage = ""; + int selectedIndex = -1; + + @override + void dispose() { + searchController.dispose(); + searchFocusNode.dispose(); + labelController.dispose(); + labelFocusNode.dispose(); + super.dispose(); + } + + Future search(String query) async { + try { + final WikiDataProvider wikiDataProvider = Provider.of(context, listen: false); + setState(() { + error = false; + isLoading = true; + }); + await wikiDataProvider.wikiDataSearch(query, maxLength); + setState(() { + tags = wikiDataProvider.tags; + }); + } catch (e) { + error = true; + errorMessage = e.toString(); + } finally { + setState(() { + isLoading = false; + }); + } + } + + Widget searchResults() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (emptySearch) const Text( + "No Result", + style: TextStyle( + color: Colors.red, + fontSize: 16, + ), + ), + if (tags.isNotEmpty) const Text( + "Search Results", + style: TextStyles.bodySecondary, + ), + ListView.builder( + padding: const EdgeInsets.all(0.0), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: tags.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.all(4.0), + child: Card( + elevation: 4.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + color: AppColors.primaryLightColor, + child: Padding( + padding: const EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 0.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tags[index].label, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18.0, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + tags[index].description, + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + color: Colors.grey.shade700, + ), + maxLines: 8, + overflow: TextOverflow.ellipsis, + ), + if (index == selectedIndex) Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Container( + height: 36, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: Colors.grey[400]!), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 6.0), + child: TextField( + textAlignVertical: TextAlignVertical.center, + controller: labelController, + focusNode: labelFocusNode, + textInputAction: TextInputAction.go, + decoration: InputDecoration( + hintText: "Tag Name", + border: InputBorder.none, + hintStyle: TextStyle(color: Colors.grey[600]!), + isCollapsed: true, + ), + ), + ), + ), + ), + ), + IconButton( + onPressed: () async { + if (labelController.text.isNotEmpty) { + await widget.addSemanticTag(tags[index].wid, labelController.text); + } + }, + icon: const Icon( + Icons.add, + color: Colors.grey, + ), + ), + ], + ), + Center( + child: IconButton( + onPressed: () { + setState(() { + if (index == selectedIndex) { + selectedIndex = -1; + labelController.text = ""; + } else { + selectedIndex = index; + } + }); + }, + icon: Icon( + (index == selectedIndex) ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, + color: Colors.grey, + ), + ), + ) + ], + ), + ), + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + height: 38, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: Colors.grey[400]!), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () async { + if (searchController.text.isNotEmpty) { + await search(searchController.text); + setState(() { + emptySearch = tags.isEmpty; + }); + } + }, + child: Container( + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + ), + width: 38, + height: 38, + child: Icon( + Icons.search, + color: Colors.indigo[500], + size: 24, + ), + ), + ), + ), + Expanded( + child: TextField( + textAlignVertical: TextAlignVertical.center, + controller: searchController, + focusNode: searchFocusNode, + textInputAction: TextInputAction.search, + decoration: InputDecoration( + hintText: "Search Tag", + border: InputBorder.none, + hintStyle: TextStyle(color: Colors.grey[600]!), + isCollapsed: true, + ), + ), + ), + ], + ), + ), + ), + (isLoading) ? const Center(child: CircularProgressIndicator()) : + (error) ? Text( + errorMessage, + style: const TextStyle( + fontSize: 16.0, + color: Colors.red, + ), + ) : searchResults() + ], + ); + } +} diff --git a/project/FrontEnd/collaborative_science_platform/macos/Flutter/GeneratedPluginRegistrant.swift b/project/FrontEnd/collaborative_science_platform/macos/Flutter/GeneratedPluginRegistrant.swift index 169c3fdc..5f077980 100644 --- a/project/FrontEnd/collaborative_science_platform/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/project/FrontEnd/collaborative_science_platform/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,10 +7,12 @@ import Foundation import path_provider_foundation import share_plus +import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/project/FrontEnd/collaborative_science_platform/pubspec.lock b/project/FrontEnd/collaborative_science_platform/pubspec.lock index 0e566143..22848f78 100644 --- a/project/FrontEnd/collaborative_science_platform/pubspec.lock +++ b/project/FrontEnd/collaborative_science_platform/pubspec.lock @@ -424,6 +424,62 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" sky_engine: dependency: transitive description: flutter diff --git a/project/FrontEnd/collaborative_science_platform/pubspec.yaml b/project/FrontEnd/collaborative_science_platform/pubspec.yaml index 4b210b89..e415e1bd 100644 --- a/project/FrontEnd/collaborative_science_platform/pubspec.yaml +++ b/project/FrontEnd/collaborative_science_platform/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: easy_search_bar: ^2.5.0 share_plus: ^7.2.1 material_floating_search_bar_2: ^0.5.0 + shared_preferences: ^2.2.2 dev_dependencies: flutter_test: diff --git a/qa/bounswe2023group9 b/qa/bounswe2023group9 new file mode 160000 index 00000000..16800aa9 --- /dev/null +++ b/qa/bounswe2023group9 @@ -0,0 +1 @@ +Subproject commit 16800aa90ebe7cb849dcad5fee5cbc1e9863c8c5