Skip to content

Commit ff3eaa8

Browse files
authored
feat(samples): implement fullscreen attachment gallery (#33)
1 parent eaebd26 commit ff3eaa8

16 files changed

+613
-13
lines changed

docs/pubspec.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,4 @@ dev_dependencies:
77
flutter:
88
sdk: flutter
99
flutter_state_notifier: ^1.0.0
10-
stream_feeds:
11-
path: ../packages/stream_feeds
10+
stream_feeds: ^0.1.0

melos.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ command:
2222
auto_route: ^10.0.0
2323
cached_network_image: ^3.4.1
2424
collection: ^1.18.0
25+
chewie: ^1.11.3
2526
dio: ^5.9.0
2627
equatable: ^2.0.5
2728
flutter_state_notifier: ^1.0.0
@@ -35,13 +36,15 @@ command:
3536
http: ^1.1.0
3637
intl: ">=0.18.1 <=0.21.0"
3738
jiffy: ^6.3.2
39+
photo_view: ^0.15.0
3840
json_annotation: ^4.9.0
3941
meta: ^1.9.1
4042
retrofit: ^4.6.0
4143
rxdart: ^0.28.0
4244
shared_preferences: ^2.5.3
4345
state_notifier: ^1.0.0
4446
stream_core: ^0.1.0
47+
video_player: ^2.10.0
4548
uuid: ^4.5.1
4649

4750
# List of all the dev_dependencies used in the project.

pubspec.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,10 @@ packages:
157157
dependency: transitive
158158
description:
159159
name: meta
160-
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
160+
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
161161
url: "https://pub.dev"
162162
source: hosted
163-
version: "1.16.0"
163+
version: "1.17.0"
164164
mustache_template:
165165
dependency: transitive
166166
description:

sample_app/analysis_options.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
include: ../analysis_options.yaml
22

3+
analyzer:
4+
# TODO: not working if added on the root analysis file
5+
exclude:
6+
# exclude all the generated files
7+
- lib/**/*.*.dart
8+
39
linter:
410
rules:
511
cascade_invocations: false

sample_app/android/app/build.gradle.kts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,20 @@ plugins {
88
android {
99
namespace = "io.getstream.feeds.flutter.sample"
1010
compileSdk = flutter.compileSdkVersion
11-
ndkVersion = flutter.ndkVersion
11+
ndkVersion = "27.0.12077973"
1212

1313
compileOptions {
1414
sourceCompatibility = JavaVersion.VERSION_11
1515
targetCompatibility = JavaVersion.VERSION_11
16+
isCoreLibraryDesugaringEnabled = true
1617
}
1718

1819
kotlinOptions {
1920
jvmTarget = JavaVersion.VERSION_11.toString()
2021
}
2122

2223
defaultConfig {
23-
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
2424
applicationId = "io.getstream.feeds.flutter.sample"
25-
// You can update the following values to match your application needs.
26-
// For more information, see: https://flutter.dev/to/review-gradle-config.
2725
minSdk = flutter.minSdkVersion
2826
targetSdk = flutter.targetSdkVersion
2927
versionCode = flutter.versionCode
@@ -42,3 +40,7 @@ android {
4240
flutter {
4341
source = "../.."
4442
}
43+
44+
dependencies {
45+
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
46+
}

sample_app/android/settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pluginManagement {
1919
plugins {
2020
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
2121
id("com.android.application") version "8.7.0" apply false
22-
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
22+
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
2323
}
2424

2525
include(":app")

sample_app/lib/navigation/app_router.dart

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import 'package:auto_route/auto_route.dart';
2+
import 'package:collection/collection.dart';
3+
import 'package:flutter/material.dart';
24
import 'package:injectable/injectable.dart';
5+
import 'package:stream_feeds/stream_feeds.dart';
36

47
import '../screens/choose_user/choose_user_screen.dart';
58
import '../screens/home/home_screen.dart';
6-
79
import '../screens/user_feed/user_feed_screen.dart';
10+
import '../widgets/attachment_gallery/attachment_gallery.dart';
11+
import '../widgets/attachment_gallery/attachment_metadata.dart';
812
import 'guards/auth_guard.dart';
913

1014
part 'app_router.gr.dart';
@@ -41,6 +45,36 @@ class AppRouter extends RootStackRouter {
4145
page: ChooseUserRoute.page,
4246
keepHistory: false,
4347
),
48+
AutoRoute(
49+
path: '/attachment_gallery',
50+
page: AttachmentGalleryRoute.page,
51+
fullscreenDialog: true,
52+
guards: [_authGuard],
53+
),
4454
];
4555
}
4656
}
57+
58+
/// Shell route for attachment gallery
59+
@RoutePage()
60+
class AttachmentGalleryPage extends StatelessWidget {
61+
const AttachmentGalleryPage({
62+
super.key,
63+
required this.attachments,
64+
required this.metadata,
65+
this.initialIndex = 0,
66+
});
67+
68+
final List<Attachment> attachments;
69+
final AttachmentMetadata metadata;
70+
final int initialIndex;
71+
72+
@override
73+
Widget build(BuildContext context) {
74+
return AttachmentGallery(
75+
attachments: attachments,
76+
metadata: metadata,
77+
initialIndex: initialIndex,
78+
);
79+
}
80+
}

sample_app/lib/navigation/app_router.gr.dart

Lines changed: 75 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sample_app/lib/screens/user_feed/feed/user_feed_item.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import 'package:auto_route/auto_route.dart';
12
import 'package:flutter/material.dart';
23
import 'package:stream_feeds/stream_feeds.dart';
34

5+
import '../../../navigation/app_router.dart';
46
import '../../../theme/extensions/theme_extensions.dart';
57
import '../../../utils/date_time_extensions.dart';
68
import '../../../widgets/action_button.dart';
9+
import '../../../widgets/attachment_gallery/attachment_metadata.dart';
710
import '../../../widgets/attachments/attachments.dart';
811
import '../../../widgets/user_avatar.dart';
912

@@ -137,7 +140,19 @@ class _ActivityBody extends StatelessWidget {
137140
AttachmentGrid(
138141
attachments: attachments,
139142
onAttachmentTap: (attachment) {
140-
// TODO: Implement fullscreen attachment view
143+
final initialIndex = attachments.indexOf(attachment);
144+
145+
context.pushRoute(
146+
AttachmentGalleryRoute(
147+
attachments: attachments,
148+
initialIndex: initialIndex >= 0 ? initialIndex : 0,
149+
metadata: AttachmentMetadata(
150+
author: data.user,
151+
createdAt: data.createdAt,
152+
caption: data.text,
153+
),
154+
),
155+
);
141156
},
142157
),
143158
],
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:stream_feeds/stream_feeds.dart';
3+
4+
import '../../theme/theme.dart';
5+
import 'attachment_gallery_chrome.dart';
6+
import 'attachment_gallery_item.dart';
7+
import 'attachment_metadata.dart';
8+
9+
class AttachmentGallery extends StatefulWidget {
10+
const AttachmentGallery({
11+
super.key,
12+
this.initialIndex = 0,
13+
required this.attachments,
14+
required this.metadata,
15+
}) : assert(initialIndex >= 0, 'Initial index must be non-negative');
16+
17+
final int initialIndex;
18+
final List<Attachment> attachments;
19+
final AttachmentMetadata metadata;
20+
21+
@override
22+
State<AttachmentGallery> createState() => _AttachmentGalleryState();
23+
}
24+
25+
class _AttachmentGalleryState extends State<AttachmentGallery> {
26+
late final PageController _pageController;
27+
late final _currentPage = ValueNotifier(widget.initialIndex);
28+
29+
late final _isDisplayingDetail = ValueNotifier<bool>(true);
30+
void switchDisplayingDetail() {
31+
_isDisplayingDetail.value = !_isDisplayingDetail.value;
32+
}
33+
34+
@override
35+
void initState() {
36+
super.initState();
37+
_pageController = PageController(initialPage: _currentPage.value);
38+
}
39+
40+
@override
41+
void dispose() {
42+
_pageController.dispose();
43+
super.dispose();
44+
}
45+
46+
@override
47+
Widget build(BuildContext context) {
48+
return Scaffold(
49+
body: ValueListenableBuilder(
50+
valueListenable: _currentPage,
51+
builder: (context, currentPage, child) {
52+
return Stack(
53+
children: [
54+
if (child case final child?) child,
55+
// Chrome overlay
56+
ValueListenableBuilder<bool>(
57+
valueListenable: _isDisplayingDetail,
58+
builder: (context, isVisible, child) {
59+
return AttachmentGalleryChrome(
60+
isVisible: isVisible,
61+
metadata: widget.metadata,
62+
currentAttachment: widget.attachments[currentPage],
63+
currentIndex: currentPage + 1,
64+
totalCount: widget.attachments.length,
65+
);
66+
},
67+
),
68+
],
69+
);
70+
},
71+
child: InkWell(
72+
onTap: switchDisplayingDetail,
73+
child: PageView.builder(
74+
controller: _pageController,
75+
itemCount: widget.attachments.length,
76+
onPageChanged: (page) => _currentPage.value = page,
77+
itemBuilder: (context, index) {
78+
final attachment = widget.attachments[index];
79+
return ValueListenableBuilder(
80+
valueListenable: _isDisplayingDetail,
81+
builder: (context, displayingDetail, child) {
82+
final padding = MediaQuery.paddingOf(context);
83+
84+
return AnimatedContainer(
85+
duration: kThemeAnimationDuration,
86+
color: switch (displayingDetail) {
87+
true => context.appColors.appBg,
88+
false => AppColorTokens.black,
89+
},
90+
padding: EdgeInsetsDirectional.only(
91+
top: padding.top + kToolbarHeight,
92+
bottom: padding.bottom + kToolbarHeight,
93+
),
94+
child: child,
95+
);
96+
},
97+
child: AttachmentGalleryItem(attachment: attachment),
98+
);
99+
},
100+
),
101+
),
102+
),
103+
);
104+
}
105+
}

0 commit comments

Comments
 (0)