diff --git a/packages/widgetbook/.gitignore b/packages/widgetbook/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/packages/widgetbook/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/widgetbook/README.md b/packages/widgetbook/README.md new file mode 100644 index 0000000..5122f24 --- /dev/null +++ b/packages/widgetbook/README.md @@ -0,0 +1,16 @@ +# widgetbook + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/widgetbook/analysis_options.yaml b/packages/widgetbook/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/packages/widgetbook/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/widgetbook/lib/main.dart b/packages/widgetbook/lib/main.dart new file mode 100644 index 0000000..9c268fc --- /dev/null +++ b/packages/widgetbook/lib/main.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_timeline/flutter_timeline.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:timeline_widgetbook/main.directories.g.dart'; +import 'package:timeline_widgetbook/mock_timeline_service.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +void main() { + initializeDateFormatting(); + + runApp(const WidgetBookApp()); +} + +@widgetbook.App() +class WidgetBookApp extends StatelessWidget { + const WidgetBookApp({super.key}); + + @override + Widget build(BuildContext context) { + return Widgetbook.material( + integrations: [ + WidgetbookCloudIntegration(), + ], + addons: [ + DeviceFrameAddon( + devices: [ + Devices.ios.iPhoneSE, + Devices.ios.iPhone13, + Devices.android.bigPhone, + Devices.android.mediumPhone, + Devices.android.smallPhone, + ], + initialDevice: Devices.ios.iPhone13, + ), + MaterialThemeAddon( + themes: [ + WidgetbookTheme( + name: 'Light', + data: ThemeData.light(), + ), + WidgetbookTheme( + name: 'Dark', + data: ThemeData.dark(), + ), + ], + initialTheme: WidgetbookTheme( + name: 'Light', + data: ThemeData.light(), + ), + ), + ], + directories: directories, + ); + } +} + +@widgetbook.UseCase( + designLink: + 'https://www.figma.com/file/PRJoVXQ5aOjAICfkQdAq2A/Iconica-User-Stories?type=design&node-id=34-2763&mode=design&t=W72P3tkEascAKDCk-4', + name: 'Timeline post screen', + type: TimelinePostScreen, +) +Widget postScreenUseCase(BuildContext context) { + var service = TestTimelineService()..fetchPosts(null); + var options = const TimelineOptions(); + return TimelinePostScreen( + userId: '1', + service: service, + options: options, + post: service.posts.first, + onPostDelete: () {}, + ); +} + +@widgetbook.UseCase( + designLink: + 'https://www.figma.com/file/PRJoVXQ5aOjAICfkQdAq2A/Iconica-User-Stories?type=design&node-id=34-2763&mode=design&t=W72P3tkEascAKDCk-4', + name: 'Timeline screen', + type: TimelineScreen, +) +Widget timelineUseCase(BuildContext context) { + var service = TestTimelineService()..fetchPosts(null); + var options = const TimelineOptions(); + return TimelineScreen( + userId: '1', + options: options, + onPostTap: (_) {}, + service: service, + ); +} diff --git a/packages/widgetbook/lib/main.directories.g.dart b/packages/widgetbook/lib/main.directories.g.dart new file mode 100644 index 0000000..be2b322 --- /dev/null +++ b/packages/widgetbook/lib/main.directories.g.dart @@ -0,0 +1,39 @@ +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_import, prefer_relative_imports, directives_ordering + +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AppGenerator +// ************************************************************************** + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:timeline_widgetbook/main.dart' as _i2; +import 'package:widgetbook/widgetbook.dart' as _i1; + +final directories = <_i1.WidgetbookNode>[ + _i1.WidgetbookFolder( + name: 'screens', + children: [ + _i1.WidgetbookLeafComponent( + name: 'TimelinePostScreen', + useCase: _i1.WidgetbookUseCase( + name: 'Timeline post screen', + builder: _i2.postScreenUseCase, + designLink: + 'https://www.figma.com/file/PRJoVXQ5aOjAICfkQdAq2A/Iconica-User-Stories?type=design&node-id=34-2763&mode=design&t=W72P3tkEascAKDCk-4', + ), + ), + _i1.WidgetbookLeafComponent( + name: 'TimelineScreen', + useCase: _i1.WidgetbookUseCase( + name: 'Timeline screen', + builder: _i2.timelineUseCase, + designLink: + 'https://www.figma.com/file/PRJoVXQ5aOjAICfkQdAq2A/Iconica-User-Stories?type=design&node-id=34-2763&mode=design&t=W72P3tkEascAKDCk-4', + ), + ), + ], + ) +]; diff --git a/packages/widgetbook/lib/mock_timeline_service.dart b/packages/widgetbook/lib/mock_timeline_service.dart new file mode 100644 index 0000000..0c49b3d --- /dev/null +++ b/packages/widgetbook/lib/mock_timeline_service.dart @@ -0,0 +1,233 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_timeline/flutter_timeline.dart'; +import 'package:uuid/uuid.dart'; + +class TestTimelineService + with ChangeNotifier + implements TimelineService, TimelineUserService { + List _posts = []; + + List get posts => _posts; + + @override + Future createPost(TimelinePost post) async { + _posts.add( + post.copyWith( + creator: const TimelinePosterUserModel(userId: 'test_user'), + ), + ); + notifyListeners(); + return post; + } + + @override + Future deletePost(TimelinePost post) async { + _posts = _posts.where((element) => element.id != post.id).toList(); + + notifyListeners(); + } + + @override + Future deletePostReaction( + TimelinePost post, + String reactionId, + ) async { + if (post.reactions != null && post.reactions!.isNotEmpty) { + var reaction = + post.reactions!.firstWhere((element) => element.id == reactionId); + var updatedPost = post.copyWith( + reaction: post.reaction - 1, + reactions: (post.reactions ?? [])..remove(reaction), + ); + _posts = _posts + .map( + (p) => p.id == post.id ? updatedPost : p, + ) + .toList(); + + notifyListeners(); + return updatedPost; + } + return post; + } + + @override + Future fetchPostDetails(TimelinePost post) async { + var reactions = post.reactions ?? []; + var updatedReactions = []; + for (var reaction in reactions) { + updatedReactions.add(reaction.copyWith( + creator: const TimelinePosterUserModel(userId: 'test_user'))); + } + var updatedPost = post.copyWith(reactions: updatedReactions); + _posts = _posts.map((p) => (p.id == post.id) ? updatedPost : p).toList(); + notifyListeners(); + return updatedPost; + } + + @override + Future> fetchPosts(String? category) async { + var posts = getMockedPosts(); + _posts = posts; + notifyListeners(); + return posts; + } + + @override + Future> fetchPostsPaginated( + String? category, + int limit, + ) async { + notifyListeners(); + return _posts; + } + + @override + Future fetchPost(TimelinePost post) async { + notifyListeners(); + return post; + } + + @override + Future> refreshPosts(String? category) async { + var posts = []; + + _posts = [...posts, ..._posts]; + notifyListeners(); + return posts; + } + + @override + TimelinePost? getPost(String postId) => + (_posts.any((element) => element.id == postId)) + ? _posts.firstWhere((element) => element.id == postId) + : null; + + @override + List getPosts(String? category) => _posts + .where((element) => category == null || element.category == category) + .toList(); + + @override + Future likePost(String userId, TimelinePost post) async { + var updatedPost = post.copyWith( + likes: post.likes + 1, + likedBy: (post.likedBy ?? [])..add(userId), + ); + _posts = _posts + .map( + (p) => p.id == post.id ? updatedPost : p, + ) + .toList(); + + notifyListeners(); + return updatedPost; + } + + @override + Future unlikePost(String userId, TimelinePost post) async { + var updatedPost = post.copyWith( + likes: post.likes - 1, + likedBy: post.likedBy?..remove(userId), + ); + _posts = _posts + .map( + (p) => p.id == post.id ? updatedPost : p, + ) + .toList(); + + notifyListeners(); + return updatedPost; + } + + @override + Future reactToPost( + TimelinePost post, + TimelinePostReaction reaction, { + Uint8List? image, + }) async { + var reactionId = const Uuid().v4(); + var updatedReaction = reaction.copyWith( + id: reactionId, + creator: const TimelinePosterUserModel(userId: 'test_user')); + + var updatedPost = post.copyWith( + reaction: post.reaction + 1, + reactions: post.reactions?..add(updatedReaction), + ); + + _posts = _posts + .map( + (p) => p.id == post.id ? updatedPost : p, + ) + .toList(); + notifyListeners(); + return updatedPost; + } + + List getMockedPosts() { + return [ + for (var i = 0; i < 20; i++) ...[ + if (i == 0) ...[ + TimelinePost( + id: 'Post$i', + creatorId: 'test_user', + title: 'Post $i', + category: 'text', + content: "Post $i content", + likes: i, + reaction: 0, + reactions: getMockedReactions('Post$i'), + createdAt: DateTime.now().subtract(Duration(days: i % 10)), + reactionEnabled: true, + imageUrl: 'https://picsum.photos/seed/$i/200/300', + ) + ] else ...[ + TimelinePost( + id: 'Post$i', + creatorId: 'test_user', + title: 'Post $i', + category: 'text', + content: "Post $i content", + likes: i, + reaction: 0, + createdAt: DateTime.now().subtract(Duration(days: i % 10)), + reactionEnabled: false, + imageUrl: 'https://picsum.photos/seed/$i/200/300', + ) + ], + ] + ]; + } + + List getMockedReactions(String posdId) { + return [ + for (var i = 0; i < 20; i++) ...[ + TimelinePostReaction( + id: 'Reaction$i', + postId: posdId, + reaction: 'Reaction $i', + createdAt: DateTime.now().subtract(Duration(days: i % 10)), + creatorId: 'test_user', + imageUrl: + (i % 2 == 0) ? 'https://picsum.photos/seed/$i/200/300' : null, + ) + ] + ]; + } + + @override + Future getUser(String userId) async { + return TimelinePosterUserModel( + userId: userId, + ); + } + + @override + set posts(List posts) { + _posts = posts; + notifyListeners(); + } +} diff --git a/packages/widgetbook/pubspec.yaml b/packages/widgetbook/pubspec.yaml new file mode 100644 index 0000000..cbc3ebb --- /dev/null +++ b/packages/widgetbook/pubspec.yaml @@ -0,0 +1,29 @@ +name: timeline_widgetbook +description: "A new Flutter project." +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.2.5 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + cupertino_icons: ^1.0.2 + widgetbook_annotation: ^3.1.0 + widgetbook: ^3.7.1 + flutter_timeline: + path: ../flutter_timeline + intl: ^0.19.0 + +dev_dependencies: + build_runner: any + widgetbook_generator: ^3.7.0 + flutter_test: + sdk: flutter + + flutter_lints: ^2.0.0 + +flutter: + uses-material-design: true \ No newline at end of file diff --git a/packages/widgetbook/web/favicon.png b/packages/widgetbook/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/packages/widgetbook/web/favicon.png differ diff --git a/packages/widgetbook/web/icons/Icon-192.png b/packages/widgetbook/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/packages/widgetbook/web/icons/Icon-192.png differ diff --git a/packages/widgetbook/web/icons/Icon-512.png b/packages/widgetbook/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/packages/widgetbook/web/icons/Icon-512.png differ diff --git a/packages/widgetbook/web/icons/Icon-maskable-192.png b/packages/widgetbook/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/packages/widgetbook/web/icons/Icon-maskable-192.png differ diff --git a/packages/widgetbook/web/icons/Icon-maskable-512.png b/packages/widgetbook/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/packages/widgetbook/web/icons/Icon-maskable-512.png differ diff --git a/packages/widgetbook/web/index.html b/packages/widgetbook/web/index.html new file mode 100644 index 0000000..9dc83bd --- /dev/null +++ b/packages/widgetbook/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + widgetbook + + + + + + + + + + diff --git a/packages/widgetbook/web/manifest.json b/packages/widgetbook/web/manifest.json new file mode 100644 index 0000000..6bd71b6 --- /dev/null +++ b/packages/widgetbook/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "widgetbook", + "short_name": "widgetbook", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}