Skip to content

Commit 359d526

Browse files
authored
✨ Integrate iCloud progress overview in previews (#184)
1 parent 469ff71 commit 359d526

File tree

5 files changed

+205
-87
lines changed

5 files changed

+205
-87
lines changed

lib/src/provider/asset_entity_image_provider.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class AssetEntityImageProvider extends ImageProvider<AssetEntityImageProvider> {
9797
);
9898
}
9999
if (data == null) {
100-
throw AssertionError('Null in entity\'s data.');
100+
throw AssertionError('Null data in entity: $entity');
101101
}
102102
return decode(data);
103103
}

lib/src/widget/builder/image_page_builder.dart

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
66
import 'package:extended_image/extended_image.dart';
77

88
import '../../constants/constants.dart';
9+
import 'locally_available_builder.dart';
910

1011
class ImagePageBuilder extends StatefulWidget {
1112
const ImagePageBuilder({
@@ -33,44 +34,45 @@ class _ImagePageBuilderState extends State<ImagePageBuilder> {
3334
DefaultAssetPickerViewerBuilderDelegate get builder =>
3435
widget.state.builder as DefaultAssetPickerViewerBuilderDelegate;
3536

36-
bool loaded = false;
37-
3837
@override
3938
Widget build(BuildContext context) {
40-
return GestureDetector(
41-
behavior: HitTestBehavior.opaque,
42-
onTap: builder.switchDisplayingDetail,
43-
child: ExtendedImage(
44-
image: AssetEntityImageProvider(
45-
widget.asset,
46-
isOriginal: widget.previewThumbSize == null,
47-
thumbSize: widget.previewThumbSize,
48-
),
49-
fit: BoxFit.contain,
50-
mode: ExtendedImageMode.gesture,
51-
onDoubleTap: builder.updateAnimation,
52-
initGestureConfigHandler: (ExtendedImageState state) {
53-
return GestureConfig(
54-
initialScale: 1.0,
55-
minScale: 1.0,
56-
maxScale: 3.0,
57-
animationMinScale: 0.6,
58-
animationMaxScale: 4.0,
59-
cacheGesture: false,
60-
inPageView: true,
61-
);
62-
},
63-
loadStateChanged: (ExtendedImageState state) {
64-
if (state.extendedImageLoadState == LoadState.completed) {
65-
loaded = true;
66-
}
67-
return builder.previewWidgetLoadStateChanged(
68-
context,
69-
state,
70-
hasLoaded: loaded,
71-
);
72-
},
73-
),
39+
return LocallyAvailableBuilder(
40+
asset: widget.asset,
41+
isOriginal: widget.previewThumbSize == null,
42+
builder: (BuildContext context, AssetEntity asset) {
43+
return GestureDetector(
44+
behavior: HitTestBehavior.opaque,
45+
onTap: builder.switchDisplayingDetail,
46+
child: ExtendedImage(
47+
image: AssetEntityImageProvider(
48+
asset,
49+
isOriginal: widget.previewThumbSize == null,
50+
thumbSize: widget.previewThumbSize,
51+
),
52+
fit: BoxFit.contain,
53+
mode: ExtendedImageMode.gesture,
54+
onDoubleTap: builder.updateAnimation,
55+
initGestureConfigHandler: (ExtendedImageState state) {
56+
return GestureConfig(
57+
initialScale: 1.0,
58+
minScale: 1.0,
59+
maxScale: 3.0,
60+
animationMinScale: 0.6,
61+
animationMaxScale: 4.0,
62+
cacheGesture: false,
63+
inPageView: true,
64+
);
65+
},
66+
loadStateChanged: (ExtendedImageState state) {
67+
return builder.previewWidgetLoadStateChanged(
68+
context,
69+
state,
70+
hasLoaded: state.extendedImageLoadState == LoadState.completed,
71+
);
72+
},
73+
),
74+
);
75+
},
7476
);
7577
}
7678
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
///
2+
/// [Author] Alex (https://github.com/AlexV525)
3+
/// [Date] 2021/7/23 16:07
4+
///
5+
import 'dart:io';
6+
7+
import 'package:flutter/material.dart';
8+
import 'package:photo_manager/photo_manager.dart';
9+
10+
import '../../constants/constants.dart';
11+
import '../../constants/extensions.dart';
12+
import '../scale_text.dart';
13+
14+
class LocallyAvailableBuilder extends StatefulWidget {
15+
const LocallyAvailableBuilder({
16+
Key? key,
17+
required this.asset,
18+
required this.builder,
19+
this.isOriginal = true,
20+
}) : super(key: key);
21+
22+
final AssetEntity asset;
23+
final Widget Function(BuildContext context, AssetEntity asset) builder;
24+
final bool isOriginal;
25+
26+
@override
27+
_LocallyAvailableBuilderState createState() =>
28+
_LocallyAvailableBuilderState();
29+
}
30+
31+
class _LocallyAvailableBuilderState extends State<LocallyAvailableBuilder> {
32+
bool _isLocallyAvailable = false;
33+
PMProgressHandler? _progressHandler;
34+
File? file;
35+
36+
@override
37+
void initState() {
38+
super.initState();
39+
_checkLocallyAvailable();
40+
}
41+
42+
Future<void> _checkLocallyAvailable() async {
43+
_isLocallyAvailable = await widget.asset.isLocallyAvailable;
44+
if (!mounted) {
45+
return;
46+
}
47+
setState(() {});
48+
if (!_isLocallyAvailable) {
49+
_progressHandler = PMProgressHandler();
50+
widget.asset
51+
.loadFile(
52+
progressHandler: _progressHandler,
53+
isOrigin: widget.isOriginal,
54+
)
55+
.then((File? f) => file = f);
56+
}
57+
_progressHandler?.stream.listen((PMProgressState s) {
58+
if (s.state == PMRequestState.success) {
59+
_isLocallyAvailable = true;
60+
file = null;
61+
if (mounted) {
62+
setState(() {});
63+
}
64+
}
65+
});
66+
}
67+
68+
Widget _indicator(BuildContext context) {
69+
return StreamBuilder<PMProgressState>(
70+
stream: _progressHandler!.stream,
71+
initialData: PMProgressState(0, PMRequestState.prepare),
72+
builder: (BuildContext c, AsyncSnapshot<PMProgressState> s) {
73+
if (s.hasData) {
74+
final double progress = s.data!.progress;
75+
final PMRequestState state = s.data!.state;
76+
return Row(
77+
mainAxisSize: MainAxisSize.min,
78+
children: <Widget>[
79+
Icon(
80+
state == PMRequestState.failed
81+
? Icons.cloud_off
82+
: Icons.cloud_queue,
83+
color: context.themeData.iconTheme.color?.withOpacity(.4),
84+
size: 28,
85+
),
86+
if (state != PMRequestState.success &&
87+
state != PMRequestState.failed)
88+
ScaleText(
89+
' iCloud ${(progress * 100).toInt()}%',
90+
style: TextStyle(
91+
color: context.themeData.textTheme.bodyText2?.color
92+
?.withOpacity(.4),
93+
),
94+
),
95+
],
96+
);
97+
}
98+
return const SizedBox.shrink();
99+
},
100+
);
101+
}
102+
103+
@override
104+
Widget build(BuildContext context) {
105+
if (_isLocallyAvailable) {
106+
return widget.builder(context, widget.asset);
107+
}
108+
if (_progressHandler != null) {
109+
return Center(child: _indicator(context));
110+
}
111+
return const SizedBox.shrink();
112+
}
113+
}

lib/src/widget/builder/video_page_builder.dart

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:video_player/video_player.dart';
77

88
import '../../constants/constants.dart';
99
import '../scale_text.dart';
10+
import 'locally_available_builder.dart';
1011

1112
class VideoPageBuilder extends StatefulWidget {
1213
const VideoPageBuilder({
@@ -136,61 +137,63 @@ class _VideoPageBuilderState extends State<VideoPageBuilder> {
136137

137138
@override
138139
Widget build(BuildContext context) {
139-
if (hasErrorWhenInitializing) {
140-
return Center(
141-
child: ScaleText(
142-
Constants.textDelegate.loadFailed,
143-
),
144-
);
145-
}
146-
if (!hasLoaded) {
147-
return const SizedBox.shrink();
148-
}
149-
return Stack(
150-
fit: StackFit.expand,
151-
children: <Widget>[
152-
Positioned.fill(
153-
child: Center(
154-
child: AspectRatio(
155-
aspectRatio: _controller.value.aspectRatio,
156-
child: VideoPlayer(_controller),
157-
),
158-
),
159-
),
160-
if (!widget.hasOnlyOneVideoAndMoment)
161-
ValueListenableBuilder<bool>(
162-
valueListenable: isPlaying,
163-
builder: (_, bool value, __) => GestureDetector(
164-
behavior: HitTestBehavior.opaque,
165-
onTap:
166-
value ? playButtonCallback : builder.switchDisplayingDetail,
140+
return LocallyAvailableBuilder(
141+
asset: widget.asset,
142+
builder: (BuildContext context, AssetEntity asset) {
143+
if (hasErrorWhenInitializing) {
144+
return Center(child: ScaleText(Constants.textDelegate.loadFailed));
145+
}
146+
if (!hasLoaded) {
147+
return const SizedBox.shrink();
148+
}
149+
return Stack(
150+
fit: StackFit.expand,
151+
children: <Widget>[
152+
Positioned.fill(
167153
child: Center(
168-
child: AnimatedOpacity(
169-
duration: kThemeAnimationDuration,
170-
opacity: value ? 0.0 : 1.0,
171-
child: GestureDetector(
172-
onTap: playButtonCallback,
173-
child: DecoratedBox(
174-
decoration: const BoxDecoration(
175-
boxShadow: <BoxShadow>[
176-
BoxShadow(color: Colors.black12)
177-
],
178-
shape: BoxShape.circle,
179-
),
180-
child: Icon(
181-
value
182-
? Icons.pause_circle_outline
183-
: Icons.play_circle_filled,
184-
size: 70.0,
185-
color: Colors.white,
154+
child: AspectRatio(
155+
aspectRatio: _controller.value.aspectRatio,
156+
child: VideoPlayer(_controller),
157+
),
158+
),
159+
),
160+
if (!widget.hasOnlyOneVideoAndMoment)
161+
ValueListenableBuilder<bool>(
162+
valueListenable: isPlaying,
163+
builder: (_, bool value, __) => GestureDetector(
164+
behavior: HitTestBehavior.opaque,
165+
onTap: value
166+
? playButtonCallback
167+
: builder.switchDisplayingDetail,
168+
child: Center(
169+
child: AnimatedOpacity(
170+
duration: kThemeAnimationDuration,
171+
opacity: value ? 0.0 : 1.0,
172+
child: GestureDetector(
173+
onTap: playButtonCallback,
174+
child: DecoratedBox(
175+
decoration: const BoxDecoration(
176+
boxShadow: <BoxShadow>[
177+
BoxShadow(color: Colors.black12)
178+
],
179+
shape: BoxShape.circle,
180+
),
181+
child: Icon(
182+
value
183+
? Icons.pause_circle_outline
184+
: Icons.play_circle_filled,
185+
size: 70.0,
186+
color: Colors.white,
187+
),
188+
),
186189
),
187190
),
188191
),
189192
),
190193
),
191-
),
192-
),
193-
],
194+
],
195+
);
196+
},
194197
);
195198
}
196199
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ dependencies:
1212
sdk: flutter
1313

1414
extended_image: ^4.1.0
15-
photo_manager: ^1.2.6+1
15+
photo_manager: ^1.2.8
1616
provider: ^5.0.0
1717
video_player: ^2.1.6

0 commit comments

Comments
 (0)