├── .gitignore
├── .metadata
├── README.md
├── analysis_options.yaml
├── lib
├── app
│ ├── app.dart
│ ├── navigation
│ │ ├── custom_rect_tween.dart
│ │ ├── hero_dialog_route.dart
│ │ └── navigation.dart
│ ├── state
│ │ ├── app_state.dart
│ │ ├── demo_users.dart
│ │ ├── models
│ │ │ ├── models.dart
│ │ │ └── user.dart
│ │ └── state.dart
│ ├── stream_agram.dart
│ ├── theme.dart
│ └── utils.dart
├── components
│ ├── app_widgets
│ │ ├── app_widgets.dart
│ │ ├── avatars.dart
│ │ ├── comment_box.dart
│ │ ├── favorite_icon.dart
│ │ └── tap_fade_icon.dart
│ ├── comments
│ │ ├── comments.dart
│ │ ├── comments_screen.dart
│ │ └── state
│ │ │ ├── comment_state.dart
│ │ │ └── state.dart
│ ├── home
│ │ ├── home.dart
│ │ └── home_screen.dart
│ ├── login
│ │ ├── login.dart
│ │ └── login_screen.dart
│ ├── new_post
│ │ ├── new_post.dart
│ │ └── new_post_screen.dart
│ ├── profile
│ │ ├── edit_profile_screen.dart
│ │ ├── profile.dart
│ │ └── profile_page.dart
│ ├── search
│ │ ├── search.dart
│ │ └── search_page.dart
│ └── timeline
│ │ ├── timeline.dart
│ │ ├── timeline_page.dart
│ │ └── widgets
│ │ ├── post_card.dart
│ │ └── widgets.dart
└── main.dart
├── previews
└── preview.png
├── pubspec.lock
└── pubspec.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | /android
2 | /ios
3 | /windows
4 | /linux
5 | /macos
6 | /web
7 |
8 | # Miscellaneous
9 | *.class
10 | *.log
11 | *.pyc
12 | *.swp
13 | .DS_Store
14 | .atom/
15 | .buildlog/
16 | .history
17 | .svn/
18 |
19 | # IntelliJ related
20 | *.iml
21 | *.ipr
22 | *.iws
23 | .idea/
24 |
25 | # The .vscode folder contains launch configuration and tasks you configure in
26 | # VS Code which you may wish to be included in version control, so this line
27 | # is commented out by default.
28 | #.vscode/
29 |
30 | # Flutter/Dart/Pub related
31 | **/doc/api/
32 | **/ios/Flutter/.last_build_id
33 | .dart_tool/
34 | .flutter-plugins
35 | .flutter-plugins-dependencies
36 | .packages
37 | .pub-cache/
38 | .pub/
39 | /build/
40 |
41 | # Web related
42 | lib/generated_plugin_registrant.dart
43 |
44 | # Symbolication related
45 | app.*.symbols
46 |
47 | # Obfuscation related
48 | app.*.map.json
49 |
50 | # Android Studio will place build artifacts here
51 | /android/app/debug
52 | /android/app/profile
53 | /android/app/release
54 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled.
5 |
6 | version:
7 | revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
8 | channel: stable
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
17 | base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
18 | - platform: android
19 | create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
20 | base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
21 | - platform: ios
22 | create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
23 | base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
24 | - platform: linux
25 | create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
26 | base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
27 | - platform: macos
28 | create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
29 | base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
30 | - platform: web
31 | create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
32 | base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
33 | - platform: windows
34 | create_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
35 | base_revision: ffccd96b62ee8cec7740dab303538c5fc26ac543
36 |
37 | # User provided section
38 |
39 | # List of Local paths (relative to this file) that should be
40 | # ignored by the migrate tool.
41 | #
42 | # Files that are not part of the templates will be ignored by default.
43 | unmanaged_files:
44 | - 'lib/main.dart'
45 | - 'ios/Runner.xcodeproj/project.pbxproj'
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Flutter Instagram Clone (Stream-agram)
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Stream-agram is a demo application showing how to recreate Instagram using Flutter and Stream Feeds.
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## Tutorials
20 | If you'd like to learn more about this project and how the code is structured, take a look at the companion blog and video. Make sure you're on the [tutorial branch](https://github.com/GetStream/flutter-instagram-clone/tree/tutorial) - this branch is the historic branch that aligns with the tutorial. The `main` branch is the most up to date with the latest code changes and package versions.
21 |
22 | ```bash
23 | git checkout tutorial
24 | ```
25 |
26 | ### Blog Post
27 | - [Article](https://getstream.io/blog/instagram-clone-flutter/) detailing a step-by-step guide for this project.
28 |
29 | ### YouTube Video
30 | - [Video](https://youtu.be/fHRB6KGoaV0) showing how to create this application from start to finish. The video follows the same steps as the blog post, and we recommend that you use both resources to make the tutorial easy to follow.
31 |
32 | ## Previews
33 |
34 |
35 |
36 |
37 |
38 |
39 | ## Supported features
40 | - Light and Dark themes.
41 | - Instagram-like animations and transitions
42 | - Sign in using different user accounts
43 | - Add and change profile pictures
44 | - Add photo posts to your own user feed (activities)
45 | - Subscribe/unsubscribe to other users’ feeds
46 | - Add comments and likes (reactions)
47 |
48 |
49 | ## Getting Started
50 | These are the steps to run this project locally, with your own Stream Feeds configuration.
51 |
52 | ### Clone This Repository
53 | ```
54 | git clone https://github.com/GetStream/flutter-instagram-clone
55 | ```
56 |
57 | The project folder needs to be renamed in order for `flutter create` to work (dashes "-" in the folder name are not allowed). You can do this however you want. For example, from a bash terminal you can do:
58 | ```bash
59 | mv flutter-instagram-clone flutter_instagram_clone
60 | ```
61 |
62 | ### Create Flutter Platform Folders
63 | Run this inside the main folder to generate platform folders.
64 | ```
65 | flutter create .
66 | ```
67 |
68 | ### Setup Image Picker Package
69 | Depending on the platform that you're targetting you will need to do some [setup](https://pub.dev/packages/image_picker#installation) for the [image_picker](https://pub.dev/packages/image_picker) Flutter package. At the time of writing this package only supports **Android**, **iOS** and **Web**.
70 |
71 | ### Add Keys and Tokens
72 | You will also need to add your **Stream API-Key** and **User Frontend Tokens**. These are marked with TODOs in the codebase. For additional information, see the [blog](https://getstream.io/blog/instagram-clone-flutter/#creating-demo-instagram-users) or [video](https://www.youtube.com/watch?v=fHRB6KGoaV0&t=627s) section on creating user tokens.
73 |
74 |
75 |
76 |
77 | ## Stream Feeds Flutter SDK
78 | Stream-agram is built with [Stream Feeds](https://getstream.io/activity-feeds/) for implementing activity feeds.
79 | - [Activity Feeds Tutorial](https://getstream.io/activity-feeds/sdk/flutter/tutorial/) - Basic tutorials for getting started with activity feeds.
80 | - [Stream Feeds Flutter Repository](https://github.com/GetStream/stream-feed-flutter) - Official Flutter SDK for Stream Feeds.
81 | - [Feed Client Documentation](https://getstream.io/activity-feeds/docs/flutter-dart/?language=dart) - Detailed documentation of the Activity Feeds client.
82 |
83 |
84 |
85 | ## Stream Chat Flutter SDK
86 | If you're interested in adding chat functionality to your Instagram clone, check out [Stream Chat](https://getstream.io/chat/).
87 | - [Chat Messaging Tutorial](https://getstream.io/chat/flutter/tutorial/) - Basic tutorials for getting started by building a simple messaging app.
88 | - [Stream Chat Flutter repository](https://github.com/GetStream/stream-chat-flutter) - Official Flutter SDK for Stream Chat.
89 | - [Chat Client Documentation](https://getstream.io/chat/docs/flutter-dart/?language=dart) - Full documentation of the Chat client for requesting API calls.
90 | - [Chat UI Components Documentation and Guides](https://getstream.io/chat/docs/sdk/flutter/) - Full documentation of the Stream UI Components.
91 | - [Sample Application](https://github.com/GetStream/flutter-samples/tree/main/packages/stream_chat_v1) - Official Flutter sample chat application.
92 |
93 | ## Find this repository useful? 💙
94 | Support it by joining __[stargazers](https://github.com/GetStream/flutter-instagram-clone/stargazers)__ :star:
95 |
96 | # License
97 | ```xml
98 | Copyright 2021 Stream.IO, Inc. All Rights Reserved.
99 |
100 | Licensed under the Apache License, Version 2.0 (the "License");
101 | you may not use this file except in compliance with the License.
102 | You may obtain a copy of the License at
103 |
104 | http://www.apache.org/licenses/LICENSE-2.0
105 |
106 | Unless required by applicable law or agreed to in writing, software
107 | distributed under the License is distributed on an "AS IS" BASIS,
108 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
109 | See the License for the specific language governing permissions and
110 | limitations under the License.
111 | ```
112 |
113 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 |
28 | # Additional information about this file can be found at
29 | # https://dart.dev/guides/language/analysis-options
30 |
--------------------------------------------------------------------------------
/lib/app/app.dart:
--------------------------------------------------------------------------------
1 | export 'state/state.dart';
2 | export 'theme.dart';
3 | export 'stream_agram.dart';
4 | export 'utils.dart';
5 | export 'navigation/navigation.dart';
6 |
--------------------------------------------------------------------------------
/lib/app/navigation/custom_rect_tween.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:flutter/widgets.dart';
4 |
5 | /// {@template custom_rect_tween}
6 | /// Linear RectTween with a [Curves.easeOut] curve.
7 | ///
8 | /// Less dramatic than the regular [RectTween] used in [Hero] animations.
9 | /// {@endtemplate}
10 | class CustomRectTween extends RectTween {
11 | /// {@macro custom_rect_tween}
12 | CustomRectTween({
13 | required Rect? begin,
14 | required Rect? end,
15 | }) : super(begin: begin, end: end);
16 |
17 | @override
18 | Rect? lerp(double t) {
19 | final elasticCurveValue = Curves.easeOut.transform(t);
20 | if (begin == null || end == null) return null;
21 | return Rect.fromLTRB(
22 | lerpDouble(begin!.left, end!.left, elasticCurveValue)!,
23 | lerpDouble(begin!.top, end!.top, elasticCurveValue)!,
24 | lerpDouble(begin!.right, end!.right, elasticCurveValue)!,
25 | lerpDouble(begin!.bottom, end!.bottom, elasticCurveValue)!,
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/app/navigation/hero_dialog_route.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | /// {@template hero_dialog_route}
4 | /// Custom [PageRoute] that creates an overlay dialog (popup effect).
5 | ///
6 | /// Best used with a [Hero] animation.
7 | /// {@endtemplate}
8 | class HeroDialogRoute extends PageRoute {
9 | /// {@macro hero_dialog_route}
10 | HeroDialogRoute({
11 | required WidgetBuilder builder,
12 | RouteSettings? settings,
13 | bool fullscreenDialog = false,
14 | }) : _builder = builder,
15 | super(settings: settings, fullscreenDialog: fullscreenDialog);
16 |
17 | final WidgetBuilder _builder;
18 |
19 | @override
20 | bool get opaque => false;
21 |
22 | @override
23 | bool get barrierDismissible => true;
24 |
25 | @override
26 | Duration get transitionDuration => const Duration(milliseconds: 300);
27 |
28 | @override
29 | bool get maintainState => true;
30 |
31 | @override
32 | Color get barrierColor => Colors.black54;
33 |
34 | @override
35 | Widget buildTransitions(BuildContext context, Animation animation,
36 | Animation secondaryAnimation, Widget child) {
37 | return FadeTransition(opacity: animation, child: child);
38 | }
39 |
40 | @override
41 | Widget buildPage(BuildContext context, Animation animation,
42 | Animation secondaryAnimation) {
43 | return _builder(context);
44 | }
45 |
46 | @override
47 | String get barrierLabel => 'Hero Dialog Open';
48 | }
49 |
--------------------------------------------------------------------------------
/lib/app/navigation/navigation.dart:
--------------------------------------------------------------------------------
1 | export 'custom_rect_tween.dart';
2 | export 'hero_dialog_route.dart';
3 |
--------------------------------------------------------------------------------
/lib/app/state/app_state.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
3 | import 'models/models.dart';
4 |
5 | import 'demo_users.dart';
6 |
7 | /// State related to Stream-agram app.
8 | ///
9 | /// Manages the connection and stores a references to the [StreamFeedClient]
10 | /// and [StreamagramUser].
11 | ///
12 | /// Provides various convenience methods.
13 | class AppState extends ChangeNotifier {
14 | /// Create new [AppState].
15 | AppState({
16 | required StreamFeedClient client,
17 | }) : _client = client;
18 |
19 | late final StreamFeedClient _client;
20 |
21 | /// Stream Feed client.
22 | StreamFeedClient get client => _client;
23 |
24 | /// Stream Feed user - [StreamUser].
25 | StreamUser get user => _client.currentUser!;
26 |
27 | StreamagramUser? _streamagramUser;
28 | var isUploadingProfilePicture = false;
29 |
30 | /// The extraData from [user], mapped to an [StreamagramUser] object.
31 | StreamagramUser? get streamagramUser => _streamagramUser;
32 |
33 | /// Current user's [FlatFeed] with name 'user'.
34 | ///
35 | /// This feed contains all of a user's personal posts.
36 | FlatFeed get currentUserFeed => _client.flatFeed('user', user.id);
37 |
38 | /// Current user's [FlatFeed] with name 'timeline'.
39 | ///
40 | /// This contains all posts that a user has subscribed (followed) to.
41 | FlatFeed get currentTimelineFeed => _client.flatFeed('timeline', user.id);
42 |
43 | /// Connect to Stream Feed with one of the demo users, using a predefined,
44 | /// hardcoded token.
45 | ///
46 | /// THIS IS ONLY FOR DEMONSTRATIONS PURPOSES. USER TOKENS SHOULD NOT BE
47 | /// HARDCODED LIKE THIS.
48 | Future connect(DemoAppUser demoUser) async {
49 | final currentUser = await _client.setUser(
50 | User(id: demoUser.id),
51 | demoUser.token!,
52 | extraData: demoUser.data,
53 | );
54 |
55 | if (currentUser.data != null) {
56 | _streamagramUser = StreamagramUser.fromMap(currentUser.data!);
57 | await currentTimelineFeed.follow(currentUserFeed);
58 | notifyListeners();
59 | return true;
60 | } else {
61 | return false;
62 | }
63 | }
64 |
65 | /// Uploads a new profile picture from the given [filePath].
66 | ///
67 | /// This will call [notifyListeners] and update the local [_streamagramUser] state.
68 | Future updateProfilePhoto(String filePath) async {
69 | // Upload the original image
70 | isUploadingProfilePicture = true;
71 | notifyListeners();
72 |
73 | final imageUrl = await client.images.upload(AttachmentFile(path: filePath));
74 | if (imageUrl == null) {
75 | debugPrint('Could not upload the image. Not setting profile picture');
76 | isUploadingProfilePicture = false;
77 | notifyListeners();
78 | return;
79 | }
80 | // Get resized images using the Stream Feed client.
81 | final results = await Future.wait([
82 | client.images.getResized(
83 | imageUrl,
84 | const Resize(500, 500),
85 | ),
86 | client.images.getResized(
87 | imageUrl,
88 | const Resize(50, 50),
89 | )
90 | ]);
91 |
92 | // Update the current user data state.
93 | _streamagramUser = _streamagramUser?.copyWith(
94 | profilePhoto: imageUrl,
95 | profilePhotoResized: results[0],
96 | profilePhotoThumbnail: results[1],
97 | );
98 |
99 | isUploadingProfilePicture = false;
100 |
101 | // Upload the new user data for the current user.
102 | if (_streamagramUser != null) {
103 | await client.currentUser!.update(_streamagramUser!.toMap());
104 | }
105 |
106 | notifyListeners();
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/lib/app/state/demo_users.dart:
--------------------------------------------------------------------------------
1 | import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
2 |
3 | /// Demo application users.
4 | enum DemoAppUser {
5 | sahil,
6 | sacha,
7 | salvatore,
8 | gordon,
9 | }
10 |
11 | /// Convenient class Extension on [DemoAppUser] enum
12 | extension DemoAppUserX on DemoAppUser {
13 | /// Convenient method Extension to generate an [id] from [DemoAppUser] enum
14 | String get id => {
15 | DemoAppUser.sahil: 'sahil-kumar',
16 | DemoAppUser.sacha: 'sacha-arbonel',
17 | DemoAppUser.salvatore: 'salvatore-giordano',
18 | DemoAppUser.gordon: 'gordon-hayes',
19 | }[this]!;
20 |
21 | /// Convenient method Extension to generate a [name] from [DemoAppUser] enum
22 | String? get name => {
23 | DemoAppUser.sahil: 'Sahil Kumar',
24 | DemoAppUser.sacha: 'Sacha Arbonel',
25 | DemoAppUser.salvatore: 'Salvatore Giordano',
26 | DemoAppUser.gordon: 'Gordon Hayes',
27 | }[this];
28 |
29 | /// Convenient method Extension to generate [data] from [DemoAppUser] enum
30 | Map? get data => {
31 | DemoAppUser.sahil: {
32 | 'first_name': 'Sahil',
33 | 'last_name': 'Kumar',
34 | 'full_name': 'Sahil Kumar',
35 | },
36 | DemoAppUser.sacha: {
37 | 'first_name': 'Sacha',
38 | 'last_name': 'Arbonel',
39 | 'full_name': 'Sacha Arbonel',
40 | },
41 | DemoAppUser.salvatore: {
42 | 'first_name': 'Salvatore',
43 | 'last_name': 'Giordano',
44 | 'full_name': 'Salvatore Giordano',
45 | },
46 | DemoAppUser.gordon: {
47 | 'first_name': 'Gordon',
48 | 'last_name': 'Hayes',
49 | 'full_name': 'Gordon Hayes',
50 | },
51 | }[this];
52 |
53 | /// Convenient method Extension to generate a [token] from [DemoAppUser] enum
54 | Token? get token => {
55 | // TODO: Generate your own tokens if you're using your own API key.
56 | DemoAppUser.sahil: const Token(
57 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FoaWwta3VtYXIifQ.Ts_4yhx6P4syDdO5g0QKJXqcET-0UO3mZHY_tKbseoA'),
58 | DemoAppUser.sacha: const Token(
59 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FjaGEtYXJib25lbCJ9.atw_x8yl5bnhXbDKntlNtIVfLfQm9fe2xpaUuzIHHsM'),
60 | DemoAppUser.salvatore: const Token(
61 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlLWdpb3JkYW5vIn0.C3sS6UM6LhZbM2evWaIDlp8N_V3g11fvah9Llk3Gs4w'),
62 | DemoAppUser.gordon: const Token(
63 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZ29yZG9uLWhheWVzIn0.q0C65xjNtMdZ62pHcVodSjP6SqVh_BL9GNYavnp0l-4'),
64 | }[this];
65 | }
66 |
--------------------------------------------------------------------------------
/lib/app/state/models/models.dart:
--------------------------------------------------------------------------------
1 | export 'user.dart';
2 |
--------------------------------------------------------------------------------
/lib/app/state/models/user.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | /// Data model for a feed user's extra data.
6 | @immutable
7 | class StreamagramUser {
8 | /// Data model for a feed user's extra data.
9 | const StreamagramUser({
10 | required this.firstName,
11 | required this.lastName,
12 | required this.fullName,
13 | required this.profilePhoto,
14 | required this.profilePhotoResized,
15 | required this.profilePhotoThumbnail,
16 | });
17 |
18 | /// Converts a Map to this.
19 | factory StreamagramUser.fromMap(Map map) {
20 | return StreamagramUser(
21 | firstName: map['first_name'] as String? ?? 'No name',
22 | lastName: map['last_name'] as String? ?? 'No last name',
23 | fullName: map['full_name'] as String? ?? 'No full name',
24 | profilePhoto: map['profile_photo'] as String?,
25 | profilePhotoResized: map['profile_photo_resized'] as String?,
26 | profilePhotoThumbnail: map['profile_photo_thumbnail'] as String?,
27 | );
28 | }
29 |
30 | /// Converts json to this.
31 | factory StreamagramUser.fromJson(String source) =>
32 | StreamagramUser.fromMap(json.decode(source) as Map);
33 |
34 | /// User's first name
35 | final String firstName;
36 |
37 | /// User's last name
38 | final String lastName;
39 |
40 | /// User's full name
41 | final String fullName;
42 |
43 | /// URL to user's profile photo.
44 | final String? profilePhoto;
45 |
46 | /// A 500x500 version of the [profilePhoto].
47 | final String? profilePhotoResized;
48 |
49 | /// A small thumbnail version of the [profilePhoto].
50 | final String? profilePhotoThumbnail;
51 |
52 | /// Convenient method to replace certain fields.
53 | StreamagramUser copyWith({
54 | String? firstName,
55 | String? lastName,
56 | String? fullName,
57 | String? profilePhoto,
58 | String? profilePhotoResized,
59 | String? profilePhotoThumbnail,
60 | }) {
61 | return StreamagramUser(
62 | firstName: firstName ?? this.firstName,
63 | lastName: lastName ?? this.lastName,
64 | fullName: fullName ?? this.fullName,
65 | profilePhoto: profilePhoto ?? this.profilePhoto,
66 | profilePhotoResized: profilePhotoResized ?? this.profilePhotoResized,
67 | profilePhotoThumbnail:
68 | profilePhotoThumbnail ?? this.profilePhotoThumbnail,
69 | );
70 | }
71 |
72 | /// Converts this model to a Map.
73 | Map toMap() {
74 | return {
75 | 'first_name': firstName,
76 | 'last_name': lastName,
77 | 'full_name': fullName,
78 | 'profile_photo': profilePhoto,
79 | 'profile_photo_resized': profilePhotoResized,
80 | 'profile_photo_thumbnail': profilePhotoThumbnail,
81 | };
82 | }
83 |
84 | /// Converts this class to json.
85 | String toJson() => json.encode(toMap());
86 |
87 | @override
88 | String toString() {
89 | return '''UserData(firstName: $firstName, lastName: $lastName, fullName: $fullName, profilePhoto: $profilePhoto, profilePhotoResized: $profilePhotoResized, profilePhotoThumbnail: $profilePhotoThumbnail)''';
90 | }
91 |
92 | @override
93 | bool operator ==(Object other) {
94 | if (identical(this, other)) return true;
95 |
96 | return other is StreamagramUser &&
97 | other.firstName == firstName &&
98 | other.lastName == lastName &&
99 | other.fullName == fullName &&
100 | other.profilePhoto == profilePhoto &&
101 | other.profilePhotoResized == profilePhotoResized &&
102 | other.profilePhotoThumbnail == profilePhotoThumbnail;
103 | }
104 |
105 | @override
106 | int get hashCode {
107 | return firstName.hashCode ^
108 | lastName.hashCode ^
109 | fullName.hashCode ^
110 | profilePhoto.hashCode ^
111 | profilePhotoResized.hashCode ^
112 | profilePhotoThumbnail.hashCode;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/lib/app/state/state.dart:
--------------------------------------------------------------------------------
1 | export 'app_state.dart';
2 | export 'demo_users.dart';
3 | export 'models/models.dart';
4 |
--------------------------------------------------------------------------------
/lib/app/stream_agram.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 | import 'package:stream_agram/app/app.dart';
4 | import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
5 |
6 | import '../components/login/login.dart';
7 |
8 | /// {@template app}
9 | /// Main entry point to the Stream-agram application.
10 | /// {@endtemplate}
11 | class StreamagramApp extends StatefulWidget {
12 | /// {@macro app}
13 | const StreamagramApp({
14 | Key? key,
15 | required this.appTheme,
16 | }) : super(key: key);
17 |
18 | /// App's theme data.
19 | final AppTheme appTheme;
20 |
21 | @override
22 | State createState() => _StreamagramAppState();
23 | }
24 |
25 | class _StreamagramAppState extends State {
26 | final _client =
27 | StreamFeedClient('eyssk29az2kj'); // TODO: Add Stream API Token
28 | late final appState = AppState(client: _client);
29 |
30 | // Important to only initialize this once.
31 | // Unless you want to update the bloc state
32 | late final feedBloc = FeedBloc(client: _client);
33 |
34 | @override
35 | Widget build(BuildContext context) {
36 | return ChangeNotifierProvider.value(
37 | value: appState,
38 | child: MaterialApp(
39 | title: 'Stream-agram',
40 | theme: widget.appTheme.lightTheme,
41 | darkTheme: widget.appTheme.darkTheme,
42 | themeMode: ThemeMode.dark,
43 | builder: (context, child) {
44 | // Stream Feeds provider to give access to [FeedBloc]
45 | // This class comes from Stream Feeds.
46 | return FeedProvider(
47 | bloc: feedBloc,
48 | child: child!,
49 | );
50 | },
51 | home: const LoginScreen(),
52 | ),
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/lib/app/theme.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | /// Global reference to application colors.
4 | abstract class AppColors {
5 | /// Dark color.
6 | static const dark = Colors.black;
7 |
8 | static const light = Color(0xFFFAFAFA);
9 |
10 | /// Grey background accent.
11 | static const grey = Color(0xFF262626);
12 |
13 | /// Primary text color
14 | static const primaryText = Colors.white;
15 |
16 | /// Secondary color.
17 | static const secondary = Color(0xFF0095F6);
18 |
19 | /// Color to use for favorite icons (indicating a like).
20 | static const like = Colors.red;
21 |
22 | /// Grey faded color.
23 | static const faded = Colors.grey;
24 |
25 | /// Light grey color
26 | static const ligthGrey = Color(0xFFEEEEEE);
27 |
28 | /// Top gradient color used in various UI components.
29 | static const topGradient = Color(0xFFE60064);
30 |
31 | /// Bottom gradient color used in various UI components.
32 | static const bottomGradient = Color(0xFFFFB344);
33 | }
34 |
35 | /// Global reference to application [TextStyle]s.
36 | abstract class AppTextStyle {
37 | /// A medium bold text style.
38 | static const textStyleBoldMedium = TextStyle(
39 | fontWeight: FontWeight.w600,
40 | );
41 |
42 | /// A bold text style.
43 | static const textStyleBold = TextStyle(
44 | fontWeight: FontWeight.bold,
45 | );
46 |
47 | static const textStyleSmallBold = TextStyle(
48 | fontWeight: FontWeight.bold,
49 | fontSize: 13,
50 | );
51 |
52 | /// A faded text style. Uses [AppColors.faded].
53 | static const textStyleFaded =
54 | TextStyle(color: AppColors.faded, fontWeight: FontWeight.w400);
55 |
56 | /// A faded text style. Uses [AppColors.faded].
57 | static const textStyleFadedSmall = TextStyle(
58 | color: AppColors.faded, fontWeight: FontWeight.w400, fontSize: 11);
59 |
60 | /// A faded text style. Uses [AppColors.faded].
61 | static const textStyleFadedSmallBold = TextStyle(
62 | color: AppColors.faded, fontWeight: FontWeight.w500, fontSize: 11);
63 |
64 | /// Light text style.
65 | static const textStyleLight = TextStyle(fontWeight: FontWeight.w300);
66 |
67 | /// Action text
68 | static const textStyleAction = TextStyle(
69 | fontWeight: FontWeight.w700,
70 | color: AppColors.secondary,
71 | );
72 | }
73 |
74 | /// Global reference to the application theme.
75 | class AppTheme {
76 | final _darkBase = ThemeData.dark();
77 | final _lightBase = ThemeData.light();
78 |
79 | /// Dark theme and its settings.
80 | ThemeData get darkTheme => _darkBase.copyWith(
81 | visualDensity: VisualDensity.adaptivePlatformDensity,
82 | backgroundColor: AppColors.dark,
83 | scaffoldBackgroundColor: AppColors.dark,
84 | appBarTheme: _darkBase.appBarTheme.copyWith(
85 | backgroundColor: AppColors.dark,
86 | foregroundColor: AppColors.light,
87 | iconTheme: const IconThemeData(color: AppColors.light),
88 | elevation: 0,
89 | ),
90 | bottomNavigationBarTheme: _darkBase.bottomNavigationBarTheme.copyWith(
91 | backgroundColor: AppColors.dark,
92 | selectedItemColor: AppColors.light,
93 | ),
94 | outlinedButtonTheme: OutlinedButtonThemeData(
95 | style: ButtonStyle(
96 | side: MaterialStateProperty.all(
97 | const BorderSide(
98 | color: AppColors.grey,
99 | ),
100 | ),
101 | foregroundColor: MaterialStateProperty.all(
102 | AppColors.light,
103 | ),
104 | backgroundColor: MaterialStateProperty.all(
105 | AppColors.dark,
106 | ),
107 | overlayColor: MaterialStateProperty.all(
108 | AppColors.grey,
109 | ),
110 | ),
111 | ),
112 | elevatedButtonTheme: ElevatedButtonThemeData(
113 | style: ButtonStyle(
114 | backgroundColor: MaterialStateProperty.all(
115 | AppColors.secondary,
116 | ),
117 | foregroundColor: MaterialStateProperty.all(
118 | AppColors.primaryText,
119 | ),
120 | overlayColor: MaterialStateProperty.all(
121 | AppColors.grey,
122 | ),
123 | ),
124 | ),
125 | textButtonTheme: TextButtonThemeData(
126 | style: ButtonStyle(
127 | foregroundColor: MaterialStateProperty.all(
128 | AppColors.secondary,
129 | ),
130 | overlayColor: MaterialStateProperty.all(
131 | AppColors.grey,
132 | ),
133 | textStyle: MaterialStateProperty.all(
134 | const TextStyle(
135 | color: AppColors.secondary,
136 | fontSize: 16,
137 | fontWeight: FontWeight.w600,
138 | ),
139 | ),
140 | ),
141 | ),
142 | brightness: Brightness.dark,
143 | colorScheme:
144 | _darkBase.colorScheme.copyWith(secondary: AppColors.secondary),
145 | );
146 |
147 | ThemeData get lightTheme => _lightBase.copyWith(
148 | visualDensity: VisualDensity.adaptivePlatformDensity,
149 | backgroundColor: AppColors.light,
150 | scaffoldBackgroundColor: AppColors.light,
151 | appBarTheme: _lightBase.appBarTheme.copyWith(
152 | backgroundColor: AppColors.light,
153 | foregroundColor: AppColors.dark,
154 | iconTheme: const IconThemeData(color: AppColors.dark),
155 | elevation: 0,
156 | ),
157 | bottomNavigationBarTheme: _lightBase.bottomNavigationBarTheme.copyWith(
158 | backgroundColor: AppColors.light,
159 | selectedItemColor: AppColors.dark,
160 | ),
161 | snackBarTheme:
162 | _lightBase.snackBarTheme.copyWith(backgroundColor: AppColors.dark),
163 | outlinedButtonTheme: OutlinedButtonThemeData(
164 | style: ButtonStyle(
165 | side: MaterialStateProperty.all(
166 | const BorderSide(
167 | color: AppColors.ligthGrey,
168 | ),
169 | ),
170 | foregroundColor: MaterialStateProperty.all(
171 | AppColors.dark,
172 | ),
173 | backgroundColor: MaterialStateProperty.all(
174 | AppColors.light,
175 | ),
176 | overlayColor: MaterialStateProperty.all(
177 | AppColors.ligthGrey,
178 | ),
179 | ),
180 | ),
181 | elevatedButtonTheme: ElevatedButtonThemeData(
182 | style: ButtonStyle(
183 | backgroundColor: MaterialStateProperty.all(
184 | AppColors.secondary,
185 | ),
186 | foregroundColor: MaterialStateProperty.all(
187 | AppColors.primaryText,
188 | ),
189 | overlayColor: MaterialStateProperty.all(
190 | AppColors.ligthGrey,
191 | ),
192 | ),
193 | ),
194 | textButtonTheme: TextButtonThemeData(
195 | style: ButtonStyle(
196 | foregroundColor: MaterialStateProperty.all(
197 | AppColors.secondary,
198 | ),
199 | textStyle: MaterialStateProperty.all(
200 | const TextStyle(
201 | color: AppColors.secondary,
202 | fontSize: 16,
203 | fontWeight: FontWeight.w600,
204 | ),
205 | ),
206 | overlayColor: MaterialStateProperty.all(
207 | AppColors.ligthGrey,
208 | ),
209 | ),
210 | ),
211 | brightness: Brightness.light,
212 | colorScheme:
213 | _lightBase.colorScheme.copyWith(secondary: AppColors.secondary),
214 | );
215 | }
216 |
--------------------------------------------------------------------------------
/lib/app/utils.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'package:provider/provider.dart';
4 |
5 | import 'state/app_state.dart';
6 |
7 | /// Extension method on [BuildContext] to easily perform snackbar operations.
8 | extension Snackbar on BuildContext {
9 | /// Removes the current active [SnackBar], and replaces it with a new snackbar
10 | /// with content of [message].
11 | void removeAndShowSnackbar(final String message) {
12 | ScaffoldMessenger.of(this).removeCurrentSnackBar();
13 | ScaffoldMessenger.of(this).showSnackBar(
14 | SnackBar(content: Text(message)),
15 | );
16 | }
17 | }
18 |
19 | /// Extension method on [BuildContext] to easily retrieve providers.
20 | extension ProviderX on BuildContext {
21 | /// Returns the application [AppState].
22 | AppState get appState => read();
23 | }
24 |
--------------------------------------------------------------------------------
/lib/components/app_widgets/app_widgets.dart:
--------------------------------------------------------------------------------
1 | export 'avatars.dart';
2 | export 'comment_box.dart';
3 | export 'tap_fade_icon.dart';
4 | export 'favorite_icon.dart';
5 |
--------------------------------------------------------------------------------
/lib/components/app_widgets/avatars.dart:
--------------------------------------------------------------------------------
1 | import 'package:cached_network_image/cached_network_image.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import '../../app/app.dart';
5 |
6 | /// An avatar that displays a user's profile picture.
7 | ///
8 | /// Supports different sizes:
9 | /// - `Avatar.tiny`
10 | /// - `Avatar.small`
11 | /// - `Avatar.medium`
12 | /// - `Avatar.big`
13 | /// - `Avatar.huge`
14 | class Avatar extends StatelessWidget {
15 | /// Creates a tiny avatar.
16 | const Avatar.tiny({
17 | Key? key,
18 | required this.streamagramUser,
19 | }) : _avatarSize = _tinyAvatarSize,
20 | _coloredCircle = _tinyColoredCircle,
21 | hasNewStory = false,
22 | fontSize = 12,
23 | isThumbnail = true,
24 | super(key: key);
25 |
26 | /// Creates a small avatar.
27 | const Avatar.small({
28 | Key? key,
29 | required this.streamagramUser,
30 | }) : _avatarSize = _smallAvatarSize,
31 | _coloredCircle = _smallColoredCircle,
32 | hasNewStory = false,
33 | fontSize = 14,
34 | isThumbnail = true,
35 | super(key: key);
36 |
37 | /// Creates a medium avatar.
38 | const Avatar.medium({
39 | Key? key,
40 | this.hasNewStory = false,
41 | required this.streamagramUser,
42 | }) : _avatarSize = _mediumAvatarSize,
43 | _coloredCircle = _mediumColoredCircle,
44 | fontSize = 20,
45 | isThumbnail = true,
46 | super(key: key);
47 |
48 | /// Creates a big avatar.
49 | const Avatar.big({
50 | Key? key,
51 | this.hasNewStory = false,
52 | required this.streamagramUser,
53 | }) : _avatarSize = _largeAvatarSize,
54 | _coloredCircle = _largeColoredCircle,
55 | fontSize = 26,
56 | isThumbnail = false,
57 | super(key: key);
58 |
59 | /// Creates a huge avatar.
60 | const Avatar.huge({
61 | Key? key,
62 | this.hasNewStory = false,
63 | required this.streamagramUser,
64 | }) : _avatarSize = _hugeAvatarSize,
65 | _coloredCircle = _hugeColoredCircle,
66 | fontSize = 30,
67 | isThumbnail = false,
68 | super(key: key);
69 |
70 | /// Indicates if the user has a new story. If yes, their avatar is surrounded
71 | /// with an indicator.
72 | final bool hasNewStory;
73 |
74 | /// The user data to show for the avatar.
75 | final StreamagramUser streamagramUser;
76 |
77 | /// Text size of the user's initials when there is no profile photo.
78 | final double fontSize;
79 |
80 | final double _avatarSize;
81 | final double _coloredCircle;
82 |
83 | // Small avatar configuration
84 | static const _tinyAvatarSize = 22.0;
85 | static const _tinyPaddedCircle = _tinyAvatarSize + 2;
86 | static const _tinyColoredCircle = _tinyPaddedCircle * 2 + 4;
87 |
88 | // Small avatar configuration
89 | static const _smallAvatarSize = 30.0;
90 | static const _smallPaddedCircle = _smallAvatarSize + 2;
91 | static const _smallColoredCircle = _smallPaddedCircle * 2 + 4;
92 |
93 | // Medium avatar configuration
94 | static const _mediumAvatarSize = 40.0;
95 | static const _mediumPaddedCircle = _mediumAvatarSize + 2;
96 | static const _mediumColoredCircle = _mediumPaddedCircle * 2 + 4;
97 |
98 | // Large avatar configuration
99 | static const _largeAvatarSize = 90.0;
100 | static const _largPaddedCircle = _largeAvatarSize + 2;
101 | static const _largeColoredCircle = _largPaddedCircle * 2 + 4;
102 |
103 | // Huge avatar configuration
104 | static const _hugeAvatarSize = 120.0;
105 | static const _hugePaddedCircle = _hugeAvatarSize + 2;
106 | static const _hugeColoredCircle = _hugePaddedCircle * 2 + 4;
107 |
108 | /// Whether this avatar uses a thumbnail as an image (low quality).
109 | final bool isThumbnail;
110 |
111 | @override
112 | Widget build(BuildContext context) {
113 | final picture = _CircularProfilePicture(
114 | size: _avatarSize,
115 | userData: streamagramUser,
116 | fontSize: fontSize,
117 | isThumbnail: isThumbnail,
118 | );
119 |
120 | if (!hasNewStory) {
121 | return picture;
122 | }
123 | return Container(
124 | width: _coloredCircle,
125 | height: _coloredCircle,
126 | decoration: const BoxDecoration(
127 | color: Colors.red,
128 | shape: BoxShape.circle,
129 | ),
130 | child: Center(child: picture),
131 | );
132 | }
133 | }
134 |
135 | class _CircularProfilePicture extends StatelessWidget {
136 | const _CircularProfilePicture({
137 | Key? key,
138 | required this.size,
139 | required this.userData,
140 | required this.fontSize,
141 | this.isThumbnail = false,
142 | }) : super(key: key);
143 |
144 | final StreamagramUser userData;
145 |
146 | final double size;
147 | final double fontSize;
148 |
149 | final bool isThumbnail;
150 |
151 | @override
152 | Widget build(BuildContext context) {
153 | final profilePhoto = isThumbnail
154 | ? userData.profilePhotoThumbnail
155 | : userData.profilePhotoResized;
156 |
157 | return (profilePhoto == null)
158 | ? Container(
159 | width: size,
160 | height: size,
161 | decoration: const BoxDecoration(
162 | color: AppColors.secondary,
163 | shape: BoxShape.circle,
164 | ),
165 | child: Center(
166 | child: Text(
167 | '${userData.firstName[0]}${userData.lastName[0]}',
168 | style: TextStyle(fontSize: fontSize),
169 | ),
170 | ),
171 | )
172 | : SizedBox(
173 | width: size,
174 | height: size,
175 | child: CachedNetworkImage(
176 | imageUrl: profilePhoto,
177 | fit: BoxFit.contain,
178 | imageBuilder: (context, imageProvider) => Container(
179 | width: size,
180 | height: size,
181 | decoration: BoxDecoration(
182 | shape: BoxShape.circle,
183 | image:
184 | DecorationImage(image: imageProvider, fit: BoxFit.cover),
185 | ),
186 | ),
187 | ),
188 | );
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/lib/components/app_widgets/comment_box.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../../app/app.dart';
4 | import 'app_widgets.dart';
5 |
6 | /// Displays a text field styled to easily add comments to posts.
7 | ///
8 | /// Quickly add emoji reactions.
9 | class CommentBox extends StatelessWidget {
10 | /// Creates a [CommentBox].
11 | const CommentBox({
12 | Key? key,
13 | required this.commenter,
14 | required this.textEditingController,
15 | required this.focusNode,
16 | required this.onSubmitted,
17 | }) : super(key: key);
18 |
19 | final StreamagramUser commenter;
20 | final TextEditingController textEditingController;
21 | final FocusNode focusNode;
22 | final Function(String?) onSubmitted;
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | final border = _border(context);
27 | return Container(
28 | decoration: BoxDecoration(
29 | color: (Theme.of(context).brightness == Brightness.light)
30 | ? AppColors.light
31 | : AppColors.dark,
32 | border: Border(
33 | top: BorderSide(
34 | color: (Theme.of(context).brightness == Brightness.light)
35 | ? AppColors.ligthGrey
36 | : AppColors.grey,
37 | )),
38 | ),
39 | child: Column(
40 | mainAxisSize: MainAxisSize.min,
41 | children: [
42 | Padding(
43 | padding: const EdgeInsets.symmetric(vertical: 8.0),
44 | child: Row(
45 | mainAxisAlignment: MainAxisAlignment.spaceAround,
46 | children: [
47 | _emojiText('❤️'),
48 | _emojiText('🙌'),
49 | _emojiText('🔥'),
50 | _emojiText('👏🏻'),
51 | _emojiText('😢'),
52 | _emojiText('😍'),
53 | _emojiText('😮'),
54 | _emojiText('😂'),
55 | ],
56 | ),
57 | ),
58 | Row(
59 | children: [
60 | Padding(
61 | padding: const EdgeInsets.all(8.0),
62 | child: Avatar.medium(streamagramUser: commenter),
63 | ),
64 | Expanded(
65 | child: TextField(
66 | controller: textEditingController,
67 | focusNode: focusNode,
68 | onSubmitted: onSubmitted,
69 | minLines: 1,
70 | maxLines: 10,
71 | style: const TextStyle(fontSize: 14),
72 | decoration: InputDecoration(
73 | suffix: _DoneButton(
74 | textEditorFocusNode: focusNode,
75 | textEditingController: textEditingController,
76 | onSubmitted: onSubmitted,
77 | ),
78 | hintText: 'Add a comment...',
79 | isDense: true,
80 | contentPadding: const EdgeInsets.symmetric(
81 | horizontal: 16, vertical: 12),
82 | focusedBorder: border,
83 | border: border,
84 | enabledBorder: border),
85 | ),
86 | ),
87 | const SizedBox(
88 | width: 8,
89 | ),
90 | ],
91 | ),
92 | ],
93 | ),
94 | );
95 | }
96 |
97 | OutlineInputBorder _border(BuildContext context) {
98 | return OutlineInputBorder(
99 | borderRadius: const BorderRadius.all(Radius.circular(24)),
100 | borderSide: BorderSide(
101 | color: (Theme.of(context).brightness == Brightness.light)
102 | ? AppColors.grey.withOpacity(0.3)
103 | : AppColors.light.withOpacity(0.5),
104 | width: 0.5,
105 | ),
106 | );
107 | }
108 |
109 | Widget _emojiText(String emoji) {
110 | return GestureDetector(
111 | onTap: () {
112 | focusNode.requestFocus();
113 | textEditingController.text = textEditingController.text + emoji;
114 | textEditingController.selection = TextSelection.fromPosition(
115 | TextPosition(offset: textEditingController.text.length));
116 | },
117 | child: Text(
118 | emoji,
119 | style: const TextStyle(fontSize: 24),
120 | ),
121 | );
122 | }
123 | }
124 |
125 | class _DoneButton extends StatefulWidget {
126 | const _DoneButton({
127 | Key? key,
128 | required this.onSubmitted,
129 | required this.textEditorFocusNode,
130 | required this.textEditingController,
131 | }) : super(key: key);
132 |
133 | final Function(String?) onSubmitted;
134 | final FocusNode textEditorFocusNode;
135 | final TextEditingController textEditingController;
136 |
137 | @override
138 | State<_DoneButton> createState() => _DoneButtonState();
139 | }
140 |
141 | class _DoneButtonState extends State<_DoneButton> {
142 | final fadedTextStyle =
143 | AppTextStyle.textStyleAction.copyWith(color: Colors.grey);
144 | late TextStyle textStyle = fadedTextStyle;
145 |
146 | @override
147 | void initState() {
148 | super.initState();
149 | widget.textEditingController.addListener(() {
150 | if (widget.textEditingController.text.isNotEmpty) {
151 | textStyle = AppTextStyle.textStyleAction;
152 | } else {
153 | textStyle = fadedTextStyle;
154 | }
155 | if (mounted) {
156 | setState(() {});
157 | }
158 | });
159 | }
160 |
161 | @override
162 | Widget build(BuildContext context) {
163 | return widget.textEditorFocusNode.hasFocus
164 | ? GestureDetector(
165 | onTap: () {
166 | widget.onSubmitted(widget.textEditingController.text);
167 | },
168 | child: Text(
169 | 'Done',
170 | style: textStyle,
171 | ),
172 | )
173 | : const SizedBox.shrink();
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/lib/components/app_widgets/favorite_icon.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:stream_agram/app/theme.dart';
3 |
4 | /// {@template favorite_icon_button}
5 | /// Animated button to indicate if a post/comment is liked.
6 | ///
7 | /// Pass in onPressed to
8 | /// {@endtemplate}
9 | class FavoriteIconButton extends StatefulWidget {
10 | /// {@macro favorite_icon_button}
11 | const FavoriteIconButton({
12 | Key? key,
13 | required this.isLiked,
14 | this.size = 22,
15 | required this.onTap,
16 | }) : super(key: key);
17 |
18 | /// Indicates if it is liked or not.
19 | final bool isLiked;
20 |
21 | /// Size of the icon.
22 | final double size;
23 |
24 | /// onTap callback. Returns a value to indicate if liked or not.
25 | final Function(bool val) onTap;
26 |
27 | @override
28 | State createState() => _FavoriteIconButtonState();
29 | }
30 |
31 | class _FavoriteIconButtonState extends State {
32 | late bool isLiked = widget.isLiked;
33 |
34 | void _handleTap() {
35 | setState(() {
36 | isLiked = !isLiked;
37 | });
38 | widget.onTap(isLiked);
39 | }
40 |
41 | @override
42 | Widget build(BuildContext context) {
43 | return GestureDetector(
44 | onTap: _handleTap,
45 | child: AnimatedCrossFade(
46 | firstCurve: Curves.easeIn,
47 | secondCurve: Curves.easeOut,
48 | firstChild: Icon(
49 | Icons.favorite,
50 | color: AppColors.like,
51 | size: widget.size,
52 | ),
53 | secondChild: Icon(
54 | Icons.favorite_outline,
55 | size: widget.size,
56 | ),
57 | crossFadeState:
58 | isLiked ? CrossFadeState.showFirst : CrossFadeState.showSecond,
59 | duration: const Duration(milliseconds: 200),
60 | ),
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/components/app_widgets/tap_fade_icon.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | /// {@template tap_fade_icon}
4 | /// A tappable icon that fades colors when tapped and held.
5 | /// {@endtemplate}
6 | class TapFadeIcon extends StatefulWidget {
7 | /// {@macro tap_fade_icon}
8 | const TapFadeIcon({
9 | Key? key,
10 | required this.onTap,
11 | required this.icon,
12 | required this.iconColor,
13 | this.size = 22,
14 | }) : super(key: key);
15 |
16 | /// Callback to handle tap.
17 | final VoidCallback onTap;
18 |
19 | /// Color of the icon.
20 | final Color iconColor;
21 |
22 | /// Type of icon.
23 | final IconData icon;
24 |
25 | /// Icon size.
26 | final double size;
27 |
28 | @override
29 | State createState() => _TapFadeIconState();
30 | }
31 |
32 | class _TapFadeIconState extends State {
33 | late Color color = widget.iconColor;
34 |
35 | void handleTapDown(TapDownDetails _) {
36 | setState(() {
37 | color = widget.iconColor.withOpacity(0.7);
38 | });
39 | }
40 |
41 | void handleTapUp(TapUpDetails _) {
42 | setState(() {
43 | color = widget.iconColor;
44 | });
45 |
46 | widget.onTap(); // Execute callback.
47 | }
48 |
49 | @override
50 | void didUpdateWidget(covariant TapFadeIcon oldWidget) {
51 | super.didUpdateWidget(oldWidget);
52 | if (oldWidget.iconColor != widget.iconColor) {
53 | color = widget.iconColor;
54 | }
55 | }
56 |
57 | @override
58 | Widget build(BuildContext context) {
59 | return GestureDetector(
60 | onTapDown: handleTapDown,
61 | onTapUp: handleTapUp,
62 | child: Icon(
63 | widget.icon,
64 | color: color,
65 | size: widget.size,
66 | ),
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/components/comments/comments.dart:
--------------------------------------------------------------------------------
1 | export 'state/state.dart';
2 |
--------------------------------------------------------------------------------
/lib/components/comments/comments_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:jiffy/jiffy.dart';
3 | import 'package:provider/provider.dart';
4 | import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
5 |
6 | import '../../app/app.dart';
7 | import '../app_widgets/app_widgets.dart';
8 | import 'state/state.dart';
9 |
10 | /// Screen that shows all comments for a given post.
11 | class CommentsScreen extends StatefulWidget {
12 | /// Creates a new [CommentsScreen].
13 | const CommentsScreen({
14 | Key? key,
15 | required this.enrichedActivity,
16 | required this.activityOwnerData,
17 | }) : super(key: key);
18 |
19 | final EnrichedActivity enrichedActivity;
20 |
21 | /// Owner / [User] of the activity.
22 | final StreamagramUser activityOwnerData;
23 |
24 | /// MaterialPageRoute to this screen.
25 | static Route route({
26 | required EnrichedActivity enrichedActivity,
27 | required StreamagramUser activityOwnerData,
28 | }) =>
29 | MaterialPageRoute(
30 | builder: (context) => CommentsScreen(
31 | enrichedActivity: enrichedActivity,
32 | activityOwnerData: activityOwnerData,
33 | ),
34 | );
35 |
36 | @override
37 | State createState() => _CommentsScreenState();
38 | }
39 |
40 | class _CommentsScreenState extends State {
41 | late FocusNode commentFocusNode;
42 | late CommentState commentState;
43 |
44 | @override
45 | void initState() {
46 | super.initState();
47 | commentFocusNode = FocusNode();
48 | commentState = CommentState(
49 | activityId: widget.enrichedActivity.id!,
50 | activityOwnerData: widget.activityOwnerData,
51 | );
52 | }
53 |
54 | @override
55 | void dispose() {
56 | commentFocusNode.dispose();
57 | super.dispose();
58 | }
59 |
60 | @override
61 | Widget build(BuildContext context) {
62 | return MultiProvider(
63 | providers: [
64 | ChangeNotifierProvider.value(value: commentState),
65 | ChangeNotifierProvider.value(value: commentFocusNode),
66 | ],
67 | child: GestureDetector(
68 | onTap: () {
69 | commentState.resetCommentFocus();
70 | FocusScope.of(context).unfocus();
71 | },
72 | child: Scaffold(
73 | appBar: AppBar(
74 | title: const Text('Comments',
75 | style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
76 | elevation: 0.5,
77 | shadowColor: Colors.white,
78 | ),
79 | body: Stack(
80 | children: [
81 | _CommentsList(
82 | activityId: widget.enrichedActivity.id!,
83 | ),
84 | _CommentBox(
85 | enrichedActivity: widget.enrichedActivity,
86 | ),
87 | ],
88 | ),
89 | ),
90 | ),
91 | );
92 | }
93 | }
94 |
95 | class _CommentsList extends StatelessWidget {
96 | const _CommentsList({
97 | Key? key,
98 | required this.activityId,
99 | }) : super(key: key);
100 |
101 | final String activityId;
102 |
103 | @override
104 | Widget build(BuildContext context) {
105 | return ReactionListCore(
106 | lookupValue: activityId,
107 | kind: 'comment',
108 | loadingBuilder: (context) =>
109 | const Center(child: CircularProgressIndicator()),
110 | errorBuilder: (context, error) =>
111 | const Center(child: Text('Could not load comments.')),
112 | emptyBuilder: (context) =>
113 | const Center(child: Text('Be the first to add a comment.')),
114 | reactionsBuilder: (context, reactions) {
115 | return ListView.builder(
116 | itemCount: reactions.length + 1,
117 | itemBuilder: (context, index) {
118 | if (index == reactions.length) {
119 | // Bottom padding to ensure [CommentBox] does not obscure
120 | // visibility
121 | return const SizedBox(
122 | height: 120,
123 | );
124 | }
125 | return Padding(
126 | padding: const EdgeInsets.symmetric(vertical: 8.0),
127 | child: _CommentTile(
128 | key: ValueKey('comment-${reactions[index].id}'),
129 | reaction: reactions[index],
130 | ),
131 | );
132 | },
133 | );
134 | },
135 | flags: EnrichmentFlags()
136 | ..withOwnChildren()
137 | ..withOwnReactions()
138 | ..withRecentReactions(),
139 | );
140 | }
141 | }
142 |
143 | class _CommentBox extends StatefulWidget {
144 | const _CommentBox({
145 | Key? key,
146 | required this.enrichedActivity,
147 | }) : super(key: key);
148 |
149 | final EnrichedActivity enrichedActivity;
150 |
151 | @override
152 | __CommentBoxState createState() => __CommentBoxState();
153 | }
154 |
155 | class __CommentBoxState extends State<_CommentBox> {
156 | late final _commentTextController = TextEditingController();
157 |
158 | Future handleSubmit(String? value) async {
159 | if (value != null && value.isNotEmpty) {
160 | _commentTextController.clear();
161 | FocusScope.of(context).unfocus();
162 |
163 | final commentState = context.read();
164 | final commentFocus = commentState.commentFocus;
165 |
166 | if (commentFocus.typeOfComment == TypeOfComment.activityComment) {
167 | await FeedProvider.of(context).bloc.onAddReaction(
168 | kind: 'comment',
169 | activity: widget.enrichedActivity,
170 | feedGroup: 'timeline',
171 | data: {'message': value},
172 | );
173 | } else if (commentFocus.typeOfComment == TypeOfComment.reactionComment) {
174 | if (commentFocus.reaction != null) {
175 | await FeedProvider.of(context).bloc.onAddChildReaction(
176 | kind: 'comment',
177 | reaction: commentFocus.reaction!,
178 | lookupValue: widget.enrichedActivity.id!,
179 | data: {'message': value},
180 | );
181 | }
182 | }
183 | }
184 | }
185 |
186 | @override
187 | void dispose() {
188 | _commentTextController.dispose();
189 | super.dispose();
190 | }
191 |
192 | @override
193 | Widget build(BuildContext context) {
194 | final commentFocus =
195 | context.select((CommentState state) => state.commentFocus);
196 |
197 | final focusNode = context.watch();
198 |
199 | return Align(
200 | alignment: Alignment.bottomCenter,
201 | child: Container(
202 | color: (Theme.of(context).brightness == Brightness.light)
203 | ? AppColors.light
204 | : AppColors.dark,
205 | child: Column(
206 | mainAxisSize: MainAxisSize.min,
207 | children: [
208 | AnimatedSwitcher(
209 | duration: const Duration(milliseconds: 200),
210 | transitionBuilder: (child, animation) {
211 | final tween =
212 | Tween(begin: const Offset(0.0, 1.0), end: Offset.zero)
213 | .chain(CurveTween(curve: Curves.easeOutQuint));
214 | final offsetAnimation = animation.drive(tween);
215 | return SlideTransition(
216 | position: offsetAnimation,
217 | child: child,
218 | );
219 | },
220 | child:
221 | (commentFocus.typeOfComment == TypeOfComment.reactionComment)
222 | ? _replyToBox(commentFocus, context)
223 | : const SizedBox.shrink(),
224 | ),
225 | CommentBox(
226 | commenter: context.appState.streamagramUser!,
227 | textEditingController: _commentTextController,
228 | onSubmitted: handleSubmit,
229 | focusNode: focusNode,
230 | ),
231 | SizedBox(
232 | height: MediaQuery.of(context).padding.bottom,
233 | )
234 | ],
235 | ),
236 | ),
237 | );
238 | }
239 |
240 | Container _replyToBox(CommentFocus commentFocus, BuildContext context) {
241 | return Container(
242 | color: (Theme.of(context).brightness == Brightness.dark)
243 | ? AppColors.grey
244 | : AppColors.ligthGrey,
245 | child: Padding(
246 | padding: const EdgeInsets.all(16.0),
247 | child: Row(
248 | children: [
249 | Text(
250 | 'Replying to ${commentFocus.user.fullName}',
251 | style: AppTextStyle.textStyleFaded,
252 | ),
253 | const Spacer(),
254 | TapFadeIcon(
255 | onTap: () {
256 | context.read().resetCommentFocus();
257 | },
258 | icon: Icons.close,
259 | size: 16,
260 | iconColor: Theme.of(context).iconTheme.color!,
261 | ),
262 | ],
263 | ),
264 | ),
265 | );
266 | }
267 | }
268 |
269 | class _CommentTile extends StatefulWidget {
270 | const _CommentTile({
271 | Key? key,
272 | required this.reaction,
273 | this.canReply = true,
274 | this.isReplyToComment = false,
275 | }) : super(key: key);
276 |
277 | final Reaction reaction;
278 | final bool canReply;
279 | final bool isReplyToComment;
280 | @override
281 | __CommentTileState createState() => __CommentTileState();
282 | }
283 |
284 | class __CommentTileState extends State<_CommentTile> {
285 | late final userData = StreamagramUser.fromMap(widget.reaction.user!.data!);
286 | late final message = extractMessage;
287 |
288 | late final timeSince = _timeSinceComment();
289 |
290 | late int numberOfLikes = widget.reaction.childrenCounts?['like'] ?? 0;
291 |
292 | late bool isLiked = _isFavorited();
293 | Reaction? likeReaction;
294 |
295 | String _timeSinceComment() {
296 | final jiffyTime = Jiffy(widget.reaction.createdAt).fromNow();
297 | if (jiffyTime == 'a few seconds ago') {
298 | return 'just now';
299 | } else {
300 | return jiffyTime;
301 | }
302 | }
303 |
304 | String numberOfLikesMessage(int count) {
305 | if (count == 0) {
306 | return '';
307 | }
308 | if (count == 1) {
309 | return '1 like';
310 | } else {
311 | return '$count likes';
312 | }
313 | }
314 |
315 | String get extractMessage {
316 | final data = widget.reaction.data;
317 | if (data != null && data['message'] != null) {
318 | return data['message'] as String;
319 | } else {
320 | return '';
321 | }
322 | }
323 |
324 | bool _isFavorited() {
325 | likeReaction = widget.reaction.ownChildren?['like']?.first;
326 | return likeReaction != null;
327 | }
328 |
329 | Future _handleFavorite(bool liked) async {
330 | if (isLiked && likeReaction != null) {
331 | await context.appState.client.reactions.delete(likeReaction!.id!);
332 | numberOfLikes--;
333 | } else {
334 | likeReaction = await context.appState.client.reactions.addChild(
335 | 'like',
336 | widget.reaction.id!,
337 | userId: context.appState.user.id,
338 | );
339 | numberOfLikes++;
340 | }
341 | setState(() {
342 | isLiked = liked;
343 | });
344 | }
345 |
346 | @override
347 | Widget build(BuildContext context) {
348 | return Column(
349 | mainAxisSize: MainAxisSize.min,
350 | crossAxisAlignment: CrossAxisAlignment.start,
351 | children: [
352 | Row(
353 | crossAxisAlignment: CrossAxisAlignment.start,
354 | children: [
355 | Padding(
356 | padding: const EdgeInsets.symmetric(horizontal: 8.0),
357 | child: (widget.isReplyToComment)
358 | ? Avatar.tiny(streamagramUser: userData)
359 | : Avatar.small(streamagramUser: userData),
360 | ),
361 | Expanded(
362 | child: Column(
363 | crossAxisAlignment: CrossAxisAlignment.start,
364 | mainAxisSize: MainAxisSize.min,
365 | children: [
366 | Row(
367 | children: [
368 | Expanded(
369 | child: Text.rich(
370 | TextSpan(
371 | children: [
372 | TextSpan(
373 | text: userData.fullName,
374 | style: AppTextStyle.textStyleSmallBold),
375 | const TextSpan(text: ' '),
376 | TextSpan(
377 | text: message,
378 | style: const TextStyle(fontSize: 13),
379 | ),
380 | ],
381 | ),
382 | ),
383 | ),
384 | Padding(
385 | padding: const EdgeInsets.symmetric(horizontal: 8.0),
386 | child: Center(
387 | child: FavoriteIconButton(
388 | isLiked: isLiked,
389 | size: 14,
390 | onTap: _handleFavorite,
391 | ),
392 | ),
393 | )
394 | ],
395 | ),
396 | Padding(
397 | padding: const EdgeInsets.only(top: 4.0),
398 | child: Row(
399 | mainAxisAlignment: MainAxisAlignment.start,
400 | children: [
401 | SizedBox(
402 | width: 80,
403 | child: Text(
404 | timeSince,
405 | style: AppTextStyle.textStyleFadedSmall,
406 | ),
407 | ),
408 | Visibility(
409 | visible: numberOfLikes > 0,
410 | child: SizedBox(
411 | width: 60,
412 | child: Text(
413 | numberOfLikesMessage(numberOfLikes),
414 | style: AppTextStyle.textStyleFadedSmall,
415 | ),
416 | ),
417 | ),
418 | Visibility(
419 | visible: widget.canReply,
420 | child: GestureDetector(
421 | onTap: () {
422 | context.read().setCommentFocus(
423 | CommentFocus(
424 | typeOfComment:
425 | TypeOfComment.reactionComment,
426 | id: widget.reaction.id!,
427 | user: StreamagramUser.fromMap(
428 | widget.reaction.user!.data!),
429 | reaction: widget.reaction,
430 | ),
431 | );
432 |
433 | FocusScope.of(context)
434 | .requestFocus(context.read());
435 | },
436 | child: const SizedBox(
437 | width: 50,
438 | child: Text(
439 | 'Reply',
440 | style: AppTextStyle.textStyleFadedSmallBold,
441 | ),
442 | ),
443 | ),
444 | )
445 | ],
446 | ),
447 | ),
448 | ],
449 | ),
450 | ),
451 | ],
452 | ),
453 | Padding(
454 | padding: const EdgeInsets.only(left: 34.0),
455 | child: _ChildCommentList(
456 | comments: widget.reaction.latestChildren?['comment']),
457 | ),
458 | ],
459 | );
460 | }
461 | }
462 |
463 | class _ChildCommentList extends StatelessWidget {
464 | const _ChildCommentList({
465 | Key? key,
466 | required this.comments,
467 | }) : super(key: key);
468 |
469 | final List? comments;
470 |
471 | @override
472 | Widget build(BuildContext context) {
473 | return Column(
474 | mainAxisSize: MainAxisSize.min,
475 | children: comments
476 | ?.map(
477 | (reaction) => Padding(
478 | padding: const EdgeInsets.only(top: 8.0),
479 | child: _CommentTile(
480 | key: ValueKey('comment-tile-${reaction.id}'),
481 | reaction: reaction,
482 | canReply: false,
483 | isReplyToComment: true,
484 | ),
485 | ),
486 | )
487 | .toList() ??
488 | [],
489 | );
490 | }
491 | }
492 |
--------------------------------------------------------------------------------
/lib/components/comments/state/comment_state.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
3 |
4 | import '../../../app/state/models/models.dart';
5 |
6 | /// Indicates the type of comment that was made.
7 | /// Can be:
8 | /// - Activity comment
9 | /// - Reaction comment
10 | enum TypeOfComment {
11 | /// Comment on an activity
12 | activityComment,
13 |
14 | /// Comment on a reaction
15 | reactionComment,
16 | }
17 |
18 | /// {@template comment_focus}
19 | /// Information on the type of comment to make. This can be a comment on an
20 | /// activity, or a comment on a reaction.
21 | ///
22 | /// It also indicates the parent user on whom the comment is made.
23 | /// {@endtemplate}
24 | class CommentFocus {
25 | /// {@macro comment_focus}
26 | const CommentFocus({
27 | required this.typeOfComment,
28 | required this.id,
29 | required this.user,
30 | this.reaction,
31 | });
32 |
33 | final Reaction? reaction;
34 |
35 | /// Indicates the type of comment. See [TypeOfComment].
36 | final TypeOfComment typeOfComment;
37 |
38 | /// Activity or reaction id on which the comment is made.
39 | final String id;
40 |
41 | /// The user data of the parent activity or reaction.
42 | final StreamagramUser user;
43 | }
44 |
45 | /// {@template comment_state}
46 | /// ChangeNotifier to facilitate posting comments to activities and reactions.
47 | /// {@endtemplate}
48 | class CommentState extends ChangeNotifier {
49 | /// {@macro comment_state}
50 | CommentState({
51 | required this.activityId,
52 | required this.activityOwnerData,
53 | });
54 |
55 | /// The id for this activity.
56 | final String activityId;
57 |
58 | /// UserData of whoever owns the activity.
59 | final StreamagramUser activityOwnerData;
60 |
61 | /// The type of commentFocus that is currently selected.
62 |
63 | late CommentFocus commentFocus = CommentFocus(
64 | typeOfComment: TypeOfComment.activityComment,
65 | id: activityId,
66 | user: activityOwnerData,
67 | );
68 |
69 | /// Sets the focus to which a comment will be posted to.
70 | ///
71 | /// See [postComment].
72 | void setCommentFocus(CommentFocus focus) {
73 | commentFocus = focus;
74 | notifyListeners();
75 | }
76 |
77 | /// Resets the comment focus to the parent activity.
78 | void resetCommentFocus() {
79 | commentFocus = CommentFocus(
80 | typeOfComment: TypeOfComment.activityComment,
81 | id: activityId,
82 | user: activityOwnerData,
83 | );
84 | notifyListeners();
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/lib/components/comments/state/state.dart:
--------------------------------------------------------------------------------
1 | export 'comment_state.dart';
2 |
--------------------------------------------------------------------------------
/lib/components/home/home.dart:
--------------------------------------------------------------------------------
1 | export 'home_screen.dart';
2 |
--------------------------------------------------------------------------------
/lib/components/home/home_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:google_fonts/google_fonts.dart';
4 | import 'package:stream_agram/components/profile/profile.dart';
5 | import 'package:stream_agram/components/search/search_page.dart';
6 | import 'package:stream_agram/components/timeline/timeline_page.dart';
7 |
8 | import '../../app/app.dart';
9 | import '../app_widgets/app_widgets.dart';
10 | import '../new_post/new_post.dart';
11 |
12 | /// HomeScreen of the application.
13 | ///
14 | /// Provides Navigation to various pages in the application and maintains their
15 | /// state.
16 | ///
17 | /// Default first page is [TimelinePage].
18 | class HomeScreen extends StatefulWidget {
19 | /// Creates a new [HomeScreen]
20 | const HomeScreen({Key? key}) : super(key: key);
21 |
22 | /// List of pages available from the home screen.
23 | static const List _homePages = [
24 | _KeepAlivePage(child: TimelinePage()),
25 | _KeepAlivePage(child: SearchPage()),
26 | _KeepAlivePage(child: ProfilePage())
27 | ];
28 |
29 | @override
30 | State createState() => _HomeScreenState();
31 | }
32 |
33 | class _HomeScreenState extends State {
34 | final PageController pageController = PageController();
35 |
36 | @override
37 | Widget build(BuildContext context) {
38 | final iconColor = Theme.of(context).appBarTheme.iconTheme!.color!;
39 | return Scaffold(
40 | appBar: AppBar(
41 | title:
42 | Text('Stream-agram', style: GoogleFonts.grandHotel(fontSize: 32)),
43 | elevation: 0,
44 | centerTitle: false,
45 | actions: [
46 | Padding(
47 | padding: const EdgeInsets.all(8),
48 | child: TapFadeIcon(
49 | onTap: () => Navigator.of(context).push(NewPostScreen.route),
50 | icon: Icons.add_circle_outline,
51 | iconColor: iconColor,
52 | ),
53 | ),
54 | Padding(
55 | padding: const EdgeInsets.all(8),
56 | child: TapFadeIcon(
57 | onTap: () async {
58 | context.removeAndShowSnackbar('Not part of the demo');
59 | },
60 | icon: Icons.favorite_outline,
61 | iconColor: iconColor,
62 | ),
63 | ),
64 | Padding(
65 | padding: const EdgeInsets.all(8.0),
66 | child: TapFadeIcon(
67 | onTap: () =>
68 | context.removeAndShowSnackbar('Not part of the demo'),
69 | icon: Icons.call_made,
70 | iconColor: iconColor,
71 | ),
72 | ),
73 | ],
74 | ),
75 | body: PageView(
76 | controller: pageController,
77 | physics: const NeverScrollableScrollPhysics(),
78 | children: HomeScreen._homePages,
79 | ),
80 | bottomNavigationBar: _StreamagramBottomNavBar(
81 | pageController: pageController,
82 | ),
83 | );
84 | }
85 | }
86 |
87 | class _StreamagramBottomNavBar extends StatefulWidget {
88 | const _StreamagramBottomNavBar({
89 | Key? key,
90 | required this.pageController,
91 | }) : super(key: key);
92 |
93 | final PageController pageController;
94 |
95 | @override
96 | State<_StreamagramBottomNavBar> createState() =>
97 | _StreamagramBottomNavBarState();
98 | }
99 |
100 | class _StreamagramBottomNavBarState extends State<_StreamagramBottomNavBar> {
101 | void _onNavigationItemTapped(int index) {
102 | widget.pageController.jumpToPage(index);
103 | }
104 |
105 | @override
106 | void initState() {
107 | super.initState();
108 | widget.pageController.addListener(() {
109 | setState(() {});
110 | });
111 | }
112 |
113 | @override
114 | Widget build(BuildContext context) {
115 | return Container(
116 | decoration: BoxDecoration(
117 | boxShadow: [
118 | BoxShadow(
119 | color: (Theme.of(context).brightness == Brightness.dark)
120 | ? AppColors.ligthGrey.withOpacity(0.5)
121 | : AppColors.faded.withOpacity(0.5),
122 | blurRadius: 1,
123 | ),
124 | ],
125 | ),
126 | child: BottomNavigationBar(
127 | onTap: _onNavigationItemTapped,
128 | showSelectedLabels: false,
129 | showUnselectedLabels: false,
130 | elevation: 0,
131 | iconSize: 28,
132 | currentIndex: widget.pageController.page?.toInt() ?? 0,
133 | items: const [
134 | BottomNavigationBarItem(
135 | icon: Icon(Icons.home_outlined),
136 | activeIcon: Icon(Icons.home),
137 | label: 'Home',
138 | ),
139 | BottomNavigationBarItem(
140 | icon: Icon(CupertinoIcons.search),
141 | activeIcon: Icon(
142 | Icons.search,
143 | size: 22,
144 | ),
145 | label: 'Search',
146 | ),
147 | BottomNavigationBarItem(
148 | icon: Icon(Icons.person_outline),
149 | activeIcon: Icon(Icons.person),
150 | label: 'Person',
151 | )
152 | ],
153 | ),
154 | );
155 | }
156 | }
157 |
158 | class _KeepAlivePage extends StatefulWidget {
159 | const _KeepAlivePage({
160 | Key? key,
161 | required this.child,
162 | }) : super(key: key);
163 |
164 | final Widget child;
165 |
166 | @override
167 | _KeepAlivePageState createState() => _KeepAlivePageState();
168 | }
169 |
170 | class _KeepAlivePageState extends State<_KeepAlivePage>
171 | with AutomaticKeepAliveClientMixin {
172 | @override
173 | Widget build(BuildContext context) {
174 | super.build(context);
175 |
176 | return widget.child;
177 | }
178 |
179 | @override
180 | bool get wantKeepAlive => true;
181 | }
182 |
--------------------------------------------------------------------------------
/lib/components/login/login.dart:
--------------------------------------------------------------------------------
1 | export 'login_screen.dart';
2 |
--------------------------------------------------------------------------------
/lib/components/login/login_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../../app/app.dart';
4 | import '../home/home.dart';
5 |
6 | /// {@template login_screen}
7 | /// Screen that presents an option of users to authenticate as.
8 | /// {@endtemplate}
9 | class LoginScreen extends StatefulWidget {
10 | /// {@macro login_screen}
11 | const LoginScreen({Key? key}) : super(key: key);
12 |
13 | @override
14 | State createState() => _LoginScreenState();
15 | }
16 |
17 | class _LoginScreenState extends State {
18 | @override
19 | Widget build(BuildContext context) {
20 | final size = MediaQuery.of(context).size;
21 | return Scaffold(
22 | appBar: AppBar(title: const Text('Demo users')),
23 | body: Padding(
24 | padding: const EdgeInsets.symmetric(horizontal: 24.0),
25 | child: SizedBox(
26 | width: size.width,
27 | height: size.height,
28 | child: SingleChildScrollView(
29 | child: Column(
30 | mainAxisAlignment: MainAxisAlignment.center,
31 | children: [
32 | const SizedBox(height: 42),
33 | for (final user in DemoAppUser.values)
34 | Padding(
35 | padding: const EdgeInsets.all(16.0),
36 | child: ElevatedButton(
37 | style: ButtonStyle(
38 | backgroundColor: MaterialStateColor.resolveWith(
39 | (states) => Colors.white),
40 | padding: MaterialStateProperty.all(
41 | const EdgeInsets.symmetric(horizontal: 4.0),
42 | ),
43 | shape: MaterialStateProperty.all(
44 | RoundedRectangleBorder(
45 | borderRadius: BorderRadius.circular(24.0),
46 | ),
47 | ),
48 | ),
49 | onPressed: () async {
50 | context.removeAndShowSnackbar('Connecting user');
51 |
52 | final success = await context.appState.connect(user);
53 |
54 | if (success) {
55 | if (!mounted) return;
56 | context.removeAndShowSnackbar('User connected');
57 |
58 | await Navigator.of(context).pushReplacement(
59 | MaterialPageRoute(
60 | builder: (_) => const HomeScreen(),
61 | ),
62 | );
63 | } else {
64 | if (!mounted) return;
65 | context
66 | .removeAndShowSnackbar('Could not connect user');
67 | }
68 | },
69 | child: Padding(
70 | padding: const EdgeInsets.symmetric(
71 | vertical: 24.0, horizontal: 36.0),
72 | child: SizedBox(
73 | width: 200,
74 | child: Text(
75 | user.name!,
76 | style: const TextStyle(
77 | fontSize: 18,
78 | color: Colors.blueGrey,
79 | ),
80 | ),
81 | ),
82 | ),
83 | ),
84 | )
85 | ],
86 | ),
87 | ),
88 | ),
89 | ),
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/lib/components/new_post/new_post.dart:
--------------------------------------------------------------------------------
1 | export 'new_post_screen.dart';
2 |
--------------------------------------------------------------------------------
/lib/components/new_post/new_post_screen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 | import 'package:flutter/material.dart';
3 | import 'package:image_picker/image_picker.dart';
4 | import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
5 | import 'package:transparent_image/transparent_image.dart';
6 |
7 | import '../../app/app.dart';
8 | import '../app_widgets/app_widgets.dart';
9 |
10 | /// Screen to choose photos and add a new feed post.
11 | class NewPostScreen extends StatefulWidget {
12 | /// Create a [NewPostScreen].
13 | const NewPostScreen({Key? key}) : super(key: key);
14 |
15 | /// Material route to this screen.
16 | static Route get route =>
17 | MaterialPageRoute(builder: (_) => const NewPostScreen());
18 |
19 | @override
20 | State createState() => _NewPostScreenState();
21 | }
22 |
23 | class _NewPostScreenState extends State {
24 | static const double maxImageHeight = 1000;
25 | static const double maxImageWidth = 800;
26 |
27 | final _formKey = GlobalKey();
28 | final _text = TextEditingController();
29 |
30 | XFile? _pickedFile;
31 | bool loading = false;
32 |
33 | final picker = ImagePicker();
34 |
35 | Future _pickFile() async {
36 | _pickedFile = await picker.pickImage(
37 | source: ImageSource.gallery,
38 | maxHeight: maxImageHeight,
39 | maxWidth: maxImageWidth,
40 | imageQuality: 70,
41 | );
42 | setState(() {});
43 | }
44 |
45 | Future _postImage() async {
46 | if (_pickedFile == null) {
47 | context.removeAndShowSnackbar('Please select an image first');
48 | return;
49 | }
50 |
51 | if (!_formKey.currentState!.validate()) {
52 | context.removeAndShowSnackbar('Please enter a caption');
53 | return;
54 | }
55 | _setLoading(true);
56 |
57 | final client = context.appState.client;
58 | final bloc = FeedProvider.of(context).bloc;
59 |
60 | var decodedImage =
61 | await decodeImageFromList(await _pickedFile!.readAsBytes());
62 |
63 | final imageUrl =
64 | await client.images.upload(AttachmentFile(path: _pickedFile!.path));
65 |
66 | if (imageUrl != null) {
67 | final resizedUrl = await client.images.getResized(
68 | imageUrl,
69 | const Resize(300, 300),
70 | );
71 |
72 | if (resizedUrl != null && client.currentUser != null) {
73 | await bloc.onAddActivity(
74 | feedGroup: 'user',
75 | verb: 'post',
76 | object: 'image',
77 | data: {
78 | 'description': _text.text,
79 | 'image_url': imageUrl,
80 | 'resized_image_url': resizedUrl,
81 | 'image_width': decodedImage.width,
82 | 'image_height': decodedImage.height,
83 | 'aspect_ratio': decodedImage.width / decodedImage.height
84 | },
85 | );
86 | }
87 | }
88 |
89 | _setLoading(false, shouldCallSetState: false);
90 | if (!mounted) return;
91 | context.removeAndShowSnackbar('Post created!');
92 |
93 | Navigator.of(context).pop();
94 | }
95 |
96 | void _setLoading(bool state, {bool shouldCallSetState = true}) {
97 | if (loading != state) {
98 | loading = state;
99 | if (shouldCallSetState) {
100 | setState(() {});
101 | }
102 | }
103 | }
104 |
105 | @override
106 | void dispose() {
107 | _text.dispose();
108 | super.dispose();
109 | }
110 |
111 | @override
112 | Widget build(BuildContext context) {
113 | return Scaffold(
114 | appBar: AppBar(
115 | leading: TapFadeIcon(
116 | onTap: () => Navigator.pop(context),
117 | icon: Icons.close,
118 | iconColor: Theme.of(context).appBarTheme.iconTheme!.color!,
119 | ),
120 | actions: [
121 | Padding(
122 | padding: const EdgeInsets.all(8.0),
123 | child: Center(
124 | child: GestureDetector(
125 | onTap: _postImage,
126 | child: const Text('Share', style: AppTextStyle.textStyleAction),
127 | ),
128 | ),
129 | )
130 | ],
131 | ),
132 | body: loading
133 | ? Center(
134 | child: Column(
135 | mainAxisAlignment: MainAxisAlignment.center,
136 | children: const [
137 | CircularProgressIndicator(),
138 | SizedBox(height: 12),
139 | Text('Uploading...')
140 | ],
141 | ),
142 | )
143 | : ListView(
144 | children: [
145 | InkWell(
146 | onTap: _pickFile,
147 | child: SizedBox(
148 | height: 400,
149 | child: (_pickedFile != null)
150 | ? FadeInImage(
151 | fit: BoxFit.contain,
152 | placeholder: MemoryImage(kTransparentImage),
153 | image: Image.file(File(_pickedFile!.path)).image,
154 | )
155 | : Container(
156 | decoration: const BoxDecoration(
157 | gradient: LinearGradient(
158 | begin: Alignment.bottomLeft,
159 | end: Alignment.topRight,
160 | colors: [
161 | AppColors.bottomGradient,
162 | AppColors.topGradient
163 | ]),
164 | ),
165 | height: 300,
166 | child: const Center(
167 | child: Text(
168 | 'Tap to select an image',
169 | style: TextStyle(
170 | color: AppColors.light,
171 | fontSize: 18,
172 | shadows: [
173 | Shadow(
174 | offset: Offset(2.0, 1.0),
175 | blurRadius: 3.0,
176 | color: Colors.black54,
177 | ),
178 | Shadow(
179 | offset: Offset(1.0, 1.5),
180 | blurRadius: 5.0,
181 | color: Colors.black54,
182 | ),
183 | ],
184 | ),
185 | ),
186 | ),
187 | ),
188 | ),
189 | ),
190 | const SizedBox(
191 | height: 22,
192 | ),
193 | Form(
194 | key: _formKey,
195 | child: Padding(
196 | padding: const EdgeInsets.all(8.0),
197 | child: TextFormField(
198 | controller: _text,
199 | decoration: const InputDecoration(
200 | hintText: 'Write a caption',
201 | border: InputBorder.none,
202 | ),
203 | validator: (text) {
204 | if (text == null || text.isEmpty) {
205 | return 'Caption is empty';
206 | }
207 | return null;
208 | },
209 | ),
210 | ),
211 | ),
212 | ],
213 | ),
214 | );
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/lib/components/profile/edit_profile_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:image_picker/image_picker.dart';
3 | import 'package:provider/provider.dart';
4 |
5 | import '../../app/app.dart';
6 | import '../app_widgets/app_widgets.dart';
7 |
8 | /// {@template edit_profile_page}
9 | /// Screen to edit a user's profile info.
10 | /// {@endtemplate}
11 | class EditProfileScreen extends StatelessWidget {
12 | /// {@macro edit_profile_page}
13 | const EditProfileScreen({
14 | Key? key,
15 | }) : super(key: key);
16 |
17 | /// Custom route to this screen. Animates from the bottom up.
18 | static Route get route => PageRouteBuilder(
19 | pageBuilder: (context, animation, secondaryAnimation) =>
20 | const EditProfileScreen(),
21 | transitionsBuilder: (context, animation, secondaryAnimation, child) {
22 | final tween = Tween(begin: const Offset(0.0, 1.0), end: Offset.zero)
23 | .chain(CurveTween(curve: Curves.easeOutQuint));
24 | final offsetAnimation = animation.drive(tween);
25 | return SlideTransition(
26 | position: offsetAnimation,
27 | child: child,
28 | );
29 | },
30 | );
31 |
32 | @override
33 | Widget build(BuildContext context) {
34 | final streamagramUser = context
35 | .select((value) => value.streamagramUser);
36 | if (streamagramUser == null) {
37 | return const Scaffold(
38 | body: Center(
39 | child: Text('You should not see this.\nUser data is empty.'),
40 | ),
41 | );
42 | }
43 | return Scaffold(
44 | appBar: AppBar(
45 | leading: TextButton(
46 | onPressed: () {
47 | Navigator.of(context).pop();
48 | },
49 | child: Text(
50 | 'Cancel',
51 | style: (Theme.of(context).brightness == Brightness.dark)
52 | ? const TextStyle(color: AppColors.light)
53 | : const TextStyle(color: AppColors.dark),
54 | ),
55 | ),
56 | leadingWidth: 80,
57 | title: const Text(
58 | ' Edit profile',
59 | style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
60 | ),
61 | actions: [
62 | TextButton(
63 | onPressed: () {
64 | Navigator.of(context).pop();
65 | },
66 | child: const Text('Done'),
67 | ),
68 | ],
69 | ),
70 | body: ListView(
71 | children: [
72 | const _ChangeProfilePictureButton(),
73 | const Divider(
74 | color: Colors.grey,
75 | ),
76 | Padding(
77 | padding: const EdgeInsets.all(8),
78 | child: Row(
79 | children: [
80 | const SizedBox(
81 | width: 100,
82 | child: Text(
83 | 'Name',
84 | style: AppTextStyle.textStyleBoldMedium,
85 | ),
86 | ),
87 | Text(
88 | '${streamagramUser.fullName} ',
89 | style: AppTextStyle.textStyleBoldMedium,
90 | ),
91 | ],
92 | ),
93 | ),
94 | Padding(
95 | padding: const EdgeInsets.all(8),
96 | child: Row(
97 | children: [
98 | const SizedBox(
99 | width: 100,
100 | child: Text(
101 | 'Username',
102 | style: AppTextStyle.textStyleBoldMedium,
103 | ),
104 | ),
105 | Text(
106 | '${context.appState.user.id} ',
107 | style: AppTextStyle.textStyleBoldMedium,
108 | ),
109 | ],
110 | ),
111 | ),
112 | const Divider(color: Colors.grey),
113 | ],
114 | ),
115 | );
116 | }
117 | }
118 |
119 | class _ChangeProfilePictureButton extends StatefulWidget {
120 | const _ChangeProfilePictureButton({
121 | Key? key,
122 | }) : super(key: key);
123 |
124 | @override
125 | __ChangeProfilePictureButtonState createState() =>
126 | __ChangeProfilePictureButtonState();
127 | }
128 |
129 | class __ChangeProfilePictureButtonState
130 | extends State<_ChangeProfilePictureButton> {
131 | final _picker = ImagePicker();
132 |
133 | Future _changePicture() async {
134 | if (context.appState.isUploadingProfilePicture == true) {
135 | return;
136 | }
137 |
138 | final pickedFile = await _picker.pickImage(
139 | source: ImageSource.gallery,
140 | maxWidth: 800,
141 | maxHeight: 800,
142 | imageQuality: 70,
143 | );
144 | if (pickedFile != null) {
145 | await context.appState.updateProfilePhoto(pickedFile.path);
146 | } else {
147 | if (!mounted) return;
148 | context.removeAndShowSnackbar('No picture selected');
149 | }
150 | }
151 |
152 | @override
153 | Widget build(BuildContext context) {
154 | final streamagramUser = context
155 | .select((value) => value.streamagramUser!);
156 | final isUploadingProfilePicture = context
157 | .select((value) => value.isUploadingProfilePicture);
158 | return Padding(
159 | padding: const EdgeInsets.all(8.0),
160 | child: Column(
161 | mainAxisSize: MainAxisSize.min,
162 | children: [
163 | SizedBox(
164 | height: 150,
165 | child: Center(
166 | child: isUploadingProfilePicture
167 | ? const CircularProgressIndicator()
168 | : GestureDetector(
169 | onTap: _changePicture,
170 | child: Avatar.huge(streamagramUser: streamagramUser),
171 | ),
172 | ),
173 | ),
174 | GestureDetector(
175 | onTap: _changePicture,
176 | child: const Text('Change Profile Photo',
177 | style: AppTextStyle.textStyleAction),
178 | ),
179 | ],
180 | ),
181 | );
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/lib/components/profile/profile.dart:
--------------------------------------------------------------------------------
1 | export 'edit_profile_screen.dart';
2 | export 'profile_page.dart';
3 |
--------------------------------------------------------------------------------
/lib/components/profile/profile_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:cached_network_image/cached_network_image.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:provider/provider.dart';
4 | import 'package:stream_agram/components/profile/edit_profile_screen.dart';
5 | import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
6 |
7 | import '../../app/app.dart';
8 | import '../app_widgets/app_widgets.dart';
9 | import '../new_post/new_post.dart';
10 |
11 | /// {@template profile_page}
12 | /// User profile page. List of user created posts.
13 | /// {@endtemplate}
14 | class ProfilePage extends StatefulWidget {
15 | /// {@macro profile_page}
16 | const ProfilePage({Key? key}) : super(key: key);
17 |
18 | @override
19 | State createState() => _ProfilePageState();
20 | }
21 |
22 | class _ProfilePageState extends State {
23 | bool _isPaginating = false;
24 |
25 | static const _feedGroup = 'user';
26 |
27 | Future _loadMore() async {
28 | // Ensure we're not already loading more activities.
29 | if (!_isPaginating) {
30 | _isPaginating = true;
31 | context.feedBloc
32 | .loadMoreEnrichedActivities(feedGroup: _feedGroup)
33 | .whenComplete(() {
34 | _isPaginating = false;
35 | });
36 | }
37 | }
38 |
39 | @override
40 | Widget build(BuildContext context) {
41 | return FlatFeedCore(
42 | feedGroup: _feedGroup,
43 | limit: 12,
44 | loadingBuilder: (context) =>
45 | const Center(child: CircularProgressIndicator()),
46 | errorBuilder: (context, error) => const Center(
47 | child: Text('Error loading profile'),
48 | ),
49 | emptyBuilder: (context) => const CustomScrollView(
50 | slivers: [
51 | SliverToBoxAdapter(
52 | child: _ProfileHeader(
53 | numberOfPosts: 0,
54 | ),
55 | ),
56 | SliverToBoxAdapter(
57 | child: _EditProfileButton(),
58 | ),
59 | SliverFillRemaining(child: _NoPostsMessage())
60 | ],
61 | ),
62 | feedBuilder: (context, activities) {
63 | return RefreshIndicator(
64 | onRefresh: () async {
65 | // Refresh follow counts
66 | await FeedProvider.of(context)
67 | .bloc
68 | .currentUser!
69 | .get(withFollowCounts: true);
70 | // Refresh activities
71 | if (!mounted) return;
72 | return FeedProvider.of(context)
73 | .bloc
74 | .refreshPaginatedEnrichedActivities(feedGroup: _feedGroup);
75 | },
76 | child: CustomScrollView(
77 | slivers: [
78 | SliverToBoxAdapter(
79 | child: _ProfileHeader(
80 | numberOfPosts: activities.length,
81 | ),
82 | ),
83 | const SliverToBoxAdapter(
84 | child: _EditProfileButton(),
85 | ),
86 | const SliverToBoxAdapter(
87 | child: SizedBox(height: 24),
88 | ),
89 | SliverGrid(
90 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
91 | crossAxisCount: 3,
92 | crossAxisSpacing: 1,
93 | mainAxisSpacing: 1,
94 | ),
95 | delegate: SliverChildBuilderDelegate(
96 | (context, index) {
97 | // Pagination (Infinite scroll)
98 | bool shouldLoadMore = activities.length - 3 == index;
99 | if (shouldLoadMore) {
100 | _loadMore();
101 | }
102 | final activity = activities[index];
103 | final url =
104 | activity.extraData!['resized_image_url'] as String;
105 | return GestureDetector(
106 | onTap: () {
107 | Navigator.of(context).push(
108 | HeroDialogRoute(
109 | builder: (context) {
110 | return _PictureViewer(activity: activity);
111 | },
112 | ),
113 | );
114 | },
115 | child: Hero(
116 | tag: 'hero-image-${activity.id}',
117 | child: CachedNetworkImage(
118 | key: ValueKey('image-${activity.id}'),
119 | width: 200,
120 | height: 200,
121 | fit: BoxFit.cover,
122 | imageUrl: url,
123 | ),
124 | ),
125 | );
126 | },
127 | childCount: activities.length,
128 | ),
129 | )
130 | ],
131 | ),
132 | );
133 | },
134 | );
135 | }
136 | }
137 |
138 | class _EditProfileButton extends StatelessWidget {
139 | const _EditProfileButton({Key? key}) : super(key: key);
140 |
141 | @override
142 | Widget build(BuildContext context) {
143 | return Padding(
144 | padding: const EdgeInsets.symmetric(horizontal: 8.0),
145 | child: OutlinedButton(
146 | onPressed: () {
147 | Navigator.of(context).push(EditProfileScreen.route);
148 | },
149 | child: const Text('Edit Profile'),
150 | ),
151 | );
152 | }
153 | }
154 |
155 | class _ProfileHeader extends StatelessWidget {
156 | const _ProfileHeader({
157 | Key? key,
158 | required this.numberOfPosts,
159 | }) : super(key: key);
160 |
161 | final int numberOfPosts;
162 |
163 | static const _statitisticsPadding =
164 | EdgeInsets.symmetric(horizontal: 12, vertical: 8.0);
165 |
166 | @override
167 | Widget build(BuildContext context) {
168 | final feedState = context.watch();
169 | final streamagramUser = feedState.streamagramUser;
170 | if (streamagramUser == null) return const SizedBox.shrink();
171 | return Column(
172 | children: [
173 | Row(
174 | children: [
175 | Padding(
176 | padding: const EdgeInsets.all(8.0),
177 | child: Avatar.big(
178 | streamagramUser: streamagramUser,
179 | ),
180 | ),
181 | const Spacer(),
182 | Row(
183 | mainAxisSize: MainAxisSize.min,
184 | children: [
185 | Padding(
186 | padding: _statitisticsPadding,
187 | child: Column(
188 | children: [
189 | Text(
190 | '$numberOfPosts',
191 | style: AppTextStyle.textStyleBold,
192 | ),
193 | const Text(
194 | 'Posts',
195 | style: AppTextStyle.textStyleLight,
196 | ),
197 | ],
198 | ),
199 | ),
200 | Padding(
201 | padding: _statitisticsPadding,
202 | child: Column(
203 | children: [
204 | Text(
205 | '${FeedProvider.of(context).bloc.currentUser?.followersCount ?? 0}',
206 | style: AppTextStyle.textStyleBold,
207 | ),
208 | const Text(
209 | 'Followers',
210 | style: AppTextStyle.textStyleLight,
211 | ),
212 | ],
213 | ),
214 | ),
215 | Padding(
216 | padding: _statitisticsPadding,
217 | child: Column(
218 | children: [
219 | Text(
220 | '${FeedProvider.of(context).bloc.currentUser?.followingCount ?? 0}',
221 | style: AppTextStyle.textStyleBold,
222 | ),
223 | const Text(
224 | 'Following',
225 | style: AppTextStyle.textStyleLight,
226 | ),
227 | ],
228 | ),
229 | ),
230 | ],
231 | ),
232 | ],
233 | ),
234 | Align(
235 | alignment: Alignment.centerLeft,
236 | child: Padding(
237 | padding: const EdgeInsets.all(8.0),
238 | child: Text(streamagramUser.fullName,
239 | style: AppTextStyle.textStyleBoldMedium),
240 | ),
241 | ),
242 | ],
243 | );
244 | }
245 | }
246 |
247 | class _NoPostsMessage extends StatelessWidget {
248 | const _NoPostsMessage({
249 | Key? key,
250 | }) : super(key: key);
251 |
252 | @override
253 | Widget build(BuildContext context) {
254 | return Column(
255 | mainAxisAlignment: MainAxisAlignment.center,
256 | children: [
257 | const Text('This is too empty'),
258 | const SizedBox(height: 12),
259 | ElevatedButton(
260 | onPressed: () {
261 | Navigator.of(context).push(NewPostScreen.route); // ADD THIS
262 | },
263 | child: const Text('Add a post'),
264 | )
265 | ],
266 | );
267 | }
268 | }
269 |
270 | class _PictureViewer extends StatelessWidget {
271 | const _PictureViewer({
272 | Key? key,
273 | required this.activity,
274 | }) : super(key: key);
275 |
276 | final EnrichedActivity activity;
277 |
278 | @override
279 | Widget build(BuildContext context) {
280 | final resizedUrl = activity.extraData!['resized_image_url'] as String?;
281 | final fullSizeUrl = activity.extraData!['image_url'] as String;
282 | final aspectRatio = activity.extraData!['aspect_ratio'] as double?;
283 |
284 | return Scaffold(
285 | appBar: AppBar(
286 | elevation: 0,
287 | backgroundColor: Colors.transparent,
288 | ),
289 | extendBodyBehindAppBar: true,
290 | body: InteractiveViewer(
291 | child: Center(
292 | child: Hero(
293 | tag: 'hero-image-${activity.id}',
294 | createRectTween: (begin, end) {
295 | return CustomRectTween(begin: begin, end: end);
296 | },
297 | child: AspectRatio(
298 | aspectRatio: aspectRatio ?? 1,
299 | child: CachedNetworkImage(
300 | fadeInDuration: Duration.zero,
301 | placeholder: (resizedUrl != null)
302 | ? (context, url) => CachedNetworkImage(
303 | imageBuilder: (context, imageProvider) =>
304 | DecoratedBox(
305 | decoration: BoxDecoration(
306 | image: DecorationImage(
307 | image: imageProvider,
308 | fit: BoxFit.contain,
309 | ),
310 | ),
311 | ),
312 | imageUrl: resizedUrl,
313 | )
314 | : null,
315 | imageBuilder: (context, imageProvider) => DecoratedBox(
316 | decoration: BoxDecoration(
317 | image: DecorationImage(
318 | image: imageProvider,
319 | fit: BoxFit.contain,
320 | ),
321 | ),
322 | ),
323 | imageUrl: fullSizeUrl,
324 | ),
325 | ),
326 | ),
327 | ),
328 | ),
329 | );
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/lib/components/search/search.dart:
--------------------------------------------------------------------------------
1 | export 'search_page.dart';
2 |
--------------------------------------------------------------------------------
/lib/components/search/search_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
3 |
4 | import '../../app/app.dart';
5 | import '../app_widgets/app_widgets.dart';
6 |
7 | /// Page to find other users and follow/unfollow.
8 | class SearchPage extends StatelessWidget {
9 | /// Create a new [SearchPage].
10 | const SearchPage({Key? key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | final users = List.from(DemoAppUser.values)
15 | ..removeWhere((it) => it.id == context.appState.user.id);
16 | return ListView.builder(
17 | itemCount: users.length,
18 | itemBuilder: (context, index) {
19 | return _UserProfile(userId: users[index].id);
20 | },
21 | );
22 | }
23 | }
24 |
25 | class _UserProfile extends StatefulWidget {
26 | const _UserProfile({
27 | Key? key,
28 | required this.userId,
29 | }) : super(key: key);
30 |
31 | final String userId;
32 |
33 | @override
34 | __UserProfileState createState() => __UserProfileState();
35 | }
36 |
37 | class __UserProfileState extends State<_UserProfile> {
38 | late StreamUser streamUser;
39 | late bool isFollowing;
40 | late Future userDataFuture = getUser();
41 |
42 | Future getUser() async {
43 | final userClient = context.appState.client.user(widget.userId);
44 | final futures = await Future.wait([
45 | userClient.get(),
46 | _isFollowingUser(widget.userId),
47 | ]);
48 | streamUser = futures[0] as StreamUser;
49 | isFollowing = futures[1] as bool;
50 |
51 | return StreamagramUser.fromMap(streamUser.data!);
52 | }
53 |
54 | /// Determine if the current authenticated user is following [user].
55 | Future _isFollowingUser(String userId) async {
56 | return FeedProvider.of(context).bloc.isFollowingFeed(followerId: userId);
57 | }
58 |
59 | @override
60 | Widget build(BuildContext context) {
61 | return FutureBuilder(
62 | future: userDataFuture,
63 | builder: (context, snapshot) {
64 | switch (snapshot.connectionState) {
65 | case ConnectionState.waiting:
66 | return const SizedBox.shrink();
67 | default:
68 | if (snapshot.hasError) {
69 | return const Padding(
70 | padding: EdgeInsets.all(8.0),
71 | child: Text('Could not load profile'),
72 | );
73 | } else {
74 | final userData = snapshot.data;
75 | if (userData != null) {
76 | return _ProfileTile(
77 | user: streamUser,
78 | userData: userData,
79 | isFollowing: isFollowing,
80 | );
81 | }
82 | return const SizedBox.shrink();
83 | }
84 | }
85 | },
86 | );
87 | }
88 | }
89 |
90 | class _ProfileTile extends StatefulWidget {
91 | const _ProfileTile({
92 | Key? key,
93 | required this.user,
94 | required this.userData,
95 | required this.isFollowing,
96 | }) : super(key: key);
97 |
98 | final StreamUser user;
99 | final StreamagramUser userData;
100 | final bool isFollowing;
101 |
102 | @override
103 | __ProfileTileState createState() => __ProfileTileState();
104 | }
105 |
106 | class __ProfileTileState extends State<_ProfileTile> {
107 | bool _isLoading = false;
108 | late bool _isFollowing = widget.isFollowing;
109 |
110 | Future followOrUnfollowUser(BuildContext context) async {
111 | setState(() {
112 | _isLoading = true;
113 | });
114 | final bloc = FeedProvider.of(context).bloc;
115 | if (_isFollowing) {
116 | final bloc = FeedProvider.of(context).bloc;
117 | await bloc.unfollowFeed(unfolloweeId: widget.user.id);
118 | _isFollowing = false;
119 | } else {
120 | await FeedProvider.of(context)
121 | .bloc
122 | .followFeed(followeeId: widget.user.id);
123 | _isFollowing = true;
124 | }
125 | bloc.refreshPaginatedEnrichedActivities(
126 | feedGroup: 'timeline',
127 | flags: EnrichmentFlags()
128 | ..withOwnReactions()
129 | ..withRecentReactions()
130 | ..withReactionCounts(),
131 | );
132 |
133 | setState(() {
134 | _isLoading = false;
135 | });
136 | }
137 |
138 | @override
139 | Widget build(BuildContext context) {
140 | return Row(
141 | children: [
142 | Padding(
143 | padding: const EdgeInsets.all(8.0),
144 | child: Avatar.medium(streamagramUser: widget.userData),
145 | ),
146 | Padding(
147 | padding: const EdgeInsets.all(8.0),
148 | child: Column(
149 | crossAxisAlignment: CrossAxisAlignment.start,
150 | children: [
151 | Text(widget.user.id, style: AppTextStyle.textStyleBold),
152 | Text(
153 | widget.userData.fullName,
154 | style: AppTextStyle.textStyleFaded,
155 | ),
156 | ],
157 | ),
158 | ),
159 | const Spacer(),
160 | Padding(
161 | padding: const EdgeInsets.symmetric(horizontal: 8.0),
162 | child: _isLoading
163 | ? const CircularProgressIndicator(strokeWidth: 3)
164 | : OutlinedButton(
165 | onPressed: () {
166 | followOrUnfollowUser(context);
167 | },
168 | child: _isFollowing
169 | ? const Text('Unfollow')
170 | : const Text('Follow'),
171 | ),
172 | )
173 | ],
174 | );
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/lib/components/timeline/timeline.dart:
--------------------------------------------------------------------------------
1 | export 'widgets/widgets.dart';
2 | export 'timeline_page.dart';
3 |
--------------------------------------------------------------------------------
/lib/components/timeline/timeline_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:stream_agram/app/app.dart';
3 | import 'package:stream_agram/components/app_widgets/app_widgets.dart';
4 | import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
5 |
6 | import 'widgets/widgets.dart';
7 |
8 | /// {@template timeline_page}
9 | /// Page to display a timeline of user created posts. Global 'timeline'
10 | /// {@endtemplate}
11 | class TimelinePage extends StatefulWidget {
12 | /// {@macro timeline_page}
13 | const TimelinePage({Key? key}) : super(key: key);
14 |
15 | @override
16 | State createState() => _TimelinePageState();
17 | }
18 |
19 | class _TimelinePageState extends State {
20 | final ValueNotifier _showCommentBox = ValueNotifier(false);
21 | final TextEditingController _commentTextController = TextEditingController();
22 | final FocusNode _commentFocusNode = FocusNode();
23 | EnrichedActivity? activeActivity;
24 | bool _isPaginating = false;
25 |
26 | static const _feedGroup = 'timeline';
27 | static final _flags = EnrichmentFlags()
28 | ..withOwnReactions()
29 | ..withRecentReactions()
30 | ..withReactionCounts();
31 |
32 | void openCommentBox(EnrichedActivity activity, {String? message}) {
33 | _commentTextController.text = message ?? '';
34 | _commentTextController.selection = TextSelection.fromPosition(
35 | TextPosition(offset: _commentTextController.text.length));
36 | activeActivity = activity;
37 | _showCommentBox.value = true;
38 | _commentFocusNode.requestFocus();
39 | }
40 |
41 | Future addComment(String? message) async {
42 | if (activeActivity != null &&
43 | message != null &&
44 | message.isNotEmpty &&
45 | message != '') {
46 | await FeedProvider.of(context).bloc.onAddReaction(
47 | kind: 'comment',
48 | activity: activeActivity!,
49 | feedGroup: 'timeline',
50 | data: {'message': message},
51 | );
52 | _commentTextController.clear();
53 | if (!mounted) return;
54 | FocusScope.of(context).unfocus();
55 | _showCommentBox.value = false;
56 | }
57 | }
58 |
59 | Future _loadMore() async {
60 | // Ensure we're not already loading more activities.
61 | if (!_isPaginating) {
62 | _isPaginating = true;
63 | context.feedBloc
64 | .loadMoreEnrichedActivities(feedGroup: _feedGroup, flags: _flags)
65 | .whenComplete(() {
66 | _isPaginating = false;
67 | });
68 | }
69 | }
70 |
71 | @override
72 | void dispose() {
73 | _commentTextController.dispose();
74 | _commentFocusNode.dispose();
75 | super.dispose();
76 | }
77 |
78 | @override
79 | Widget build(BuildContext context) {
80 | return GestureDetector(
81 | onTap: () {
82 | FocusScope.of(context).unfocus();
83 | _showCommentBox.value = false;
84 | },
85 | child: Stack(
86 | children: [
87 | FlatFeedCore(
88 | feedGroup: _feedGroup,
89 | errorBuilder: (context, error) =>
90 | const Text('Could not load profile'),
91 | loadingBuilder: (context) => const SizedBox(),
92 | emptyBuilder: (context) => const Center(
93 | child: Text('No Posts\nGo and post something'),
94 | ),
95 | flags: _flags,
96 | feedBuilder: (context, activities) {
97 | return RefreshIndicator(
98 | onRefresh: () {
99 | return FeedProvider.of(context)
100 | .bloc
101 | .refreshPaginatedEnrichedActivities(
102 | feedGroup: 'timeline',
103 | flags: EnrichmentFlags()
104 | ..withOwnReactions()
105 | ..withRecentReactions()
106 | ..withReactionCounts(),
107 | );
108 | },
109 | child: ListView.builder(
110 | itemCount: activities.length,
111 | itemBuilder: (context, index) {
112 | // Pagination (Infinite scroll)
113 | bool shouldLoadMore = activities.length - 3 == index;
114 | if (shouldLoadMore) {
115 | _loadMore();
116 | }
117 |
118 | return PostCard(
119 | key: ValueKey('post-${activities[index].id}'),
120 | enrichedActivity: activities[index],
121 | onAddComment: openCommentBox,
122 | );
123 | },
124 | ),
125 | );
126 | },
127 | ),
128 | _CommentBox(
129 | commenter: context.appState.streamagramUser!,
130 | textEditingController: _commentTextController,
131 | focusNode: _commentFocusNode,
132 | addComment: addComment,
133 | showCommentBox: _showCommentBox,
134 | )
135 | ],
136 | ),
137 | );
138 | }
139 | }
140 |
141 | class _CommentBox extends StatefulWidget {
142 | const _CommentBox({
143 | Key? key,
144 | required this.commenter,
145 | required this.textEditingController,
146 | required this.focusNode,
147 | required this.addComment,
148 | required this.showCommentBox,
149 | }) : super(key: key);
150 |
151 | final StreamagramUser commenter;
152 | final TextEditingController textEditingController;
153 | final FocusNode focusNode;
154 | final Function(String?) addComment;
155 | final ValueNotifier showCommentBox;
156 |
157 | @override
158 | __CommentBoxState createState() => __CommentBoxState();
159 | }
160 |
161 | class __CommentBoxState extends State<_CommentBox>
162 | with SingleTickerProviderStateMixin {
163 | late final AnimationController _controller;
164 |
165 | late final Animation _animation = CurvedAnimation(
166 | parent: _controller,
167 | curve: Curves.easeOut,
168 | reverseCurve: Curves.easeIn,
169 | );
170 |
171 | bool visibility = false;
172 |
173 | @override
174 | void initState() {
175 | super.initState();
176 | _controller = AnimationController(
177 | vsync: this,
178 | duration: const Duration(milliseconds: 300),
179 | );
180 | _controller.addStatusListener((status) {
181 | if (status == AnimationStatus.dismissed) {
182 | setState(() {
183 | visibility = false;
184 | });
185 | } else {
186 | setState(() {
187 | visibility = true;
188 | });
189 | }
190 | });
191 | widget.showCommentBox.addListener(_showHideCommentBox);
192 | }
193 |
194 | void _showHideCommentBox() {
195 | if (widget.showCommentBox.value) {
196 | _controller.forward();
197 | } else {
198 | _controller.reverse();
199 | }
200 | }
201 |
202 | @override
203 | void dispose() {
204 | super.dispose();
205 | _controller.dispose();
206 | }
207 |
208 | @override
209 | Widget build(BuildContext context) {
210 | return Visibility(
211 | visible: visibility,
212 | child: FadeTransition(
213 | opacity: _animation,
214 | child: Builder(builder: (context) {
215 | return Align(
216 | alignment: Alignment.bottomCenter,
217 | child: CommentBox(
218 | commenter: widget.commenter,
219 | textEditingController: widget.textEditingController,
220 | focusNode: widget.focusNode,
221 | onSubmitted: widget.addComment,
222 | ),
223 | );
224 | }),
225 | ),
226 | );
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/lib/components/timeline/widgets/post_card.dart:
--------------------------------------------------------------------------------
1 | import 'package:cached_network_image/cached_network_image.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:jiffy/jiffy.dart';
4 | import 'package:provider/provider.dart';
5 | import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
6 |
7 | import '../../../app/app.dart';
8 | import '../../app_widgets/app_widgets.dart';
9 | import '../../comments/comments_screen.dart';
10 |
11 | typedef OnAddComment = void Function(
12 | EnrichedActivity activity, {
13 | String? message,
14 | });
15 |
16 | /// {@template post_card}
17 | /// A card that displays a user post/activity.
18 | /// {@endtemplate}
19 | class PostCard extends StatelessWidget {
20 | /// {@macro post_card}
21 | const PostCard({
22 | Key? key,
23 | required this.enrichedActivity,
24 | required this.onAddComment,
25 | }) : super(key: key);
26 |
27 | /// Enriched activity (post) to display.
28 | final EnrichedActivity enrichedActivity;
29 | final OnAddComment onAddComment;
30 |
31 | @override
32 | Widget build(BuildContext context) {
33 | final actorData = enrichedActivity.actor!.data;
34 | final userData = StreamagramUser.fromMap(actorData as Map);
35 |
36 | return Column(
37 | crossAxisAlignment: CrossAxisAlignment.start,
38 | children: [
39 | _ProfileSlab(
40 | userData: userData,
41 | ),
42 | _PictureCarousal(
43 | enrichedActivity: enrichedActivity,
44 | ),
45 | _Description(
46 | enrichedActivity: enrichedActivity,
47 | ),
48 | _InteractiveCommentSlab(
49 | enrichedActivity: enrichedActivity,
50 | onAddComment: onAddComment,
51 | ),
52 | ],
53 | );
54 | }
55 | }
56 |
57 | class _PictureCarousal extends StatefulWidget {
58 | const _PictureCarousal({
59 | Key? key,
60 | required this.enrichedActivity,
61 | }) : super(key: key);
62 |
63 | final EnrichedActivity enrichedActivity;
64 |
65 | @override
66 | __PictureCarousalState createState() => __PictureCarousalState();
67 | }
68 |
69 | class __PictureCarousalState extends State<_PictureCarousal> {
70 | late var likeReactions = getLikeReactions() ?? [];
71 | late var likeCount = getLikeCount() ?? 0;
72 |
73 | Reaction? latestLikeReaction;
74 |
75 | List? getLikeReactions() {
76 | return widget.enrichedActivity.latestReactions?['like'] ?? [];
77 | }
78 |
79 | int? getLikeCount() {
80 | return widget.enrichedActivity.reactionCounts?['like'] ?? 0;
81 | }
82 |
83 | Future _addLikeReaction() async {
84 | latestLikeReaction = await context.appState.client.reactions.add(
85 | 'like',
86 | widget.enrichedActivity.id!,
87 | userId: context.appState.user.id,
88 | );
89 |
90 | setState(() {
91 | likeReactions.add(latestLikeReaction!);
92 | likeCount++;
93 | });
94 | }
95 |
96 | Future _removeLikeReaction() async {
97 | late String? reactionId;
98 | // A new reaction was added to this state.
99 | if (latestLikeReaction != null) {
100 | reactionId = latestLikeReaction?.id;
101 | } else {
102 | // An old reaction has been retrieved from Stream.
103 | final prevReaction = widget.enrichedActivity.ownReactions?['like'];
104 | if (prevReaction != null && prevReaction.isNotEmpty) {
105 | reactionId = prevReaction[0].id;
106 | }
107 | }
108 |
109 | try {
110 | if (reactionId != null) {
111 | await context.appState.client.reactions.delete(reactionId);
112 | }
113 | } catch (e) {
114 | debugPrint(e.toString());
115 | }
116 | setState(() {
117 | likeReactions.removeWhere((element) => element.id == reactionId);
118 | likeCount--;
119 | latestLikeReaction = null;
120 | });
121 | }
122 |
123 | @override
124 | Widget build(BuildContext context) {
125 | return Column(
126 | crossAxisAlignment: CrossAxisAlignment.start,
127 | children: [
128 | ..._pictureCarousel(context),
129 | _likes(),
130 | ],
131 | );
132 | }
133 |
134 | /// Picture carousal and interaction buttons.
135 | List _pictureCarousel(BuildContext context) {
136 | const iconPadding = EdgeInsets.symmetric(horizontal: 8, vertical: 4);
137 | var imageUrl = widget.enrichedActivity.extraData!['image_url'] as String;
138 | double aspectRatio =
139 | widget.enrichedActivity.extraData!['aspect_ratio'] as double? ?? 1.0;
140 | final iconColor = Theme.of(context).iconTheme.color!;
141 | return [
142 | Padding(
143 | padding: const EdgeInsets.symmetric(vertical: 8.0),
144 | child: Center(
145 | child: ConstrainedBox(
146 | constraints: const BoxConstraints(maxHeight: 500),
147 | child: AspectRatio(
148 | aspectRatio: aspectRatio,
149 | child: CachedNetworkImage(
150 | imageUrl: imageUrl,
151 | ),
152 | ),
153 | ),
154 | ),
155 | ),
156 | Row(
157 | children: [
158 | const SizedBox(
159 | width: 4,
160 | ),
161 | Padding(
162 | padding: iconPadding,
163 | child: FavoriteIconButton(
164 | isLiked: widget.enrichedActivity.ownReactions?['like'] != null,
165 | onTap: (liked) {
166 | if (liked) {
167 | _addLikeReaction();
168 | } else {
169 | _removeLikeReaction();
170 | }
171 | },
172 | ),
173 | ),
174 | Padding(
175 | padding: iconPadding,
176 | child: TapFadeIcon(
177 | onTap: () {
178 | // ADD THIS
179 | final map = widget.enrichedActivity.actor!.data!;
180 |
181 | // AND THIS
182 | Navigator.of(context).push(
183 | CommentsScreen.route(
184 | enrichedActivity: widget.enrichedActivity,
185 | activityOwnerData: StreamagramUser.fromMap(map),
186 | ),
187 | );
188 | },
189 | icon: Icons.chat_bubble_outline,
190 | iconColor: iconColor,
191 | ),
192 | ),
193 | Padding(
194 | padding: iconPadding,
195 | child: TapFadeIcon(
196 | onTap: () =>
197 | context.removeAndShowSnackbar('Message: Not yet implemented'),
198 | icon: Icons.call_made,
199 | iconColor: iconColor,
200 | ),
201 | ),
202 | const Spacer(),
203 | Padding(
204 | padding: iconPadding,
205 | child: TapFadeIcon(
206 | onTap: () => context
207 | .removeAndShowSnackbar('Bookmark: Not yet implemented'),
208 | icon: Icons.bookmark_border,
209 | iconColor: iconColor,
210 | ),
211 | ),
212 | ],
213 | )
214 | ];
215 | }
216 |
217 | Widget _likes() {
218 | if (likeReactions.isNotEmpty) {
219 | return Padding(
220 | padding: const EdgeInsets.only(left: 16.0, top: 8),
221 | child: Text.rich(
222 | TextSpan(
223 | text: 'Liked by ',
224 | style: AppTextStyle.textStyleLight,
225 | children: [
226 | TextSpan(
227 | text: StreamagramUser.fromMap(
228 | likeReactions[0].user?.data as Map)
229 | .fullName,
230 | style: AppTextStyle.textStyleBold),
231 | if (likeCount > 1 && likeCount < 3) ...[
232 | const TextSpan(text: ' and '),
233 | TextSpan(
234 | text: StreamagramUser.fromMap(
235 | likeReactions[1].user?.data as Map)
236 | .fullName,
237 | style: AppTextStyle.textStyleBold),
238 | ],
239 | if (likeCount > 3) ...[
240 | const TextSpan(text: ' and '),
241 | const TextSpan(
242 | text: 'others', style: AppTextStyle.textStyleBold),
243 | ],
244 | ],
245 | ),
246 | ),
247 | );
248 | } else {
249 | return const SizedBox.shrink();
250 | }
251 | }
252 | }
253 |
254 | class _Description extends StatelessWidget {
255 | const _Description({
256 | Key? key,
257 | required this.enrichedActivity,
258 | }) : super(key: key);
259 |
260 | final EnrichedActivity enrichedActivity;
261 |
262 | @override
263 | Widget build(BuildContext context) {
264 | return Padding(
265 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6),
266 | child: Text.rich(
267 | TextSpan(
268 | children: [
269 | TextSpan(
270 | text: enrichedActivity.actor!.id!,
271 | style: AppTextStyle.textStyleBold),
272 | const TextSpan(text: ' '),
273 | TextSpan(
274 | text: enrichedActivity.extraData?['description'] as String? ??
275 | ''),
276 | ],
277 | ),
278 | ),
279 | );
280 | }
281 | }
282 |
283 | class _InteractiveCommentSlab extends StatefulWidget {
284 | const _InteractiveCommentSlab({
285 | Key? key,
286 | required this.enrichedActivity,
287 | required this.onAddComment,
288 | }) : super(key: key);
289 |
290 | final EnrichedActivity enrichedActivity;
291 | final OnAddComment onAddComment;
292 |
293 | @override
294 | _InteractiveCommentSlabState createState() => _InteractiveCommentSlabState();
295 | }
296 |
297 | class _InteractiveCommentSlabState extends State<_InteractiveCommentSlab> {
298 | EnrichedActivity get enrichedActivity => widget.enrichedActivity;
299 |
300 | late final String _timeSinceMessage =
301 | Jiffy(widget.enrichedActivity.time).fromNow();
302 |
303 | List get _commentReactions =>
304 | enrichedActivity.latestReactions?['comment'] ?? [];
305 |
306 | int get _commentCount => enrichedActivity.reactionCounts?['comment'] ?? 0;
307 |
308 | @override
309 | Widget build(BuildContext context) {
310 | const textPadding = EdgeInsets.all(8);
311 | const spacePadding = EdgeInsets.only(left: 20.0, top: 8);
312 | final comments = _commentReactions;
313 | final commentCount = _commentCount;
314 | return Column(
315 | crossAxisAlignment: CrossAxisAlignment.start,
316 | children: [
317 | if (commentCount > 0 && comments.isNotEmpty)
318 | Padding(
319 | padding: spacePadding,
320 | child: Text.rich(
321 | TextSpan(
322 | children: [
323 | TextSpan(
324 | text: StreamagramUser.fromMap(
325 | comments[0].user?.data as Map)
326 | .fullName,
327 | style: AppTextStyle.textStyleBold),
328 | const TextSpan(text: ' '),
329 | TextSpan(text: comments[0].data?['message'] as String?),
330 | ],
331 | ),
332 | ),
333 | ),
334 | if (commentCount > 1 && comments.isNotEmpty)
335 | Padding(
336 | padding: spacePadding,
337 | child: Text.rich(
338 | TextSpan(
339 | children: [
340 | TextSpan(
341 | text: StreamagramUser.fromMap(
342 | comments[1].user?.data as Map)
343 | .fullName,
344 | style: AppTextStyle.textStyleBold),
345 | const TextSpan(text: ' '),
346 | TextSpan(text: comments[1].data?['message'] as String?),
347 | ],
348 | ),
349 | ),
350 | ),
351 | if (commentCount > 2)
352 | Padding(
353 | padding: spacePadding,
354 | child: GestureDetector(
355 | onTap: () {
356 | final map =
357 | widget.enrichedActivity.actor!.data as Map;
358 | // AND THIS
359 | Navigator.of(context).push(CommentsScreen.route(
360 | enrichedActivity: widget.enrichedActivity,
361 | activityOwnerData: StreamagramUser.fromMap(map),
362 | ));
363 | },
364 | child: Text(
365 | 'View all $commentCount comments',
366 | style: AppTextStyle.textStyleFaded,
367 | ),
368 | ),
369 | ),
370 | GestureDetector(
371 | behavior: HitTestBehavior.opaque,
372 | onTap: () {
373 | widget.onAddComment(enrichedActivity);
374 | },
375 | child: Padding(
376 | padding: const EdgeInsets.only(left: 16.0, top: 3, right: 8),
377 | child: Row(
378 | children: [
379 | const _ProfilePicture(),
380 | const Expanded(
381 | child: Padding(
382 | padding: EdgeInsets.only(left: 8.0),
383 | child: Text(
384 | 'Add a comment',
385 | style: TextStyle(
386 | color: AppColors.faded,
387 | fontSize: 14,
388 | ),
389 | ),
390 | ),
391 | ),
392 | GestureDetector(
393 | onTap: () {
394 | widget.onAddComment(enrichedActivity, message: '❤️');
395 | },
396 | child: const Padding(
397 | padding: textPadding,
398 | child: Text('❤️'),
399 | ),
400 | ),
401 | GestureDetector(
402 | onTap: () {
403 | widget.onAddComment(enrichedActivity, message: '🙌');
404 | },
405 | child: const Padding(
406 | padding: textPadding,
407 | child: Text('🙌'),
408 | ),
409 | ),
410 | ],
411 | ),
412 | ),
413 | ),
414 | Padding(
415 | padding: const EdgeInsets.only(left: 16.0, top: 4),
416 | child: Text(
417 | _timeSinceMessage,
418 | style: const TextStyle(
419 | color: AppColors.faded,
420 | fontWeight: FontWeight.w400,
421 | fontSize: 13,
422 | ),
423 | ),
424 | ),
425 | ],
426 | );
427 | }
428 | }
429 |
430 | class _ProfilePicture extends StatelessWidget {
431 | const _ProfilePicture({Key? key}) : super(key: key);
432 |
433 | @override
434 | Widget build(BuildContext context) {
435 | final streamagramUser = context.watch().streamagramUser;
436 | if (streamagramUser == null) {
437 | return const Icon(Icons.error);
438 | }
439 | return Avatar.small(
440 | streamagramUser: streamagramUser,
441 | );
442 | }
443 | }
444 |
445 | class _ProfileSlab extends StatelessWidget {
446 | const _ProfileSlab({
447 | Key? key,
448 | required this.userData,
449 | }) : super(key: key);
450 |
451 | final StreamagramUser userData;
452 |
453 | @override
454 | Widget build(BuildContext context) {
455 | return Padding(
456 | padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0),
457 | child: Row(
458 | children: [
459 | Avatar.medium(streamagramUser: userData),
460 | Padding(
461 | padding: const EdgeInsets.all(8.0),
462 | child: Text(
463 | userData.fullName,
464 | style: AppTextStyle.textStyleBold,
465 | ),
466 | ),
467 | const Spacer(),
468 | TapFadeIcon(
469 | onTap: () => context.removeAndShowSnackbar('Not part of the demo'),
470 | icon: Icons.more_horiz,
471 | iconColor: Theme.of(context).iconTheme.color!,
472 | ),
473 | ],
474 | ),
475 | );
476 | }
477 | }
478 |
--------------------------------------------------------------------------------
/lib/components/timeline/widgets/widgets.dart:
--------------------------------------------------------------------------------
1 | export 'post_card.dart';
2 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'app/app.dart';
4 |
5 | void main() {
6 | WidgetsFlutterBinding.ensureInitialized();
7 | final theme = AppTheme();
8 | runApp(StreamagramApp(appTheme: theme));
9 | }
10 |
--------------------------------------------------------------------------------
/previews/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GetStream/flutter-instagram-clone/fce74f7795ae3c18034023dfc87008f9926a3658/previews/preview.png
--------------------------------------------------------------------------------
/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | asn1lib:
5 | dependency: transitive
6 | description:
7 | name: asn1lib
8 | url: "https://pub.dartlang.org"
9 | source: hosted
10 | version: "1.1.0"
11 | async:
12 | dependency: transitive
13 | description:
14 | name: async
15 | url: "https://pub.dartlang.org"
16 | source: hosted
17 | version: "2.9.0"
18 | boolean_selector:
19 | dependency: transitive
20 | description:
21 | name: boolean_selector
22 | url: "https://pub.dartlang.org"
23 | source: hosted
24 | version: "2.1.0"
25 | cached_network_image:
26 | dependency: "direct main"
27 | description:
28 | name: cached_network_image
29 | url: "https://pub.dartlang.org"
30 | source: hosted
31 | version: "3.2.0"
32 | cached_network_image_platform_interface:
33 | dependency: transitive
34 | description:
35 | name: cached_network_image_platform_interface
36 | url: "https://pub.dartlang.org"
37 | source: hosted
38 | version: "1.0.0"
39 | cached_network_image_web:
40 | dependency: transitive
41 | description:
42 | name: cached_network_image_web
43 | url: "https://pub.dartlang.org"
44 | source: hosted
45 | version: "1.0.1"
46 | characters:
47 | dependency: transitive
48 | description:
49 | name: characters
50 | url: "https://pub.dartlang.org"
51 | source: hosted
52 | version: "1.2.1"
53 | charcode:
54 | dependency: transitive
55 | description:
56 | name: charcode
57 | url: "https://pub.dartlang.org"
58 | source: hosted
59 | version: "1.3.1"
60 | clock:
61 | dependency: transitive
62 | description:
63 | name: clock
64 | url: "https://pub.dartlang.org"
65 | source: hosted
66 | version: "1.1.1"
67 | collection:
68 | dependency: transitive
69 | description:
70 | name: collection
71 | url: "https://pub.dartlang.org"
72 | source: hosted
73 | version: "1.16.0"
74 | convert:
75 | dependency: transitive
76 | description:
77 | name: convert
78 | url: "https://pub.dartlang.org"
79 | source: hosted
80 | version: "3.0.1"
81 | cross_file:
82 | dependency: transitive
83 | description:
84 | name: cross_file
85 | url: "https://pub.dartlang.org"
86 | source: hosted
87 | version: "0.3.2"
88 | crypto:
89 | dependency: transitive
90 | description:
91 | name: crypto
92 | url: "https://pub.dartlang.org"
93 | source: hosted
94 | version: "3.0.1"
95 | crypto_keys:
96 | dependency: transitive
97 | description:
98 | name: crypto_keys
99 | url: "https://pub.dartlang.org"
100 | source: hosted
101 | version: "0.3.0"
102 | cupertino_icons:
103 | dependency: "direct main"
104 | description:
105 | name: cupertino_icons
106 | url: "https://pub.dartlang.org"
107 | source: hosted
108 | version: "1.0.4"
109 | dio:
110 | dependency: transitive
111 | description:
112 | name: dio
113 | url: "https://pub.dartlang.org"
114 | source: hosted
115 | version: "4.0.4"
116 | equatable:
117 | dependency: transitive
118 | description:
119 | name: equatable
120 | url: "https://pub.dartlang.org"
121 | source: hosted
122 | version: "2.0.3"
123 | fake_async:
124 | dependency: transitive
125 | description:
126 | name: fake_async
127 | url: "https://pub.dartlang.org"
128 | source: hosted
129 | version: "1.3.1"
130 | faye_dart:
131 | dependency: transitive
132 | description:
133 | name: faye_dart
134 | url: "https://pub.dartlang.org"
135 | source: hosted
136 | version: "0.1.1+2"
137 | ffi:
138 | dependency: transitive
139 | description:
140 | name: ffi
141 | url: "https://pub.dartlang.org"
142 | source: hosted
143 | version: "1.1.2"
144 | file:
145 | dependency: transitive
146 | description:
147 | name: file
148 | url: "https://pub.dartlang.org"
149 | source: hosted
150 | version: "6.1.2"
151 | flutter:
152 | dependency: "direct main"
153 | description: flutter
154 | source: sdk
155 | version: "0.0.0"
156 | flutter_blurhash:
157 | dependency: transitive
158 | description:
159 | name: flutter_blurhash
160 | url: "https://pub.dartlang.org"
161 | source: hosted
162 | version: "0.6.4"
163 | flutter_cache_manager:
164 | dependency: transitive
165 | description:
166 | name: flutter_cache_manager
167 | url: "https://pub.dartlang.org"
168 | source: hosted
169 | version: "3.3.0"
170 | flutter_lints:
171 | dependency: "direct dev"
172 | description:
173 | name: flutter_lints
174 | url: "https://pub.dartlang.org"
175 | source: hosted
176 | version: "2.0.1"
177 | flutter_plugin_android_lifecycle:
178 | dependency: transitive
179 | description:
180 | name: flutter_plugin_android_lifecycle
181 | url: "https://pub.dartlang.org"
182 | source: hosted
183 | version: "2.0.5"
184 | flutter_test:
185 | dependency: "direct dev"
186 | description: flutter
187 | source: sdk
188 | version: "0.0.0"
189 | flutter_web_plugins:
190 | dependency: transitive
191 | description: flutter
192 | source: sdk
193 | version: "0.0.0"
194 | google_fonts:
195 | dependency: "direct main"
196 | description:
197 | name: google_fonts
198 | url: "https://pub.dartlang.org"
199 | source: hosted
200 | version: "2.3.1"
201 | http:
202 | dependency: transitive
203 | description:
204 | name: http
205 | url: "https://pub.dartlang.org"
206 | source: hosted
207 | version: "0.13.4"
208 | http_parser:
209 | dependency: transitive
210 | description:
211 | name: http_parser
212 | url: "https://pub.dartlang.org"
213 | source: hosted
214 | version: "4.0.0"
215 | image_picker:
216 | dependency: "direct main"
217 | description:
218 | name: image_picker
219 | url: "https://pub.dartlang.org"
220 | source: hosted
221 | version: "0.8.4+10"
222 | image_picker_for_web:
223 | dependency: transitive
224 | description:
225 | name: image_picker_for_web
226 | url: "https://pub.dartlang.org"
227 | source: hosted
228 | version: "2.1.6"
229 | image_picker_platform_interface:
230 | dependency: transitive
231 | description:
232 | name: image_picker_platform_interface
233 | url: "https://pub.dartlang.org"
234 | source: hosted
235 | version: "2.4.4"
236 | intl:
237 | dependency: transitive
238 | description:
239 | name: intl
240 | url: "https://pub.dartlang.org"
241 | source: hosted
242 | version: "0.17.0"
243 | jiffy:
244 | dependency: "direct main"
245 | description:
246 | name: jiffy
247 | url: "https://pub.dartlang.org"
248 | source: hosted
249 | version: "5.0.0"
250 | jose:
251 | dependency: transitive
252 | description:
253 | name: jose
254 | url: "https://pub.dartlang.org"
255 | source: hosted
256 | version: "0.3.2"
257 | js:
258 | dependency: transitive
259 | description:
260 | name: js
261 | url: "https://pub.dartlang.org"
262 | source: hosted
263 | version: "0.6.4"
264 | json_annotation:
265 | dependency: transitive
266 | description:
267 | name: json_annotation
268 | url: "https://pub.dartlang.org"
269 | source: hosted
270 | version: "4.4.0"
271 | lints:
272 | dependency: transitive
273 | description:
274 | name: lints
275 | url: "https://pub.dartlang.org"
276 | source: hosted
277 | version: "2.0.0"
278 | logging:
279 | dependency: transitive
280 | description:
281 | name: logging
282 | url: "https://pub.dartlang.org"
283 | source: hosted
284 | version: "1.0.2"
285 | matcher:
286 | dependency: transitive
287 | description:
288 | name: matcher
289 | url: "https://pub.dartlang.org"
290 | source: hosted
291 | version: "0.12.12"
292 | material_color_utilities:
293 | dependency: transitive
294 | description:
295 | name: material_color_utilities
296 | url: "https://pub.dartlang.org"
297 | source: hosted
298 | version: "0.1.5"
299 | meta:
300 | dependency: transitive
301 | description:
302 | name: meta
303 | url: "https://pub.dartlang.org"
304 | source: hosted
305 | version: "1.8.0"
306 | mime:
307 | dependency: transitive
308 | description:
309 | name: mime
310 | url: "https://pub.dartlang.org"
311 | source: hosted
312 | version: "1.0.1"
313 | nested:
314 | dependency: transitive
315 | description:
316 | name: nested
317 | url: "https://pub.dartlang.org"
318 | source: hosted
319 | version: "1.0.0"
320 | octo_image:
321 | dependency: transitive
322 | description:
323 | name: octo_image
324 | url: "https://pub.dartlang.org"
325 | source: hosted
326 | version: "1.0.1"
327 | path:
328 | dependency: transitive
329 | description:
330 | name: path
331 | url: "https://pub.dartlang.org"
332 | source: hosted
333 | version: "1.8.2"
334 | path_provider:
335 | dependency: transitive
336 | description:
337 | name: path_provider
338 | url: "https://pub.dartlang.org"
339 | source: hosted
340 | version: "2.0.9"
341 | path_provider_android:
342 | dependency: transitive
343 | description:
344 | name: path_provider_android
345 | url: "https://pub.dartlang.org"
346 | source: hosted
347 | version: "2.0.12"
348 | path_provider_ios:
349 | dependency: transitive
350 | description:
351 | name: path_provider_ios
352 | url: "https://pub.dartlang.org"
353 | source: hosted
354 | version: "2.0.8"
355 | path_provider_linux:
356 | dependency: transitive
357 | description:
358 | name: path_provider_linux
359 | url: "https://pub.dartlang.org"
360 | source: hosted
361 | version: "2.1.5"
362 | path_provider_macos:
363 | dependency: transitive
364 | description:
365 | name: path_provider_macos
366 | url: "https://pub.dartlang.org"
367 | source: hosted
368 | version: "2.0.5"
369 | path_provider_platform_interface:
370 | dependency: transitive
371 | description:
372 | name: path_provider_platform_interface
373 | url: "https://pub.dartlang.org"
374 | source: hosted
375 | version: "2.0.3"
376 | path_provider_windows:
377 | dependency: transitive
378 | description:
379 | name: path_provider_windows
380 | url: "https://pub.dartlang.org"
381 | source: hosted
382 | version: "2.0.5"
383 | pedantic:
384 | dependency: transitive
385 | description:
386 | name: pedantic
387 | url: "https://pub.dartlang.org"
388 | source: hosted
389 | version: "1.11.1"
390 | platform:
391 | dependency: transitive
392 | description:
393 | name: platform
394 | url: "https://pub.dartlang.org"
395 | source: hosted
396 | version: "3.1.0"
397 | plugin_platform_interface:
398 | dependency: transitive
399 | description:
400 | name: plugin_platform_interface
401 | url: "https://pub.dartlang.org"
402 | source: hosted
403 | version: "2.1.2"
404 | pointycastle:
405 | dependency: transitive
406 | description:
407 | name: pointycastle
408 | url: "https://pub.dartlang.org"
409 | source: hosted
410 | version: "3.5.1"
411 | process:
412 | dependency: transitive
413 | description:
414 | name: process
415 | url: "https://pub.dartlang.org"
416 | source: hosted
417 | version: "4.2.4"
418 | provider:
419 | dependency: "direct main"
420 | description:
421 | name: provider
422 | url: "https://pub.dartlang.org"
423 | source: hosted
424 | version: "6.0.2"
425 | quiver:
426 | dependency: transitive
427 | description:
428 | name: quiver
429 | url: "https://pub.dartlang.org"
430 | source: hosted
431 | version: "3.0.1+1"
432 | rxdart:
433 | dependency: transitive
434 | description:
435 | name: rxdart
436 | url: "https://pub.dartlang.org"
437 | source: hosted
438 | version: "0.27.3"
439 | sky_engine:
440 | dependency: transitive
441 | description: flutter
442 | source: sdk
443 | version: "0.0.99"
444 | source_span:
445 | dependency: transitive
446 | description:
447 | name: source_span
448 | url: "https://pub.dartlang.org"
449 | source: hosted
450 | version: "1.9.0"
451 | sqflite:
452 | dependency: transitive
453 | description:
454 | name: sqflite
455 | url: "https://pub.dartlang.org"
456 | source: hosted
457 | version: "2.0.2"
458 | sqflite_common:
459 | dependency: transitive
460 | description:
461 | name: sqflite_common
462 | url: "https://pub.dartlang.org"
463 | source: hosted
464 | version: "2.2.0"
465 | stack_trace:
466 | dependency: transitive
467 | description:
468 | name: stack_trace
469 | url: "https://pub.dartlang.org"
470 | source: hosted
471 | version: "1.10.0"
472 | stream_channel:
473 | dependency: transitive
474 | description:
475 | name: stream_channel
476 | url: "https://pub.dartlang.org"
477 | source: hosted
478 | version: "2.1.0"
479 | stream_feed:
480 | dependency: transitive
481 | description:
482 | name: stream_feed
483 | url: "https://pub.dartlang.org"
484 | source: hosted
485 | version: "0.6.0+2"
486 | stream_feed_flutter_core:
487 | dependency: "direct main"
488 | description:
489 | name: stream_feed_flutter_core
490 | url: "https://pub.dartlang.org"
491 | source: hosted
492 | version: "0.8.0"
493 | string_scanner:
494 | dependency: transitive
495 | description:
496 | name: string_scanner
497 | url: "https://pub.dartlang.org"
498 | source: hosted
499 | version: "1.1.1"
500 | synchronized:
501 | dependency: transitive
502 | description:
503 | name: synchronized
504 | url: "https://pub.dartlang.org"
505 | source: hosted
506 | version: "3.0.0"
507 | term_glyph:
508 | dependency: transitive
509 | description:
510 | name: term_glyph
511 | url: "https://pub.dartlang.org"
512 | source: hosted
513 | version: "1.2.1"
514 | test_api:
515 | dependency: transitive
516 | description:
517 | name: test_api
518 | url: "https://pub.dartlang.org"
519 | source: hosted
520 | version: "0.4.12"
521 | transparent_image:
522 | dependency: "direct main"
523 | description:
524 | name: transparent_image
525 | url: "https://pub.dartlang.org"
526 | source: hosted
527 | version: "2.0.0"
528 | typed_data:
529 | dependency: transitive
530 | description:
531 | name: typed_data
532 | url: "https://pub.dartlang.org"
533 | source: hosted
534 | version: "1.3.0"
535 | uuid:
536 | dependency: transitive
537 | description:
538 | name: uuid
539 | url: "https://pub.dartlang.org"
540 | source: hosted
541 | version: "3.0.6"
542 | vector_math:
543 | dependency: transitive
544 | description:
545 | name: vector_math
546 | url: "https://pub.dartlang.org"
547 | source: hosted
548 | version: "2.1.2"
549 | web_socket_channel:
550 | dependency: transitive
551 | description:
552 | name: web_socket_channel
553 | url: "https://pub.dartlang.org"
554 | source: hosted
555 | version: "2.1.0"
556 | win32:
557 | dependency: transitive
558 | description:
559 | name: win32
560 | url: "https://pub.dartlang.org"
561 | source: hosted
562 | version: "2.4.1"
563 | x509:
564 | dependency: transitive
565 | description:
566 | name: x509
567 | url: "https://pub.dartlang.org"
568 | source: hosted
569 | version: "0.2.2"
570 | xdg_directories:
571 | dependency: transitive
572 | description:
573 | name: xdg_directories
574 | url: "https://pub.dartlang.org"
575 | source: hosted
576 | version: "0.2.0+1"
577 | sdks:
578 | dart: ">=2.17.0-206.0.dev <3.0.0"
579 | flutter: ">=2.10.0-0"
580 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: stream_agram
2 | description: A new Flutter project.
3 |
4 | # The following line prevents the package from being accidentally published to
5 | # pub.dev using `flutter pub publish`. This is preferred for private packages.
6 | publish_to: "none" # Remove this line if you wish to publish to pub.dev
7 |
8 | # The following defines the version and build number for your application.
9 | # A version number is three numbers separated by dots, like 1.2.43
10 | # followed by an optional build number separated by a +.
11 | # Both the version and the builder number may be overridden in flutter
12 | # build by specifying --build-name and --build-number, respectively.
13 | # In Android, build-name is used as versionName while build-number used as versionCode.
14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning
15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
16 | # Read more about iOS versioning at
17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
18 | version: 1.0.0+1
19 |
20 | environment:
21 | sdk: ">=2.16.1 <3.0.0"
22 |
23 | # Dependencies specify other packages that your package needs in order to work.
24 | # To automatically upgrade your package dependencies to the latest versions
25 | # consider running `flutter pub upgrade --major-versions`. Alternatively,
26 | # dependencies can be manually updated by changing the version numbers below to
27 | # the latest version available on pub.dev. To see which dependencies have newer
28 | # versions available, run `flutter pub outdated`.
29 | dependencies:
30 | flutter:
31 | sdk: flutter
32 |
33 | # The following adds the Cupertino Icons font to your application.
34 | # Use with the CupertinoIcons class for iOS style icons.
35 | # The following adds the Cupertino Icons font to your application.
36 | # Use with the CupertinoIcons class for iOS style icons.
37 | cupertino_icons: ^1.0.2
38 | stream_feed_flutter_core: ^0.8.0
39 | provider: ^6.0.2
40 | google_fonts: ^2.3.1
41 | image_picker: ^0.8.4+9
42 | cached_network_image: ^3.2.0
43 | transparent_image: ^2.0.0
44 | jiffy: ^5.0.0
45 |
46 | dev_dependencies:
47 | flutter_test:
48 | sdk: flutter
49 |
50 | # The "flutter_lints" package below contains a set of recommended lints to
51 | # encourage good coding practices. The lint set provided by the package is
52 | # activated in the `analysis_options.yaml` file located at the root of your
53 | # package. See that file for information about deactivating specific lint
54 | # rules and activating additional ones.
55 | flutter_lints: ^2.0.0
56 |
57 | # For information on the generic Dart part of this file, see the
58 | # following page: https://dart.dev/tools/pub/pubspec
59 |
60 | # The following section is specific to Flutter.
61 | flutter:
62 | # The following line ensures that the Material Icons font is
63 | # included with your application, so that you can use the icons in
64 | # the material Icons class.
65 | uses-material-design: true
66 |
67 | # To add assets to your application, add an assets section, like this:
68 | # assets:
69 | # - images/a_dot_burr.jpeg
70 | # - images/a_dot_ham.jpeg
71 |
72 | # An image asset can refer to one or more resolution-specific "variants", see
73 | # https://flutter.dev/assets-and-images/#resolution-aware.
74 |
75 | # For details regarding adding assets from package dependencies, see
76 | # https://flutter.dev/assets-and-images/#from-packages
77 |
78 | # To add custom fonts to your application, add a fonts section here,
79 | # in this "flutter" section. Each entry in this list should have a
80 | # "family" key with the font family name, and a "fonts" key with a
81 | # list giving the asset and other descriptors for the font. For
82 | # example:
83 | # fonts:
84 | # - family: Schyler
85 | # fonts:
86 | # - asset: fonts/Schyler-Regular.ttf
87 | # - asset: fonts/Schyler-Italic.ttf
88 | # style: italic
89 | # - family: Trajan Pro
90 | # fonts:
91 | # - asset: fonts/TrajanPro.ttf
92 | # - asset: fonts/TrajanPro_Bold.ttf
93 | # weight: 700
94 | #
95 | # For details regarding fonts from package dependencies,
96 | # see https://flutter.dev/custom-fonts/#from-packages
97 |
--------------------------------------------------------------------------------