目录
接上文Flutter简单聊天界面布局及语音录制播放_chw-di的博客-CSDN博客
本文主要对聊天布局内的图片及视频的上传、显示和保存到相册进行简单开发。
#相册插件
image_picker: ^0.8.5+3
#查看图片组件
photo_view: ^0.14.0
#缓存照片插件
cached_network_image: ^3.2.1
#视频播放
video_player: ^2.4.7
#视频缩略图
video_thumbnail: ^0.5.2
#文件目录获取
path_provider: ^2.0.11
#保存视频、照片到本地相册
image_gallery_saver: ^1.7.1
在info.plist中添加
<key>NSPhotoLibraryAddUsageDescription</key>
<string>保存图片</string>
<key>NSAppTransportSecurity</key>
<string>http</string>
在AndroidManifest.xml中添加
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<application ...
android:requestLegacyExternalStorage="true"
...
</application>
//获取相册照片并上传
_getPhotos() async {
final XFile? pickImage =
await _picker.pickImage(source: ImageSource.gallery);
//上传
var filePath = await _uploadFile(pickImage!, MessageType.photo);
insertFile(MessageType.photo,filePath,"");
}
//拍照并上传
_takePhotos() async {
final XFile? pickImage =
await _picker.pickImage(source: ImageSource.camera);
//上传
var filePath = await _uploadFile(pickImage!, MessageType.photo);
insertFile(MessageType.photo,filePath,"");
}
//上传视频
_getVideo() async {
final XFile? pickImage = await _picker.pickVideo(source: ImageSource.gallery);
//获取缩略图文件
File videoThumbnailFile = await _getVideoThumbnail(pickImage!);
XFile file = XFile(videoThumbnailFile.path);
//上传缩略图
var videoThumbnailFilePath = await _uploadFile(file, MessageType.photo);
//上传视频
var videoPath = await _uploadFile(pickImage, MessageType.video);
insertFile(MessageType.video,videoPath,videoThumbnailFilePath);
}
//获取视频缩略图
Future<File> _getVideoThumbnail(XFile videoFile) async {
Uint8List? thumbnail = await VideoThumbnail.thumbnailData(
video: videoFile.path,
imageFormat: ImageFormat.JPEG,
quality: 25,
);
var tempDir = await getTemporaryDirectory();
//生成file文件格式
String videoThumbnail = '${tempDir.path}/image_${DateTime.now().millisecond}.jpg';
var file = await File(videoThumbnail).create();
file.writeAsBytesSync(thumbnail!);
return file;
}
//上传文件
Future<String> _uploadFile(XFile imageDir, String type) async {
String filePath = imageDir.path;
var list = filePath.split(".");
var last = list.last;
String fileName = "${const Uuid().v4()}.$last";
FormData formData = FormData.fromMap({
"file": await MultipartFile.fromFile(imageDir.path, filename: fileName),
});
Dio dio = Dio();
var response = await dio.post("http://192.168.9.253:8091/sc/file/upload", data: formData);
var path = response.data["data"]["detail"]["filePath"];
return path;
}
//写入文件
void insertFile(String type,String path,String thumbnail) {
Map data = {};
data['messageId'] = const Uuid().v4();
data['message'] = "图片";
data['messageType'] = type;
data['messageTime'] =
TimeUtils.getFormatDataString(DateTime.now(), "yyyy-MM-dd HH:mm:ss");
data['isMe'] = Random.secure().nextBool();
data['fileUrl'] = path;
if(thumbnail.isNotEmpty){
data['thumbnail'] = thumbnail;
}
setState(() {
_messageData.insert(0, data);
});
}
//照片显示组件:
GestureDetector(
onTap: () {
Navigator.push(
context,
PanPageRouteBuilder(
builder: (context) =>
FullImageWidget(imageUrl: data['fileUrl']),
popDirection: AxisDirection.up));
},
child: Container(
clipBehavior: Clip.hardEdge,
width: ScreenAdapter.width(300),
height: ScreenAdapter.height(400),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(10))),
child: CachedNetworkImage(
imageUrl: data['fileUrl'],
fit: BoxFit.cover,
)),
);
//视频显示组件
GestureDetector(
onTap: () {
Navigator.push(
context,
PanPageRouteBuilder(
builder: (context) =>
FullVideoWidget(videoUrl: data['fileUrl']),
popDirection: AxisDirection.up));
},
child: Stack(
alignment: Alignment.center,
children: [
Container(
color: Colors.white,
width: ScreenAdapter.width(300),
height: ScreenAdapter.height(400),
child: Image.network(data['thumbnail'],fit: BoxFit.cover,),),
Icon(Icons.play_circle,color: Colors.white,size: ScreenAdapter.size(70),)
],
)
);
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:new_chat/service/screen_adapter.dart';
import 'package:new_chat/widget/toast_widget.dart';
import 'package:photo_view/photo_view.dart';
class FullImageWidget extends StatelessWidget {
final String imageUrl;
const FullImageWidget({Key? key, required this.imageUrl}) : super(key: key);
//保存照片
_saveImage() async {
var response = await Dio().get(
imageUrl,
options: Options(responseType: ResponseType.bytes));
final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(response.data),
name: "hello");
if(result['isSuccess']){
ToastWidget.showToast("照片保存成功", ToastGravity.CENTER);
}
}
//长摁保存照片组件
_showSaveVideoWidget(BuildContext context) async {
showModalBottomSheet(
context: context,
isDismissible: true,
isScrollControlled: false,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15))),
builder: (BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(topLeft: Radius.circular(15),topRight: Radius.circular(15))
),
height: ScreenAdapter.height(400),
child: Column(
children: [
Container(padding: EdgeInsets.all(ScreenAdapter.height(40)),child: Text(textAlign:TextAlign.center,"保存照片到相册",maxLines:2,style: TextStyle(color: const Color.fromRGBO(102, 102, 102, 1),fontSize: ScreenAdapter.size(25)),),),
Divider(height: ScreenAdapter.height(0.5)),
InkWell(
child:Padding(padding: EdgeInsets.all(ScreenAdapter.height(18)),child: Center(child: Text("保存照片",style: TextStyle(color: Colors.red,fontSize: ScreenAdapter.size(30)),),),),
onTap: () async{
_saveImage();
Navigator.pop(context);
},
),
Container(color: const Color.fromRGBO(245, 245, 245,1),height: ScreenAdapter.height(15)),
InkWell(
onTap: (){
Navigator.pop(context);
},
child:Padding(padding: EdgeInsets.fromLTRB(ScreenAdapter.width(0),ScreenAdapter.height(10),ScreenAdapter.width(0),ScreenAdapter.height(15)),child: Text("取消",style: TextStyle(color: const Color.fromRGBO(51, 51, 51, 1),fontSize: ScreenAdapter.size(30)),)),),
],
),
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black87,
body: GestureDetector(
child: Center(
child: PhotoView(
imageProvider: NetworkImage(imageUrl),
)),
onTap: () {
Navigator.pop(context);
},
//长摁弹出保存照片界面
onLongPress: (){
_showSaveVideoWidget(context);
},
),
);
}
}
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:new_chat/service/screen_adapter.dart';
import 'package:new_chat/widget/toast_widget.dart';
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';
import 'package:video_player/video_player.dart';
class FullVideoWidget extends StatefulWidget {
final String videoUrl;
const FullVideoWidget({Key? key, required this.videoUrl}) : super(key: key);
@override
State<FullVideoWidget> createState() => _FullVideoWidgetState();
}
class _FullVideoWidgetState extends State<FullVideoWidget> {
late VideoPlayerController _controller;
//视频总时长
String videoPlayerEndTime = "";
//视频正在播放的时长
String videoPlayerTime = "";
@override
void initState() {
_controller = VideoPlayerController.network(widget.videoUrl)
..initialize().then((_) {
setState(() {
_controller.play();
});
});
_controller.addListener(() {
setState(() {
//拼接视频总时长
int endMinutes = _controller.value.duration.inMinutes;
//不足2位补0
var endMinutesPadLeft = endMinutes.toString().padLeft(2,"0");
int endSeconds = _controller.value.duration.inSeconds;
var endSecondsPadLeft = endSeconds.toString().padLeft(2,"0");
videoPlayerEndTime = "$endMinutesPadLeft:$endSecondsPadLeft";
int videoPlayerMinutes = _controller.value.position.inMinutes;
var videoPlayerMinutesPadLeft = videoPlayerMinutes.toString().padLeft(2,"0");
int videoPlayerSeconds = _controller.value.position.inSeconds;
var videoPlayerSecondsPadLeft = videoPlayerSeconds.toString().padLeft(2,"0");
videoPlayerTime = "$videoPlayerMinutesPadLeft:$videoPlayerSecondsPadLeft";
});
});
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
//保存视频
_saveVideo() async {
var appDocDir = await getTemporaryDirectory();
String savePath = "${appDocDir.path}+${const Uuid().v4()}/temp.mp4";
await Dio().download(widget.videoUrl, savePath);
final result = await ImageGallerySaver.saveFile(savePath);
if(result['isSuccess']){
ToastWidget.showToast("视频保存成功", ToastGravity.CENTER);
}
}
//长摁保存视频组件
_showSaveVideoWidget(BuildContext context) async {
showModalBottomSheet(
context: context,
isDismissible: true,
isScrollControlled: false,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15))),
builder: (BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(topLeft: Radius.circular(15),topRight: Radius.circular(15))
),
height: ScreenAdapter.height(400),
child: Column(
children: [
Container(padding: EdgeInsets.all(ScreenAdapter.height(40)),child: Text(textAlign:TextAlign.center,"保存视频到相册",maxLines:2,style: TextStyle(color: const Color.fromRGBO(102, 102, 102, 1),fontSize: ScreenAdapter.size(25)),),),
Divider(height: ScreenAdapter.height(0.5)),
InkWell(
child:Padding(padding: EdgeInsets.all(ScreenAdapter.height(18)),child: Center(child: Text("保存视频",style: TextStyle(color: Colors.red,fontSize: ScreenAdapter.size(30)),),),),
onTap: () async{
_saveVideo();
Navigator.pop(context);
},
),
Container(color: const Color.fromRGBO(245, 245, 245,1),height: ScreenAdapter.height(15)),
InkWell(
onTap: (){
Navigator.pop(context);
},
child:Padding(padding: EdgeInsets.fromLTRB(ScreenAdapter.width(0),ScreenAdapter.height(10),ScreenAdapter.width(0),ScreenAdapter.height(15)),child: Text("取消",style: TextStyle(color: const Color.fromRGBO(51, 51, 51, 1),fontSize: ScreenAdapter.size(30)),)),),
],
),
);
});
}
@override
Widget build(BuildContext context) {
ScreenAdapter.init(context);
return Scaffold(
backgroundColor: Colors.black87,
body: GestureDetector(
onLongPress: ()async {
//长摁弹出保存视频界面
await _showSaveVideoWidget(context);
},
child: Stack(
children: [
//视频内容
Align(
child: Container(
child: _controller.value.isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: Container(),
),
),
//播放暂定和播放进度条和视频时间
Container(
margin: EdgeInsets.only(bottom: ScreenAdapter.height(200)),
child: Align(
alignment: Alignment.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
padding:
EdgeInsets.only(left: ScreenAdapter.width(20)),
child: GestureDetector(
onTap: () {
_controller.value.isPlaying
? _controller.pause()
: _controller.play();
},
child: Icon(
_controller.value.isPlaying
? Icons.pause_outlined
: Icons.play_arrow,
color: Colors.white,
size: ScreenAdapter.size(60),
),
),
),
Container(
padding:
EdgeInsets.only(left: ScreenAdapter.width(40)),
child: Text(
videoPlayerTime,
style: const TextStyle(color: Colors.white),
),
),
Expanded(
flex: 1,
child: VideoProgressIndicator(
_controller,
allowScrubbing: true,
colors: const VideoProgressColors(
playedColor: Colors.white,
bufferedColor: Colors.white10,
backgroundColor: Colors.black26),
padding: EdgeInsets.fromLTRB(
ScreenAdapter.width(20),
0,
ScreenAdapter.width(20),
0),
),
),
Container(
padding:
EdgeInsets.only(right: ScreenAdapter.width(50)),
child: Text(
videoPlayerEndTime,
style: const TextStyle(color: Colors.white),
),
),
],
)),
),
//关闭视频按钮
Align(
alignment: Alignment.bottomLeft,
child:Container(
padding: EdgeInsets.fromLTRB(ScreenAdapter.width(20),0,0,ScreenAdapter.height(100)),
child: GestureDetector(
onTap: (){
Navigator.pop(context);
},
child: Icon(Icons.cancel,color: Colors.white,size: ScreenAdapter.size(60),),
),),),
//视频保存按钮
Align(
alignment: Alignment.bottomRight,
child:Container(
padding: EdgeInsets.fromLTRB(0,0,ScreenAdapter.width(20),ScreenAdapter.height(100)),
child: GestureDetector(
onTap: () async{
_saveVideo();
},
child: Icon(Icons.download_for_offline,color: Colors.white,size: ScreenAdapter.size(60),),
),),),
],
),)
);
}
}
语音聊天优化&照片及视频发送和保存到相册配套视频
我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou
我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我得到了一个包含嵌套链接的表单。编辑时链接字段为空的问题。这是我的表格:Editingkategori{:action=>'update',:id=>@konkurrancer.id})do|f|%>'Trackingurl',:style=>'width:500;'%>'Editkonkurrence'%>|我的konkurrencer模型:has_one:link我的链接模型:classLink我的konkurrancer编辑操作:defedit@konkurrancer=Konkurrancer.find(params[:id])@konkurrancer.link_attrib
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为
为了将Cucumber用于命令行脚本,我按照提供的说明安装了arubagem。它在我的Gemfile中,我可以验证是否安装了正确的版本并且我已经包含了require'aruba/cucumber'在'features/env.rb'中为了确保它能正常工作,我写了以下场景:@announceScenario:Testingcucumber/arubaGivenablankslateThentheoutputfrom"ls-la"shouldcontain"drw"假设事情应该失败。它确实失败了,但失败的原因是错误的:@announceScenario:Testingcucumber/ar
我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer
我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server
在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',