Reason Pun

clear and repair

Showing 100 changed files with 826 additions and 380 deletions
...@@ -50,7 +50,7 @@ android { ...@@ -50,7 +50,7 @@ android {
50 defaultConfig { 50 defaultConfig {
51 // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 51 // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
52 applicationId "com.mofunsky.one_poem" 52 applicationId "com.mofunsky.one_poem"
53 - minSdkVersion 20 53 + minSdkVersion 21
54 targetSdkVersion 30 54 targetSdkVersion 30
55 versionCode flutterVersionCode.toInteger() 55 versionCode flutterVersionCode.toInteger()
56 versionName flutterVersionName 56 versionName flutterVersionName
......
...@@ -17,19 +17,6 @@ ...@@ -17,19 +17,6 @@
17 the Android process has started. This theme is visible to the user 17 the Android process has started. This theme is visible to the user
18 while the Flutter UI initializes. After that, this theme continues 18 while the Flutter UI initializes. After that, this theme continues
19 to determine the Window background behind the Flutter UI. --> 19 to determine the Window background behind the Flutter UI. -->
20 - <meta-data
21 - android:name="io.flutter.embedding.android.NormalTheme"
22 - android:resource="@style/NormalTheme"
23 - />
24 - <!-- Displays an Android View that continues showing the launch screen
25 - Drawable until Flutter paints its first frame, then this splash
26 - screen fades out. A splash screen is useful to avoid any visual
27 - gap between the end of Android's launch screen and the painting of
28 - Flutter's first frame. -->
29 - <meta-data
30 - android:name="io.flutter.embedding.android.SplashScreenDrawable"
31 - android:resource="@drawable/launch_background"
32 - />
33 <intent-filter> 20 <intent-filter>
34 <action android:name="android.intent.action.MAIN"/> 21 <action android:name="android.intent.action.MAIN"/>
35 <category android:name="android.intent.category.LAUNCHER"/> 22 <category android:name="android.intent.category.LAUNCHER"/>
......

72.3 KB | W: | H:

22.4 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

72.3 KB | W: | H:

22.4 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

2.63 KB | W: | H:

2.63 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

1.67 KB | W: | H:

1.67 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

3.59 KB | W: | H:

3.59 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

5.53 KB | W: | H:

5.5 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

7.26 KB | W: | H:

7.21 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

122 KB | W: | H:

41.2 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

190 KB | W: | H:

64.6 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

202 KB | W: | H:

71.4 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

13 KB | W: | H:

4.19 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

888 KB | W: | H:

231 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

72.3 KB | W: | H:

22.4 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin

611 Bytes | W: | H:

610 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin

938 Bytes | W: | H:

939 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
...@@ -3,7 +3,7 @@ import 'dart:ui'; ...@@ -3,7 +3,7 @@ import 'dart:ui';
3 import 'package:flutter/cupertino.dart'; 3 import 'package:flutter/cupertino.dart';
4 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
5 import 'package:one_poem/poem/widgets/poem_content.dart'; 5 import 'package:one_poem/poem/widgets/poem_content.dart';
6 -import 'package:one_poem/recorder/widgets/poem_voice_widget.dart'; 6 +import 'package:one_poem/recorder/audio/widgets/poem_voice_widget.dart';
7 import 'package:one_poem/routers/fluro_navigator.dart'; 7 import 'package:one_poem/routers/fluro_navigator.dart';
8 import 'package:one_poem/util/toast_utils.dart'; 8 import 'package:one_poem/util/toast_utils.dart';
9 import 'package:one_poem/widgets/bars/home_action_bar.dart'; 9 import 'package:one_poem/widgets/bars/home_action_bar.dart';
......
1 -// Copyright 2013 The Flutter Authors. All rights reserved.
2 -// Use of this source code is governed by a BSD-style license that can be
3 -// found in the LICENSE file.
4 -
5 -// ignore_for_file: public_member_api_docs
6 -
7 -import 'dart:async';
8 import 'dart:io'; 1 import 'dart:io';
9 2
10 -import 'package:flutter/foundation.dart'; 3 +import 'package:camera/camera.dart';
11 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
12 -import 'package:image_picker/image_picker.dart'; 5 +import 'package:flutter/services.dart';
13 -import 'package:one_poem/widgets/my_app_bar.dart'; 6 +import 'package:one_poem/recorder/video/preview_screen.dart';
7 +import 'package:one_poem/util/toast_utils.dart';
8 +import 'package:path_provider/path_provider.dart';
14 import 'package:video_player/video_player.dart'; 9 import 'package:video_player/video_player.dart';
15 10
16 class PoemRecordVideoPage extends StatefulWidget { 11 class PoemRecordVideoPage extends StatefulWidget {
17 - const PoemRecordVideoPage({Key? key, this.title}) : super(key: key); 12 + const PoemRecordVideoPage({Key? key}) : super(key: key);
18 -
19 - final String? title;
20 13
21 @override 14 @override
22 _PoemRecordVideoPageState createState() => _PoemRecordVideoPageState(); 15 _PoemRecordVideoPageState createState() => _PoemRecordVideoPageState();
23 } 16 }
24 17
25 -class _PoemRecordVideoPageState extends State<PoemRecordVideoPage> { 18 +class _PoemRecordVideoPageState extends State<PoemRecordVideoPage>
26 - List<XFile>? _imageFileList; 19 + with WidgetsBindingObserver {
20 + CameraController? controller;
21 + VideoPlayerController? videoController;
22 +
23 + File? _imageFile;
24 + File? _videoFile;
25 +
26 + // Initial values
27 + bool _isCameraInitialized = false;
28 + bool _isRearCameraSelected = true;
29 + bool _isVideoCameraSelected = false;
30 + bool _isRecordingInProgress = false;
31 + double _minAvailableExposureOffset = 0.0;
32 + double _maxAvailableExposureOffset = 0.0;
33 + double _minAvailableZoom = 1.0;
34 + double _maxAvailableZoom = 1.0;
35 +
36 + // Current values
37 + double _currentZoomLevel = 1.0;
38 + double _currentExposureOffset = 0.0;
39 + FlashMode? _currentFlashMode;
40 +
41 + List<File> allFileList = [];
42 +
43 + final resolutionPresets = ResolutionPreset.values;
44 +
45 + ResolutionPreset currentResolutionPreset = ResolutionPreset.high;
27 46
28 - set _imageFile(XFile? value) { 47 + List<CameraDescription>? cameras = [];
29 - _imageFileList = value == null ? null : [value]; 48 + @override
49 + void initState() {
50 + // Hide the status bar in Android
51 + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
52 + initCameras().then((value) {
53 + cameras = value;
54 + if (cameras != null) {
55 + onNewCameraSelected(cameras![0]);
56 + refreshAlreadyCapturedImages();
57 + } else {
58 + Toast.show("摄像头出错啦~");
59 + }
60 + });
61 + super.initState();
30 } 62 }
31 63
32 - dynamic _pickImageError; 64 + Future<List<CameraDescription>?> initCameras() async {
33 - bool isVideo = false; 65 + try {
66 + WidgetsFlutterBinding.ensureInitialized();
67 + List<CameraDescription> cameras = await availableCameras();
68 + return cameras;
69 + } on CameraException catch (e) {
70 + return null;
71 + }
72 + }
34 73
35 - VideoPlayerController? _controller; 74 + refreshAlreadyCapturedImages() async {
36 - VideoPlayerController? _toBeDisposed; 75 + final directory = await getApplicationDocumentsDirectory();
37 - String? _retrieveDataError; 76 + List<FileSystemEntity> fileList = await directory.list().toList();
77 + allFileList.clear();
78 + List<Map<int, dynamic>> fileNames = [];
38 79
39 - final ImagePicker _picker = ImagePicker(); 80 + for (var file in fileList) {
40 - final TextEditingController maxWidthController = TextEditingController(); 81 + if (file.path.contains('.jpg') || file.path.contains('.mp4')) {
41 - final TextEditingController maxHeightController = TextEditingController(); 82 + allFileList.add(File(file.path));
42 - final TextEditingController qualityController = TextEditingController(); 83 +
84 + String name = file.path.split('/').last.split('.').first;
85 + fileNames.add({0: int.parse(name), 1: file.path.split('/').last});
86 + }
87 + }
43 88
44 - Future<void> _playVideo(XFile? file) async { 89 + if (fileNames.isNotEmpty) {
45 - if (file != null && mounted) { 90 + final recentFile =
46 - await _disposeVideoController(); 91 + fileNames.reduce((curr, next) => curr[0] > next[0] ? curr : next);
47 - late VideoPlayerController controller; 92 + String recentFileName = recentFile[1];
48 - if (kIsWeb) { 93 + if (recentFileName.contains('.mp4')) {
49 - controller = VideoPlayerController.network(file.path); 94 + _videoFile = File('${directory.path}/$recentFileName');
95 + _imageFile = null;
96 + _startVideoPlayer();
50 } else { 97 } else {
51 - controller = VideoPlayerController.file(File(file.path)); 98 + _imageFile = File('${directory.path}/$recentFileName');
52 - } 99 + _videoFile = null;
53 - _controller = controller; 100 + }
54 - // In web, most browsers won't honor a programmatic call to .play 101 +
55 - // if the video has a sound track (and is not muted).
56 - // Mute the video so it auto-plays in web!
57 - // This is not needed if the call to .play is the result of user
58 - // interaction (clicking on a "play" button, for example).
59 - final double volume = kIsWeb ? 0.0 : 1.0;
60 - await controller.setVolume(volume);
61 - await controller.initialize();
62 - await controller.setLooping(true);
63 - await controller.play();
64 setState(() {}); 102 setState(() {});
65 } 103 }
66 } 104 }
67 105
68 - void _onImageButtonPressed(ImageSource source, 106 + Future<XFile?> takePicture() async {
69 - {BuildContext? context, bool isMultiImage = false}) async { 107 + final CameraController? cameraController = controller;
70 - if (_controller != null) { 108 +
71 - await _controller!.setVolume(0.0); 109 + if (cameraController!.value.isTakingPicture) {
110 + // A capture is already pending, do nothing.
111 + return null;
72 } 112 }
73 - if (isVideo) { 113 +
74 - final XFile? file = await _picker.pickVideo(
75 - source: source, maxDuration: const Duration(seconds: 10));
76 - await _playVideo(file);
77 - } else if (isMultiImage) {
78 - await _displayPickImageDialog(context!,
79 - (double? maxWidth, double? maxHeight, int? quality) async {
80 try { 114 try {
81 - final pickedFileList = await _picker.pickMultiImage( 115 + XFile file = await cameraController.takePicture();
82 - maxWidth: maxWidth, 116 + return file;
83 - maxHeight: maxHeight, 117 + } on CameraException catch (e) {
84 - imageQuality: quality, 118 + return null;
85 - );
86 - setState(() {
87 - _imageFileList = pickedFileList;
88 - });
89 - } catch (e) {
90 - setState(() {
91 - _pickImageError = e;
92 - });
93 } 119 }
120 + }
121 +
122 + Future<void> _startVideoPlayer() async {
123 + if (_videoFile != null) {
124 + videoController = VideoPlayerController.file(_videoFile!);
125 + await videoController!.initialize().then((_) {
126 + // Ensure the first frame is shown after the video is initialized,
127 + // even before the play button has been pressed.
128 + setState(() {});
94 }); 129 });
95 - } else { 130 + await videoController!.setLooping(true);
96 - await _displayPickImageDialog(context!, 131 + await videoController!.play();
97 - (double? maxWidth, double? maxHeight, int? quality) async { 132 + }
133 + }
134 +
135 + Future<void> startVideoRecording() async {
136 + final CameraController? cameraController = controller;
137 +
138 + if (controller!.value.isRecordingVideo) {
139 + // A recording has already started, do nothing.
140 + return;
141 + }
142 +
98 try { 143 try {
99 - final pickedFile = await _picker.pickImage( 144 + await cameraController!.startVideoRecording();
100 - source: source,
101 - maxWidth: maxWidth,
102 - maxHeight: maxHeight,
103 - imageQuality: quality,
104 - );
105 - setState(() {
106 - _imageFile = pickedFile;
107 - });
108 - } catch (e) {
109 setState(() { 145 setState(() {
110 - _pickImageError = e; 146 + _isRecordingInProgress = true;
111 }); 147 });
148 + } on CameraException catch (e) {
149 + // print('Error starting to record video: $e');
112 } 150 }
113 - });
114 } 151 }
152 +
153 + Future<XFile?> stopVideoRecording() async {
154 + if (!controller!.value.isRecordingVideo) {
155 + // Recording is already is stopped state
156 + return null;
115 } 157 }
116 158
117 - @override 159 + try {
118 - void deactivate() { 160 + XFile file = await controller!.stopVideoRecording();
119 - if (_controller != null) { 161 + setState(() {
120 - _controller!.setVolume(0.0); 162 + _isRecordingInProgress = false;
121 - _controller!.pause(); 163 + });
164 + return file;
165 + } on CameraException catch (e) {
166 + return null;
122 } 167 }
123 - super.deactivate();
124 } 168 }
125 169
126 - @override 170 + Future<void> pauseVideoRecording() async {
127 - void dispose() { 171 + if (!controller!.value.isRecordingVideo) {
128 - _disposeVideoController(); 172 + // Video recording is not in progress
129 - maxWidthController.dispose(); 173 + return;
130 - maxHeightController.dispose();
131 - qualityController.dispose();
132 - super.dispose();
133 } 174 }
134 175
135 - Future<void> _disposeVideoController() async { 176 + try {
136 - if (_toBeDisposed != null) { 177 + await controller!.pauseVideoRecording();
137 - await _toBeDisposed!.dispose(); 178 + } on CameraException catch (e) {
179 + // print('Error pausing video recording: $e');
138 } 180 }
139 - _toBeDisposed = _controller;
140 - _controller = null;
141 } 181 }
142 182
143 - Widget _previewVideo() { 183 + Future<void> resumeVideoRecording() async {
144 - final Text? retrieveError = _getRetrieveErrorWidget(); 184 + if (!controller!.value.isRecordingVideo) {
145 - if (retrieveError != null) { 185 + // No video recording was in progress
146 - return retrieveError; 186 + return;
147 } 187 }
148 - if (_controller == null) { 188 +
149 - return const Text( 189 + try {
150 - 'You have not yet picked a video', 190 + await controller!.resumeVideoRecording();
151 - textAlign: TextAlign.center, 191 + } on CameraException catch (e) {
152 - ); 192 + // print('Error resuming video recording: $e');
153 } 193 }
154 - return Padding(
155 - padding: const EdgeInsets.all(10.0),
156 - child: AspectRatioVideo(_controller),
157 - );
158 } 194 }
159 195
160 - Widget _previewImages() { 196 + void resetCameraValues() async {
161 - final Text? retrieveError = _getRetrieveErrorWidget(); 197 + _currentZoomLevel = 1.0;
162 - if (retrieveError != null) { 198 + _currentExposureOffset = 0.0;
163 - return retrieveError;
164 - }
165 - if (_imageFileList != null) {
166 - return Semantics(
167 - child: ListView.builder(
168 - key: UniqueKey(),
169 - itemBuilder: (context, index) {
170 - // Why network for web?
171 - // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform
172 - return Semantics(
173 - label: 'image_picker_example_picked_image',
174 - child: kIsWeb
175 - ? Image.network(_imageFileList![index].path)
176 - : Image.file(File(_imageFileList![index].path)),
177 - );
178 - },
179 - itemCount: _imageFileList!.length,
180 - ),
181 - label: 'image_picker_example_picked_images');
182 - } else if (_pickImageError != null) {
183 - return Text(
184 - 'Pick image error: $_pickImageError',
185 - textAlign: TextAlign.center,
186 - );
187 - } else {
188 - return const Text(
189 - 'You have not yet picked an image.',
190 - textAlign: TextAlign.center,
191 - );
192 } 199 }
200 +
201 + void onNewCameraSelected(CameraDescription cameraDescription) async {
202 + final previousCameraController = controller;
203 +
204 + final CameraController cameraController = CameraController(
205 + cameraDescription,
206 + currentResolutionPreset,
207 + imageFormatGroup: ImageFormatGroup.jpeg,
208 + );
209 +
210 + await previousCameraController?.dispose();
211 +
212 + resetCameraValues();
213 +
214 + if (mounted) {
215 + setState(() {
216 + controller = cameraController;
217 + });
193 } 218 }
194 219
195 - Widget _handlePreview() { 220 + // Update UI if controller updated
196 - if (isVideo) { 221 + cameraController.addListener(() {
197 - return _previewVideo(); 222 + if (mounted) setState(() {});
198 - } else { 223 + });
199 - return _previewImages(); 224 +
225 + try {
226 + await cameraController.initialize();
227 + await Future.wait([
228 + cameraController
229 + .getMinExposureOffset()
230 + .then((value) => _minAvailableExposureOffset = value),
231 + cameraController
232 + .getMaxExposureOffset()
233 + .then((value) => _maxAvailableExposureOffset = value),
234 + cameraController
235 + .getMaxZoomLevel()
236 + .then((value) => _maxAvailableZoom = value),
237 + cameraController
238 + .getMinZoomLevel()
239 + .then((value) => _minAvailableZoom = value),
240 + ]);
241 +
242 + _currentFlashMode = controller!.value.flashMode;
243 + } on CameraException catch (e) {
244 + // print('Error initializing camera: $e');
245 + }
246 +
247 + if (mounted) {
248 + setState(() {
249 + _isCameraInitialized = controller!.value.isInitialized;
250 + });
200 } 251 }
201 } 252 }
202 253
203 - Future<void> retrieveLostData() async { 254 + @override
204 - final LostDataResponse response = await _picker.retrieveLostData(); 255 + void didChangeAppLifecycleState(AppLifecycleState state) {
205 - if (response.isEmpty) { 256 + final CameraController? cameraController = controller;
257 +
258 + // App state changed before we got the chance to initialize.
259 + if (cameraController == null || !cameraController.value.isInitialized) {
206 return; 260 return;
207 } 261 }
208 - if (response.file != null) { 262 +
209 - if (response.type == RetrieveType.video) { 263 + if (state == AppLifecycleState.inactive) {
210 - isVideo = true; 264 + cameraController.dispose();
211 - await _playVideo(response.file); 265 + } else if (state == AppLifecycleState.resumed) {
212 - } else { 266 + onNewCameraSelected(cameraController.description);
213 - isVideo = false;
214 - setState(() {
215 - _imageFile = response.file;
216 - _imageFileList = response.files;
217 - });
218 } 267 }
219 - } else {
220 - _retrieveDataError = response.exception!.code;
221 } 268 }
269 +
270 + @override
271 + void dispose() {
272 + controller?.dispose();
273 + videoController?.dispose();
274 + super.dispose();
222 } 275 }
223 276
224 @override 277 @override
225 Widget build(BuildContext context) { 278 Widget build(BuildContext context) {
226 - return Scaffold( 279 + return SafeArea(
227 - appBar: const MyAppBar( 280 + child: Scaffold(
228 - isBack: true, 281 + backgroundColor: Colors.black,
229 - isTransparent: true, 282 + body: _isCameraInitialized
230 - ), 283 + ? Column(
231 - body: Center( 284 + children: [
232 - child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android 285 + AspectRatio(
233 - ? FutureBuilder<void>( 286 + aspectRatio: 1 / controller!.value.aspectRatio,
234 - future: retrieveLostData(), 287 + child: Stack(
235 - builder: (BuildContext context, AsyncSnapshot<void> snapshot) { 288 + children: [
236 - switch (snapshot.connectionState) { 289 + controller!.buildPreview(),
237 - case ConnectionState.none: 290 + Padding(
238 - case ConnectionState.waiting: 291 + padding: const EdgeInsets.fromLTRB(
239 - return const Text( 292 + 16.0,
240 - 'You have not yet picked an image.', 293 + 8.0,
241 - textAlign: TextAlign.center, 294 + 16.0,
242 - ); 295 + 8.0,
243 - case ConnectionState.done: 296 + ),
244 - return _handlePreview(); 297 + child: Column(
245 - default: 298 + crossAxisAlignment: CrossAxisAlignment.end,
246 - if (snapshot.hasError) { 299 + children: [
247 - return Text( 300 + Align(
248 - 'Pick image/video error: ${snapshot.error}}', 301 + alignment: Alignment.topRight,
249 - textAlign: TextAlign.center, 302 + child: Container(
250 - ); 303 + decoration: BoxDecoration(
251 - } else { 304 + color: Colors.black87,
252 - return const Text( 305 + borderRadius: BorderRadius.circular(10.0),
253 - 'You have not yet picked an image.', 306 + ),
254 - textAlign: TextAlign.center, 307 + child: Padding(
255 - ); 308 + padding: const EdgeInsets.only(
256 - } 309 + left: 8.0,
257 - } 310 + right: 8.0,
258 - }, 311 + ),
312 + child: DropdownButton<ResolutionPreset>(
313 + dropdownColor: Colors.black87,
314 + underline: Container(),
315 + value: currentResolutionPreset,
316 + items: [
317 + for (ResolutionPreset preset
318 + in resolutionPresets)
319 + DropdownMenuItem(
320 + child: Text(
321 + preset
322 + .toString()
323 + .split('.')[1]
324 + .toUpperCase(),
325 + style: const TextStyle(
326 + color: Colors.white),
327 + ),
328 + value: preset,
259 ) 329 )
260 - : _handlePreview(), 330 + ],
261 - ), 331 + onChanged: (value) {
262 - floatingActionButton: Column( 332 + setState(() {
263 - mainAxisAlignment: MainAxisAlignment.end, 333 + currentResolutionPreset = value!;
264 - children: <Widget>[ 334 + _isCameraInitialized = false;
265 - Semantics( 335 + });
266 - label: 'image_picker_example_from_gallery', 336 + onNewCameraSelected(
267 - child: FloatingActionButton( 337 + controller!.description);
268 - onPressed: () {
269 - isVideo = false;
270 - _onImageButtonPressed(ImageSource.gallery, context: context);
271 }, 338 },
272 - heroTag: 'image0', 339 + hint: const Text("Select item"),
273 - tooltip: 'Pick Image from gallery',
274 - child: const Icon(Icons.photo),
275 ), 340 ),
276 ), 341 ),
277 - Padding(
278 - padding: const EdgeInsets.only(top: 16.0),
279 - child: FloatingActionButton(
280 - onPressed: () {
281 - isVideo = false;
282 - _onImageButtonPressed(
283 - ImageSource.gallery,
284 - context: context,
285 - isMultiImage: true,
286 - );
287 - },
288 - heroTag: 'image1',
289 - tooltip: 'Pick Multiple Image from gallery',
290 - child: const Icon(Icons.photo_library),
291 ), 342 ),
292 ), 343 ),
344 + // Spacer(),
293 Padding( 345 Padding(
294 - padding: const EdgeInsets.only(top: 16.0), 346 + padding: const EdgeInsets.only(
295 - child: FloatingActionButton( 347 + right: 8.0, top: 16.0),
296 - onPressed: () { 348 + child: Container(
297 - isVideo = false; 349 + decoration: BoxDecoration(
298 - _onImageButtonPressed(ImageSource.camera, context: context); 350 + color: Colors.white,
351 + borderRadius: BorderRadius.circular(10.0),
352 + ),
353 + child: Padding(
354 + padding: const EdgeInsets.all(8.0),
355 + child: Text(
356 + _currentExposureOffset
357 + .toStringAsFixed(1) +
358 + 'x',
359 + style:
360 + const TextStyle(color: Colors.black),
361 + ),
362 + ),
363 + ),
364 + ),
365 + Expanded(
366 + child: RotatedBox(
367 + quarterTurns: 3,
368 + child: SizedBox(
369 + height: 30,
370 + child: Slider(
371 + value: _currentExposureOffset,
372 + min: _minAvailableExposureOffset,
373 + max: _maxAvailableExposureOffset,
374 + activeColor: Colors.white,
375 + inactiveColor: Colors.white30,
376 + onChanged: (value) async {
377 + setState(() {
378 + _currentExposureOffset = value;
379 + });
380 + await controller!
381 + .setExposureOffset(value);
299 }, 382 },
300 - heroTag: 'image2',
301 - tooltip: 'Take a Photo',
302 - child: const Icon(Icons.camera_alt),
303 ), 383 ),
304 ), 384 ),
305 - Padding( 385 + ),
306 - padding: const EdgeInsets.only(top: 16.0), 386 + ),
307 - child: FloatingActionButton( 387 + Row(
308 - backgroundColor: Colors.red, 388 + children: [
309 - onPressed: () { 389 + Expanded(
310 - isVideo = true; 390 + child: Slider(
311 - _onImageButtonPressed(ImageSource.gallery); 391 + value: _currentZoomLevel,
392 + min: _minAvailableZoom,
393 + max: _maxAvailableZoom,
394 + activeColor: Colors.white,
395 + inactiveColor: Colors.white30,
396 + onChanged: (value) async {
397 + setState(() {
398 + _currentZoomLevel = value;
399 + });
400 + await controller!.setZoomLevel(value);
312 }, 401 },
313 - heroTag: 'video0',
314 - tooltip: 'Pick Video from gallery',
315 - child: const Icon(Icons.video_library),
316 ), 402 ),
317 ), 403 ),
318 Padding( 404 Padding(
319 - padding: const EdgeInsets.only(top: 16.0), 405 + padding: const EdgeInsets.only(right: 8.0),
320 - child: FloatingActionButton( 406 + child: Container(
321 - backgroundColor: Colors.red, 407 + decoration: BoxDecoration(
322 - onPressed: () { 408 + color: Colors.black87,
323 - isVideo = true; 409 + borderRadius:
324 - _onImageButtonPressed(ImageSource.camera); 410 + BorderRadius.circular(10.0),
325 - }, 411 + ),
326 - heroTag: 'video1', 412 + child: Padding(
327 - tooltip: 'Take a Video', 413 + padding: const EdgeInsets.all(8.0),
328 - child: const Icon(Icons.videocam), 414 + child: Text(
415 + _currentZoomLevel.toStringAsFixed(1) +
416 + 'x',
417 + style: const TextStyle(
418 + color: Colors.white),
419 + ),
420 + ),
329 ), 421 ),
330 ), 422 ),
331 ], 423 ],
332 ), 424 ),
333 - ); 425 + Row(
334 - } 426 + mainAxisAlignment:
335 - 427 + MainAxisAlignment.spaceBetween,
336 - Text? _getRetrieveErrorWidget() { 428 + children: [
337 - if (_retrieveDataError != null) { 429 + InkWell(
338 - final Text result = Text(_retrieveDataError!); 430 + onTap: _isRecordingInProgress
339 - _retrieveDataError = null; 431 + ? () async {
340 - return result; 432 + if (controller!
433 + .value.isRecordingPaused) {
434 + await resumeVideoRecording();
435 + } else {
436 + await pauseVideoRecording();
341 } 437 }
342 - return null;
343 } 438 }
344 - 439 + : () {
345 - Future<void> _displayPickImageDialog( 440 + setState(() {
346 - BuildContext context, OnPickImageCallback onPick) async { 441 + _isCameraInitialized = false;
347 - return showDialog( 442 + });
348 - context: context, 443 + onNewCameraSelected(cameras![
349 - builder: (context) { 444 + _isRearCameraSelected ? 1 : 0]);
350 - return AlertDialog( 445 + setState(() {
351 - title: const Text('Add optional parameters'), 446 + _isRearCameraSelected =
352 - content: Column( 447 + !_isRearCameraSelected;
353 - children: <Widget>[ 448 + });
354 - TextField( 449 + },
355 - controller: maxWidthController, 450 + child: Stack(
356 - keyboardType: 451 + alignment: Alignment.center,
357 - const TextInputType.numberWithOptions(decimal: true), 452 + children: [
358 - decoration: const InputDecoration( 453 + const Icon(
359 - hintText: "Enter maxWidth if desired"), 454 + Icons.circle,
360 - ), 455 + color: Colors.black38,
361 - TextField( 456 + size: 60,
362 - controller: maxHeightController, 457 + ),
363 - keyboardType: 458 + _isRecordingInProgress
364 - const TextInputType.numberWithOptions(decimal: true), 459 + ? controller!
365 - decoration: const InputDecoration( 460 + .value.isRecordingPaused
366 - hintText: "Enter maxHeight if desired"), 461 + ? const Icon(
367 - ), 462 + Icons.play_arrow,
368 - TextField( 463 + color: Colors.white,
369 - controller: qualityController, 464 + size: 30,
370 - keyboardType: TextInputType.number, 465 + )
371 - decoration: const InputDecoration( 466 + : const Icon(
372 - hintText: "Enter quality if desired"), 467 + Icons.pause,
468 + color: Colors.white,
469 + size: 30,
470 + )
471 + : Icon(
472 + _isRearCameraSelected
473 + ? Icons.camera_front
474 + : Icons.camera_rear,
475 + color: Colors.white,
476 + size: 30,
373 ), 477 ),
374 ], 478 ],
375 ), 479 ),
376 - actions: <Widget>[
377 - TextButton(
378 - child: const Text('CANCEL'),
379 - onPressed: () {
380 - Navigator.of(context).pop();
381 - },
382 ), 480 ),
383 - TextButton( 481 + InkWell(
384 - child: const Text('PICK'), 482 + onTap: _isVideoCameraSelected
385 - onPressed: () { 483 + ? () async {
386 - double? width = maxWidthController.text.isNotEmpty 484 + if (_isRecordingInProgress) {
387 - ? double.parse(maxWidthController.text) 485 + XFile? rawVideo =
388 - : null; 486 + await stopVideoRecording();
389 - double? height = maxHeightController.text.isNotEmpty 487 + File videoFile =
390 - ? double.parse(maxHeightController.text) 488 + File(rawVideo!.path);
391 - : null; 489 +
392 - int? quality = qualityController.text.isNotEmpty 490 + int currentUnix = DateTime.now()
393 - ? int.parse(qualityController.text) 491 + .millisecondsSinceEpoch;
394 - : null; 492 +
395 - onPick(width, height, quality); 493 + final directory =
396 - Navigator.of(context).pop(); 494 + await getApplicationDocumentsDirectory();
397 - }), 495 +
398 - ], 496 + String fileFormat = videoFile.path
497 + .split('.')
498 + .last;
499 +
500 + _videoFile = await videoFile.copy(
501 + '${directory.path}/$currentUnix.$fileFormat',
399 ); 502 );
400 - });
401 - }
402 -}
403 -
404 -typedef OnPickImageCallback = void Function(
405 - double? maxWidth, double? maxHeight, int? quality);
406 503
407 -class AspectRatioVideo extends StatefulWidget { 504 + _startVideoPlayer();
408 - const AspectRatioVideo(this.controller, {Key? key}) : super(key: key); 505 + } else {
506 + await startVideoRecording();
507 + }
508 + }
509 + : () async {
510 + XFile? rawImage =
511 + await takePicture();
512 + File imageFile =
513 + File(rawImage!.path);
409 514
410 - final VideoPlayerController? controller; 515 + int currentUnix = DateTime.now()
516 + .millisecondsSinceEpoch;
411 517
412 - @override 518 + final directory =
413 - AspectRatioVideoState createState() => AspectRatioVideoState(); 519 + await getApplicationDocumentsDirectory();
414 -}
415 520
416 -class AspectRatioVideoState extends State<AspectRatioVideo> { 521 + String fileFormat =
417 - VideoPlayerController? get controller => widget.controller; 522 + imageFile.path.split('.').last;
418 - bool initialized = false; 523 + await imageFile.copy(
524 + '${directory.path}/$currentUnix.$fileFormat',
525 + );
419 526
420 - void _onVideoControllerUpdate() { 527 + refreshAlreadyCapturedImages();
421 - if (!mounted) { 528 + },
422 - return; 529 + child: Stack(
423 - } 530 + alignment: Alignment.center,
424 - if (initialized != controller!.value.isInitialized) { 531 + children: [
425 - initialized = controller!.value.isInitialized; 532 + Icon(
426 - setState(() {}); 533 + Icons.circle,
427 - } 534 + color: _isVideoCameraSelected
535 + ? Colors.white
536 + : Colors.white38,
537 + size: 80,
538 + ),
539 + Icon(
540 + Icons.circle,
541 + color: _isVideoCameraSelected
542 + ? Colors.red
543 + : Colors.white,
544 + size: 65,
545 + ),
546 + _isVideoCameraSelected &&
547 + _isRecordingInProgress
548 + ? const Icon(
549 + Icons.stop_rounded,
550 + color: Colors.white,
551 + size: 32,
552 + )
553 + : Container(),
554 + ],
555 + ),
556 + ),
557 + InkWell(
558 + onTap:
559 + _imageFile != null || _videoFile != null
560 + ? () {
561 + Navigator.of(context).push(
562 + MaterialPageRoute(
563 + builder: (context) =>
564 + PreviewScreen(
565 + imageFile: _imageFile!,
566 + fileList: allFileList,
567 + ),
568 + ),
569 + );
428 } 570 }
429 - 571 + : null,
430 - @override 572 + child: Container(
431 - void initState() { 573 + width: 60,
432 - super.initState(); 574 + height: 60,
433 - controller!.addListener(_onVideoControllerUpdate); 575 + decoration: BoxDecoration(
576 + color: Colors.black,
577 + borderRadius:
578 + BorderRadius.circular(10.0),
579 + border: Border.all(
580 + color: Colors.white,
581 + width: 2,
582 + ),
583 + image: _imageFile != null
584 + ? DecorationImage(
585 + image: FileImage(_imageFile!),
586 + fit: BoxFit.cover,
587 + )
588 + : null,
589 + ),
590 + child: videoController != null &&
591 + videoController!
592 + .value.isInitialized
593 + ? ClipRRect(
594 + borderRadius:
595 + BorderRadius.circular(8.0),
596 + child: AspectRatio(
597 + aspectRatio: videoController!
598 + .value.aspectRatio,
599 + child: VideoPlayer(
600 + videoController!),
601 + ),
602 + )
603 + : Container(),
604 + ),
605 + ),
606 + ],
607 + ),
608 + ],
609 + ),
610 + ),
611 + ],
612 + ),
613 + ),
614 + Expanded(
615 + child: SingleChildScrollView(
616 + physics: const BouncingScrollPhysics(),
617 + child: Column(
618 + children: [
619 + Padding(
620 + padding: const EdgeInsets.only(top: 8.0),
621 + child: Row(
622 + children: [
623 + Expanded(
624 + child: Padding(
625 + padding: const EdgeInsets.only(
626 + left: 8.0,
627 + right: 4.0,
628 + ),
629 + child: TextButton(
630 + onPressed: _isRecordingInProgress
631 + ? null
632 + : () {
633 + if (_isVideoCameraSelected) {
634 + setState(() {
635 + _isVideoCameraSelected =
636 + false;
637 + });
434 } 638 }
435 - 639 + },
436 - @override 640 + style: TextButton.styleFrom(
437 - void dispose() { 641 + primary: _isVideoCameraSelected
438 - controller!.removeListener(_onVideoControllerUpdate); 642 + ? Colors.black54
439 - super.dispose(); 643 + : Colors.black,
644 + backgroundColor: _isVideoCameraSelected
645 + ? Colors.white30
646 + : Colors.white,
647 + ),
648 + child: const Text('IMAGE'),
649 + ),
650 + ),
651 + ),
652 + Expanded(
653 + child: Padding(
654 + padding: const EdgeInsets.only(
655 + left: 4.0, right: 8.0),
656 + child: TextButton(
657 + onPressed: () {
658 + if (!_isVideoCameraSelected) {
659 + setState(() {
660 + _isVideoCameraSelected = true;
661 + });
440 } 662 }
441 - 663 + },
442 - @override 664 + style: TextButton.styleFrom(
443 - Widget build(BuildContext context) { 665 + primary: _isVideoCameraSelected
444 - if (initialized) { 666 + ? Colors.black
445 - return Center( 667 + : Colors.black54,
446 - child: AspectRatio( 668 + backgroundColor: _isVideoCameraSelected
447 - aspectRatio: controller!.value.aspectRatio, 669 + ? Colors.white
448 - child: VideoPlayer(controller!), 670 + : Colors.white30,
671 + ),
672 + child: const Text('VIDEO'),
673 + ),
674 + ),
675 + ),
676 + ],
677 + ),
678 + ),
679 + Padding(
680 + padding:
681 + const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0),
682 + child: Row(
683 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
684 + children: [
685 + InkWell(
686 + onTap: () async {
687 + setState(() {
688 + _currentFlashMode = FlashMode.off;
689 + });
690 + await controller!.setFlashMode(
691 + FlashMode.off,
692 + );
693 + },
694 + child: Icon(
695 + Icons.flash_off,
696 + color: _currentFlashMode == FlashMode.off
697 + ? Colors.amber
698 + : Colors.white,
699 + ),
700 + ),
701 + InkWell(
702 + onTap: () async {
703 + setState(() {
704 + _currentFlashMode = FlashMode.auto;
705 + });
706 + await controller!.setFlashMode(
707 + FlashMode.auto,
708 + );
709 + },
710 + child: Icon(
711 + Icons.flash_auto,
712 + color: _currentFlashMode == FlashMode.auto
713 + ? Colors.amber
714 + : Colors.white,
715 + ),
716 + ),
717 + InkWell(
718 + onTap: () async {
719 + setState(() {
720 + _currentFlashMode = FlashMode.always;
721 + });
722 + await controller!.setFlashMode(
723 + FlashMode.always,
724 + );
725 + },
726 + child: Icon(
727 + Icons.flash_on,
728 + color: _currentFlashMode == FlashMode.always
729 + ? Colors.amber
730 + : Colors.white,
731 + ),
732 + ),
733 + InkWell(
734 + onTap: () async {
735 + setState(() {
736 + _currentFlashMode = FlashMode.torch;
737 + });
738 + await controller!.setFlashMode(
739 + FlashMode.torch,
740 + );
741 + },
742 + child: Icon(
743 + Icons.highlight,
744 + color: _currentFlashMode == FlashMode.torch
745 + ? Colors.amber
746 + : Colors.white,
747 + ),
748 + ),
749 + ],
750 + ),
751 + )
752 + ],
753 + ),
754 + ),
755 + ),
756 + ],
757 + )
758 + : const Center(
759 + child: Text(
760 + 'LOADING',
761 + style: TextStyle(color: Colors.white),
762 + ),
763 + ),
449 ), 764 ),
450 ); 765 );
451 - } else {
452 - return Container();
453 - }
454 } 766 }
455 } 767 }
......
1 +import 'dart:io';
2 +
3 +import 'package:flutter/material.dart';
4 +
5 +import 'preview_screen.dart';
6 +
7 +class CapturesScreen extends StatelessWidget {
8 + final List<File> imageFileList;
9 +
10 + const CapturesScreen({required this.imageFileList});
11 +
12 + @override
13 + Widget build(BuildContext context) {
14 + return Scaffold(
15 + backgroundColor: Colors.black,
16 + body: SingleChildScrollView(
17 + physics: BouncingScrollPhysics(),
18 + child: Column(
19 + crossAxisAlignment: CrossAxisAlignment.start,
20 + children: [
21 + Padding(
22 + padding: const EdgeInsets.all(16.0),
23 + child: Text(
24 + 'Captures',
25 + style: TextStyle(
26 + fontSize: 32.0,
27 + color: Colors.white,
28 + ),
29 + ),
30 + ),
31 + GridView.count(
32 + shrinkWrap: true,
33 + physics: NeverScrollableScrollPhysics(),
34 + crossAxisCount: 2,
35 + children: [
36 + for (File imageFile in imageFileList)
37 + Container(
38 + decoration: BoxDecoration(
39 + border: Border.all(
40 + color: Colors.black,
41 + width: 2,
42 + ),
43 + ),
44 + child: InkWell(
45 + onTap: () {
46 + Navigator.of(context).pushReplacement(
47 + MaterialPageRoute(
48 + builder: (context) => PreviewScreen(
49 + fileList: imageFileList,
50 + imageFile: imageFile,
51 + ),
52 + ),
53 + );
54 + },
55 + child: Image.file(
56 + imageFile,
57 + fit: BoxFit.cover,
58 + ),
59 + ),
60 + ),
61 + ],
62 + ),
63 + ],
64 + ),
65 + ),
66 + );
67 + }
68 +}
1 +import 'dart:io';
2 +
3 +import 'package:flutter/material.dart';
4 +
5 +import 'captures_screen.dart';
6 +
7 +class PreviewScreen extends StatelessWidget {
8 + final File imageFile;
9 + final List<File> fileList;
10 +
11 + const PreviewScreen({
12 + required this.imageFile,
13 + required this.fileList,
14 + });
15 +
16 + @override
17 + Widget build(BuildContext context) {
18 + return Scaffold(
19 + backgroundColor: Colors.black,
20 + body: Column(
21 + crossAxisAlignment: CrossAxisAlignment.start,
22 + children: [
23 + Padding(
24 + padding: const EdgeInsets.all(8.0),
25 + child: TextButton(
26 + onPressed: () {
27 + Navigator.of(context).pushReplacement(
28 + MaterialPageRoute(
29 + builder: (context) => CapturesScreen(
30 + imageFileList: fileList,
31 + ),
32 + ),
33 + );
34 + },
35 + child: Text('Go to all captures'),
36 + style: TextButton.styleFrom(
37 + primary: Colors.black,
38 + backgroundColor: Colors.white,
39 + ),
40 + ),
41 + ),
42 + Expanded(
43 + child: Image.file(imageFile),
44 + ),
45 + ],
46 + ),
47 + );
48 + }
49 +}
...@@ -31,7 +31,7 @@ class _AccountManagerPageState extends State<AccountManagerPage> { ...@@ -31,7 +31,7 @@ class _AccountManagerPageState extends State<AccountManagerPage> {
31 children: <Widget>[ 31 children: <Widget>[
32 Stack( 32 Stack(
33 children: <Widget>[ 33 children: <Widget>[
34 - ClickItem(title: '店铺logo', onTap: () {}), 34 + ClickItem(title: '头像', onTap: () {}),
35 Positioned( 35 Positioned(
36 top: 8.px, 36 top: 8.px,
37 bottom: 8.px, 37 bottom: 8.px,
......
...@@ -120,6 +120,27 @@ packages: ...@@ -120,6 +120,27 @@ packages:
120 url: "https://pub.dartlang.org" 120 url: "https://pub.dartlang.org"
121 source: hosted 121 source: hosted
122 version: "1.0.1" 122 version: "1.0.1"
123 + camera:
124 + dependency: "direct main"
125 + description:
126 + name: camera
127 + url: "https://pub.dartlang.org"
128 + source: hosted
129 + version: "0.9.4+5"
130 + camera_platform_interface:
131 + dependency: transitive
132 + description:
133 + name: camera_platform_interface
134 + url: "https://pub.dartlang.org"
135 + source: hosted
136 + version: "2.1.4"
137 + camera_web:
138 + dependency: transitive
139 + description:
140 + name: camera_web
141 + url: "https://pub.dartlang.org"
142 + source: hosted
143 + version: "0.2.1+1"
123 characters: 144 characters:
124 dependency: transitive 145 dependency: transitive
125 description: 146 description:
...@@ -632,7 +653,7 @@ packages: ...@@ -632,7 +653,7 @@ packages:
632 source: hosted 653 source: hosted
633 version: "1.8.0" 654 version: "1.8.0"
634 path_provider: 655 path_provider:
635 - dependency: transitive 656 + dependency: "direct main"
636 description: 657 description:
637 name: path_provider 658 name: path_provider
638 url: "https://pub.dartlang.org" 659 url: "https://pub.dartlang.org"
...@@ -764,6 +785,13 @@ packages: ...@@ -764,6 +785,13 @@ packages:
764 url: "https://pub.dartlang.org" 785 url: "https://pub.dartlang.org"
765 source: hosted 786 source: hosted
766 version: "1.0.1" 787 version: "1.0.1"
788 + quiver:
789 + dependency: transitive
790 + description:
791 + name: quiver
792 + url: "https://pub.dartlang.org"
793 + source: hosted
794 + version: "3.0.1+1"
767 rational: 795 rational:
768 dependency: transitive 796 dependency: transitive
769 description: 797 description:
......
...@@ -89,12 +89,16 @@ dependencies: ...@@ -89,12 +89,16 @@ dependencies:
89 tapped: ^2.0.0-nullsafety.0 89 tapped: ^2.0.0-nullsafety.0
90 # 加载动画库 90 # 加载动画库
91 flutter_spinkit: ^5.0.0 91 flutter_spinkit: ^5.0.0
92 - # fijkplayer (Video player plugin for Flutter) Flutter 媒体播放器
93 - fijkplayer: ^0.10.1
94 92
95 json_annotation: ^4.4.0 93 json_annotation: ^4.4.0
96 flutter_plugin_record: ^1.0.1 94 flutter_plugin_record: ^1.0.1
97 95
96 + # fijkplayer (Video player plugin for Flutter) Flutter 媒体播放器
97 + fijkplayer: ^0.10.1
98 +
99 + camera: ^0.9.4+5
100 + path_provider: ^2.0.8
101 +
98 dependency_overrides: 102 dependency_overrides:
99 decimal: 1.5.0 103 decimal: 1.5.0
100 104
...@@ -169,9 +173,7 @@ flutter: ...@@ -169,9 +173,7 @@ flutter:
169 assets: 173 assets:
170 - assets/data/ 174 - assets/data/
171 - assets/images/ 175 - assets/images/
172 - - assets/images/home/
173 - assets/images/login/ 176 - assets/images/login/
174 - - assets/images/account/
175 - assets/images/state/ 177 - assets/images/state/
176 - assets/images/poem/ 178 - assets/images/poem/
177 - assets/images/category/ 179 - assets/images/category/
......