656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 飞鱼
7 |
8 |
9 |
10 |
11 |
12 | # 简介
13 |
14 | Feiyu is a video player, which is developed using Flutter and supports #.m3u8, #.rtsp, #.rtmp video formats.
15 |
16 | You are free to add search sources within the application that complies with the FeiFeiCms interface specification or Live feeds in #.dpl, #.m3u, #.xspf formats.
17 |
18 | Meanwhile, you can also collect your favorite movies and make a movie list to record your movie-going history. That's all, have a good time.
19 |
20 | 飞鱼是使用Flutter开发的支援 #.m3u8, #.rtsp, #.rtmp 格式的视频播放器。
21 |
22 | 你可以在应用内自由添加符合 FeiFeiCms 接口规范的搜索源或者 #.dpl, #.m3u, #.xspf 格式的直播源。
23 |
24 | 同时,你也可以收藏自己中意的影片并制作成影单,记录自己的观影历史,不负好时光。
25 |
26 | # 预览
27 |
28 | **旧版** *(已开源)*
29 |
30 | |  |  |  |  |
31 | | :------------: | :------------: | :------------: | :------------: |
32 |
33 | **新版**
34 |
35 | |  |  |  |  |
36 | | :------------: | :------------: | :------------: | :------------: |
37 |
38 | # 免责声明
39 |
40 | 本项目仅供 Flutter 学习交流。
41 |
42 | # 鸣谢
43 |
44 | befovy / fijkplayer *MIT License*
45 | https://github.com/befovy/fijkplayer/blob/master/LICENSE
46 |
47 | succlz123 / DLNA-Dart *Apache License*
48 | https://github.com/succlz123/DLNA-Dart/blob/master/LICENSE
49 |
50 |
51 | IwantBEStrong / video_player_full_funciton
52 | https://github.com/IwantBEStrong/video_player_full_funciton
53 |
--------------------------------------------------------------------------------
/lib/app/config/config.dart:
--------------------------------------------------------------------------------
1 | String APP = '20200621';
2 |
3 | String SERVER = 'http://x.xxx.xxx/xxxx';
4 |
5 | String PUSH = r'''
6 | {
7 | "flag": "关",
8 | "force": "是",
9 | "info": "抱歉,当前版本飞鱼已停用,请关注公众号“乂乂又又”,回复“飞鱼”,获取最新版本!",
10 | "about": "飞鱼是本人兴趣爱好之作,发布纯粹是开源分享、仅供学习交流。本软件不得用于商业用途或从事违反中国人民共和国相关法律所禁止的活动,本人对于用户擅自使用本软件从事违法活动不承担任何责任(包括但不限于刑事责任、行政责任、民事责任)。若您继续使用本软件则表明您已完全阅读理解并同意上述约定,否则请立即关闭并卸载本软件。 想要了解更多信息,请关注我的公众号:乂乂又又。"
11 | }
12 | ''';
13 |
14 | String PLAYER = r'''
15 | {
16 | "server": "https://www.dplayer.tv/dp/",
17 | "title": "title",
18 | "m3u8": "url",
19 | "public": "是"
20 | }
21 | ''';
22 |
23 | String SITES = r'''
24 | [{
25 | "name": "OK",
26 | "server": "http://www.apiokzy.com/",
27 | "title": ".*?target=\"_blank\">(.*?)",
28 | "link": "(.*?)",
30 | "xml": "http://cj.okzy.tv/inc/api.php",
31 | "item": "",
32 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)",
33 | "cover": "
(.*?)(.*?)",
41 | "xml": "http://cj.yongjiuzyw.com/inc/api.php",
42 | "item": "",
43 | "m3u8": "name=\"copy_sel\" value=\"(.*?\\.m3u8?)\" checked=\"\">",
44 | "cover": "
"
45 | },
46 | {
47 | "name": "最大",
48 | "server": "http://www.zuidazy5.com/",
49 | "title": ".*?target=\"_blank\">(.*?)<",
50 | "link": "(.*?)",
52 | "xml": "http://www.zdziyuan.com/inc/api_zuidam3u8.php",
53 | "item": "",
54 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)",
55 | "cover": "
.*?target=\"_blank\">(.*?)",
61 | "link": "(.*?)",
63 | "xml": "http://api.kbzyapi.com/inc/api.php",
64 | "item": "",
65 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)",
66 | "cover": "
.*?target=\"_blank\">(.*?)",
72 | "link": "(.*?)",
74 | "xml": "http://api.zuixinapi.com/inc/api.php",
75 | "item": "",
76 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)",
77 | "cover": "
.*?target=\"_blank\">(.*?)",
83 | "link": "(.*?)",
85 | "xml": "http://cj.wlzy.tv/inc/api_mac_m3u8.php",
86 | "item": "",
87 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)",
88 | "cover": "
.*?target=\"_blank\">(.*?)",
94 | "link": "(.*?)",
96 | "xml": "https://www.mhapi123.com/inc/api_all.php",
97 | "item": "",
98 | "m3u8": "name=\"copy_sel.*>(.*\\$http.*?m3u8)",
99 | "cover": "
playlist;
6 |
7 | Movie({this.title, this.desp, this.cover, this.playlist});
8 |
9 | Movie.fromJson(Map json) {
10 | title = json['title'];
11 | desp = json['desp'];
12 | cover = json['cover'];
13 | if (json['playlist'] != null) {
14 | playlist = new List();
15 | json['playlist'].forEach((v) {
16 | playlist.add(new Playlist.fromJson(v));
17 | });
18 | }
19 | }
20 |
21 | Map toJson() {
22 | final Map data = new Map();
23 | data['title'] = this.title;
24 | data['desp'] = this.desp;
25 | data['cover'] = this.cover;
26 | if (this.playlist != null) {
27 | data['playlist'] = this.playlist.map((v) => v.toJson()).toList();
28 | }
29 | return data;
30 | }
31 | }
32 |
33 | class Playlist {
34 | String name;
35 | String m3u8;
36 |
37 | Playlist({this.name, this.m3u8});
38 |
39 | Playlist.fromJson(Map json) {
40 | name = json['name'];
41 | m3u8 = json['m3u8'];
42 | }
43 |
44 | Map toJson() {
45 | final Map data = new Map();
46 | data['name'] = this.name;
47 | data['m3u8'] = this.m3u8;
48 | return data;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/app/model/player.dart:
--------------------------------------------------------------------------------
1 | class Player {
2 | String server;
3 | String title;
4 | String m3u8;
5 | String public;
6 |
7 | Player({this.server, this.title, this.m3u8, this.public});
8 |
9 | Player.fromJson(Map json) {
10 | server = json['server'];
11 | title = json['title'];
12 | m3u8 = json['m3u8'];
13 | public = json['public'];
14 | }
15 |
16 | Map toJson() {
17 | final Map data = new Map();
18 | data['server'] = this.server;
19 | data['title'] = this.title;
20 | data['m3u8'] = this.m3u8;
21 | data['public'] = this.public;
22 | return data;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/app/model/push.dart:
--------------------------------------------------------------------------------
1 | class Push {
2 | String flag;
3 | String force;
4 | String info;
5 | String about;
6 |
7 | Push({this.flag, this.info, this.about, this.force});
8 |
9 | Push.fromJson(Map json) {
10 | flag = json['flag'];
11 | force = json['force'];
12 | info = json['info'];
13 | about = json['about'];
14 | }
15 |
16 | Map toJson() {
17 | final Map data = new Map();
18 | data['flag'] = this.flag;
19 | data['force'] = this.force;
20 | data['info'] = this.info;
21 | data['about'] = this.about;
22 | return data;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/app/model/site.dart:
--------------------------------------------------------------------------------
1 | class Site {
2 | String name;
3 | String server;
4 | String title;
5 | String link;
6 | String desp;
7 | String item;
8 | String m3u8;
9 | String cover;
10 | String xml;
11 |
12 | Site(
13 | {this.name,
14 | this.server,
15 | this.title,
16 | this.link,
17 | this.desp,
18 | this.item,
19 | this.m3u8,
20 | this.cover,
21 | this.xml});
22 |
23 | Site.fromJson(Map json) {
24 | name = json['name'];
25 | server = json['server'];
26 | title = json['title'];
27 | link = json['link'];
28 | desp = json['desp'];
29 | item = json['item'];
30 | m3u8 = json['m3u8'];
31 | cover = json['cover'];
32 | xml = json['xml'];
33 | }
34 |
35 | Map toJson() {
36 | final Map data = new Map();
37 | data['name'] = this.name;
38 | data['server'] = this.server;
39 | data['title'] = this.title;
40 | data['link'] = this.link;
41 | data['desp'] = this.desp;
42 | data['item'] = this.item;
43 | data['m3u8'] = this.m3u8;
44 | data['cover'] = this.cover;
45 | data['xml'] = this.xml;
46 | return data;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/app/tool/decode.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | String encodex(String s) => urlEncode(b64(s));
4 |
5 | String b64(s) => base64Encode(utf8.encode(s));
6 |
7 | String urlEncode(String s) => Uri.encodeComponent(s);
8 |
9 | String toBase64(String data) {
10 | var content = utf8.encode(data);
11 | var digest = base64Encode(content);
12 | return digest;
13 | }
14 |
15 | String fromBase64(String data) {
16 | return utf8.decode(base64Decode(data));
17 | }
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/lib/app/tool/http.dart:
--------------------------------------------------------------------------------
1 | import 'package:dio/dio.dart';
2 |
3 | class Http {
4 | Dio dio;
5 | BaseOptions options;
6 |
7 | Http() {
8 | options = new BaseOptions(
9 | contentType: "application/x-www-form-urlencoded",
10 | responseType: ResponseType.plain, //以文本方式接收数据
11 | headers: {
12 | // 'User-Agent':
13 | // 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362'
14 | });
15 |
16 | dio = new Dio(options);
17 |
18 | //添加拦截器
19 | dio.interceptors
20 | .add(InterceptorsWrapper(onRequest: (RequestOptions options) {
21 | // print("-----开始请求-----");
22 | return options;
23 | }, onResponse: (Response response) {
24 | // print("-----开始响应-----");
25 | return response;
26 | }, onError: (DioError e) {
27 | // print("-----发生错误-----");
28 | return e;
29 | }));
30 | }
31 |
32 | /*
33 | * get请求
34 | */
35 | Future get(url, {data, options}) async {
36 | dio.options.responseType = ResponseType.plain;
37 | dio.options.contentType = "application/x-www-form-urlencoded";
38 | try {
39 | Response response =
40 | await dio.get(url, queryParameters: data, options: options);
41 | return response.data.toString();
42 | } on DioError catch (e) {
43 | if (e != null && e.response != null) {
44 | if (e.response.statusCode == 301) {
45 | var re = await get(e.response.headers['location'][0],
46 | options: options, data: data);
47 | return re;
48 | }
49 | if (e.response.statusCode == 302) {
50 | var re = await get(e.response.headers['location'][0],
51 | options: options, data: data);
52 | return re;
53 | }
54 | }
55 | return formatError(e);
56 | }
57 | }
58 |
59 | /*
60 | * post请求
61 | */
62 | Future post(url, {data, options}) async {
63 | dio.options.responseType = ResponseType.plain;
64 | dio.options.contentType = "application/x-www-form-urlencoded";
65 | try {
66 | Response response =
67 | await dio.post(url, queryParameters: data, options: options);
68 | return response.data.toString();
69 | } on DioError catch (e) {
70 | if (e != null && e.response != null) {
71 | if (e.response.statusCode == 301) {
72 | var re = await post(e.response.headers['location'][0],
73 | options: options, data: data);
74 | return re;
75 | }
76 | if (e.response.statusCode == 302) {
77 | var re = await post(e.response.headers['location'][0],
78 | options: options, data: data);
79 | return re;
80 | }
81 | }
82 | return formatError(e);
83 | }
84 | }
85 |
86 | /*
87 | * 下载文件
88 | */
89 | Future download(String urlPath, String savePath,
90 | {void Function(int, int) progress}) async {
91 | try {
92 | double before = 0;
93 | await dio.download(urlPath, savePath,
94 | options: Options(receiveTimeout: 0),
95 | onReceiveProgress: progress ??
96 | (int count, int total) {
97 | double position = (count / total) * 100;
98 | if (position - before > 5) {
99 | print('下载进度---> ${position.round()}% ');
100 | before = position;
101 | }
102 | });
103 | return true;
104 | } on DioError {
105 | return false;
106 | }
107 | }
108 |
109 | /*
110 | * error统一处理
111 | */
112 | String formatError(DioError e) {
113 | return "请求失败";
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/lib/app/tool/random.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | //生成定长随机数
4 | String randomCodeX(int strlenght) {
5 | String alphabet = '0123456789';
6 | String left = '';
7 | for (var i = 0; i < strlenght; i++) {
8 | left = left + alphabet[Random().nextInt(alphabet.length)];
9 | }
10 | return left;
11 | }
12 |
13 | //生成时间戳
14 | String randomCode(int strlenght) => '${DateTime.now().millisecondsSinceEpoch}';
15 |
16 | randomListItem(List temp) => temp[Random().nextInt(temp.length)];
17 |
18 | List copyList(List temp) =>
19 | List.generate(temp.length, (int index) => temp[index], growable: true);
20 |
21 | List randomList(List order) {
22 | List old = copyList(order);
23 | int index = 0;
24 | while (old.length > 1) {
25 | var e = randomListItem(old);
26 | order[index] = e;
27 | old.remove(e);
28 | index++;
29 | }
30 | return order;
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/lib/app/tool/search.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:feiyu/app/model/movie.dart';
4 | import 'package:feiyu/app/model/site.dart';
5 | import 'package:feiyu/app/tool/http.dart';
6 |
7 | import 'package:xml2json/xml2json.dart';
8 |
9 | class Search {
10 | static Future> api(String keyword, Site site,
11 | {bool force = true}) async =>
12 | spiderAPI(keyword, site, force: force);
13 |
14 | static Future> web(String keyword, Site site,
15 | {bool force = true}) async =>
16 | spiderWeb(keyword, site, force: force);
17 |
18 | static Future> spiderAPI(String keyword, Site site,
19 | {bool force = true}) async {
20 | dynamic temp;
21 | Http http = Http();
22 | List movies = [];
23 | String text = await http.get(site.xml + "?wd=" + keyword);
24 | try {
25 | temp = getUrlsApi(text, site);
26 | } catch (e) {
27 | return movies;
28 | }
29 | List links = temp['links'];
30 | List titles = temp['titles'];
31 | if (links.length < 1) return movies;
32 | for (var link in links) {
33 | var title = titles[links.indexOf(link)].trim();
34 | //严格模式
35 | if (force && !title.contains(keyword)) continue;
36 | //获取详情
37 | text = await http.get(site.xml + '?ac=videolist&ids=' + link);
38 | try {
39 | temp = getM3u8sApi(text, site);
40 | } catch (e) {
41 | continue;
42 | }
43 | List m3u8s = temp['m3u8s'];
44 | List items = temp['items'];
45 | String cover = 'http' + (site.server + temp['cover']).split('http').last;
46 | String desp = temp['desp'];
47 | List playlists = [];
48 | for (var m3u8 in m3u8s) {
49 | playlists.add(Playlist(m3u8: m3u8, name: items[m3u8s.indexOf(m3u8)]));
50 | }
51 | movies.add(
52 | Movie(title: title, cover: cover, desp: desp, playlist: playlists));
53 | }
54 | return movies;
55 | }
56 |
57 | static getM3u8sApi(String text, Site site) {
58 | String cover, desp;
59 | List m3u8s, items = [], m3u8x = [];
60 | var video = jsonDecode(xmlToJson(text))['rss']['list']['video'];
61 | desp = video['des'];
62 | cover = video['pic'];
63 | m3u8s = video['dl']['dd'].split('#');
64 | for (var m3u8 in m3u8s) {
65 | if (m3u8.contains(r'$')) {
66 | items.add(m3u8.split(r'$')[0]);
67 | m3u8x.add(m3u8.split(r'$')[1]);
68 | }
69 | }
70 | return {"m3u8s": m3u8x, "cover": cover, "desp": desp, "items": items};
71 | }
72 |
73 | static getUrlsApi(String text, Site site) {
74 | List titles = [], links = [];
75 | var json = jsonDecode(xmlToJson(text));
76 | if (json['rss']['list'] == null) {
77 | //搜索结果为空
78 | return {"titles": [], "links": []};
79 | }
80 | var videos = json['rss']['list']['video'];
81 | if (!(json['rss']['list']['video'] is List)) {
82 | //只搜到一个
83 | return {
84 | "titles": [videos['name'] + '(' + videos['note'] + ')'],
85 | "links": [videos['id']]
86 | };
87 | }
88 | //搜到多个
89 | for (var video in videos) {
90 | titles.add(video['name'] + '(' + video['note'] + ')');
91 | links.add(video['id']);
92 | }
93 | return {"titles": titles, "links": links};
94 | }
95 |
96 | static String xmlToJson(String xml) {
97 | var myTransformer = Xml2Json();
98 | try {
99 | myTransformer.parse(xml);
100 | } catch (e) {
101 | return '{"rss":{"list":null}}';
102 | }
103 | return myTransformer.toParker();
104 | }
105 |
106 | static Future> spiderWeb(String keyword, Site site,
107 | {bool force = true}) async {
108 | Http http = Http();
109 | List movies = [];
110 | String text = await http.post(site.server + "index.php?m=vod-search",
111 | data: {"wd": keyword, "submit": "search"});
112 | dynamic temp = getUrlsWeb(text, site);
113 | List links = temp['links'];
114 | List titles = temp['titles'];
115 | if (links.length < 1 || links.length != titles.length) return movies;
116 | for (var link in links) {
117 | var title = titles[links.indexOf(link)].trim();
118 | //严格模式
119 | if (force && !title.contains(keyword)) continue;
120 | //获取详情
121 | text = await http.get((site.server + link)
122 | .replaceAll('//?', '/?')
123 | .replaceAll(site.server * 2, site.server));
124 | temp = getM3u8sWeb(text, site);
125 | List m3u8s = temp['m3u8s'];
126 | List items = temp['items'];
127 | if (temp['cover'].length < 1 || temp['desp'].length < 1) continue;
128 | String cover =
129 | 'http' + (site.server + temp['cover'][0]).split('http').last;
130 | String desp = temp['desp'][0];
131 | List playlists = [];
132 | for (var m3u8 in m3u8s) {
133 | playlists.add(Playlist(m3u8: m3u8, name: items[m3u8s.indexOf(m3u8)]));
134 | }
135 | movies.add(
136 | Movie(title: title, cover: cover, desp: desp, playlist: playlists));
137 | }
138 | return movies;
139 | }
140 |
141 | static getM3u8sWeb(String text, Site site) {
142 | List cover, m3u8s, desp, items = [], m3u8x = [];
143 | cover = findAll(text, site.cover);
144 | m3u8s = findAll(text, site.m3u8);
145 | // items = findAll(text, site.item);
146 | desp = findAll(text, site.desp);
147 | for (var m3u8 in m3u8s) {
148 | if (m3u8.contains(r'$')) {
149 | items.add(m3u8.split(r'$')[0]);
150 | m3u8x.add(m3u8.split(r'$')[1]);
151 | }
152 | }
153 | return {"m3u8s": m3u8x, "cover": cover, "desp": desp, "items": items};
154 | }
155 |
156 | static getUrlsWeb(String text, Site site) {
157 | List titles, links;
158 | titles = findAll(text, site.title);
159 | links = findAll(text, site.link);
160 | return {"titles": titles, "links": links};
161 | }
162 |
163 | static List findAll(String text, String exp) =>
164 | RegExp(exp).allMatches(text).map((s) {
165 | if (s.groupCount < 1) return '';
166 | return s.group(1);
167 | }).toList();
168 | }
169 |
--------------------------------------------------------------------------------
/lib/app/tool/time.dart:
--------------------------------------------------------------------------------
1 |
2 | String videoTime(int duration) {
3 | String twoDigits(int n) {
4 | if (n >= 10) return "$n";
5 | return "0$n";
6 | }
7 | String twoDigitHours =
8 | twoDigits((duration ~/ (1000 * 60 * 60)).remainder(24));
9 | String twoDigitMinutes = twoDigits((duration ~/ (1000 * 60)).remainder(60));
10 | String twoDigitSeconds = twoDigits((duration ~/ (1000)).remainder(60));
11 | return twoDigitHours == '00'
12 | ? "$twoDigitMinutes:$twoDigitSeconds"
13 | : "$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds";
14 | }
--------------------------------------------------------------------------------
/lib/app/widget/myBlurButton.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:flustars/flustars.dart';
4 | import 'package:flutter/material.dart';
5 |
6 | myBlurButton(IconData icon, {void Function() onTap}) {
7 | var screenWidth = ScreenUtil.getInstance().screenWidth;
8 | var size = screenWidth / 10*1.2;
9 | return GestureDetector(
10 | onTap: onTap ?? () {},
11 | behavior: HitTestBehavior.translucent,
12 | child: ClipRRect(
13 | borderRadius: BorderRadius.circular(size / 2),
14 | child: BackdropFilter(
15 | filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
16 | child: Container(
17 | alignment: Alignment.center,
18 | width: size,
19 | height: size,
20 | decoration: BoxDecoration(
21 | color: Colors.white.withOpacity(0.1),
22 | borderRadius: BorderRadius.circular(size / 2)),
23 | child: Icon(icon, size: size / 2, color: Colors.white))),
24 | ));
25 | }
26 |
--------------------------------------------------------------------------------
/lib/app/widget/myBottomInput.dart:
--------------------------------------------------------------------------------
1 | import 'package:flustars/flustars.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import 'myBottomSheet.dart';
5 | import 'myRoundButton.dart';
6 | import 'myText.dart';
7 | import 'myTextField.dart';
8 |
9 | myBottomInput(context, {title = '请输入', hint = ':)', autoFocus = false}) async =>
10 | await Navigator.push(
11 | context,
12 | MyBottomSheet(
13 | child: BottomInput(
14 | title: title, hintText: hint, autoFocus: autoFocus)));
15 |
16 | class BottomInput extends StatefulWidget {
17 | final String title;
18 | final String hintText;
19 | final bool autoFocus;
20 |
21 | BottomInput(
22 | {this.title = '请输入', this.hintText = ':)', this.autoFocus = false});
23 |
24 | @override
25 | _BottomInputState createState() => _BottomInputState();
26 | }
27 |
28 | class _BottomInputState extends State {
29 | String myTxt;
30 | @override
31 | Widget build(BuildContext context) {
32 | return Scaffold(
33 | backgroundColor: Colors.black45,
34 | body: Column(
35 | children: [
36 | Expanded(
37 | child: GestureDetector(
38 | child: Container(
39 | color: Colors.transparent,
40 | ),
41 | onTap: () {
42 | Navigator.pop(context);
43 | },
44 | )),
45 | myRadiusBottom(context),
46 | ],
47 | ),
48 | );
49 | }
50 |
51 | myRadiusBottom(context, {double height}) {
52 | if (height == null) height = ScreenUtil.getInstance().screenWidth / 20 * 12;
53 | double radius = height / 12 * 1.5;
54 | return Container(
55 | //弹窗布局
56 | width: double.infinity,
57 | height: height, //弹窗高度不能超过屏幕的一半!!!
58 | decoration: BoxDecoration(
59 | color: Colors.white,
60 | borderRadius: BorderRadiusDirectional.only(
61 | topStart: Radius.circular(radius),
62 | topEnd: Radius.circular(radius))),
63 | child: Column(
64 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
65 | children: [
66 | myText(widget.title, size: height / 12 * 1),
67 | mySearchBox(context,
68 | hintText: widget.hintText, height: height / 12 * 3),
69 | Row(
70 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
71 | children: [
72 | myRoundButton('取 消', onTap: () {
73 | Navigator.pop(context); //传回空值
74 | }, height: height / 12 * 3, colorBack: const Color(0xFFf4f5fa)),
75 | myRoundButton('确 定', onTap: () {
76 | Navigator.pop(context, myTxt); //传回输入值
77 | },
78 | height: height / 12 * 3,
79 | colorText: Colors.lightBlue,
80 | colorBack: const Color(0xFFf4f5fa)),
81 | ],
82 | )
83 | ],
84 | ));
85 | }
86 |
87 | mySearchBox(context, {String hintText, double height}) {
88 | //底部搜索框
89 | return MyTextField(
90 | autofocus: widget.autoFocus,
91 | height: height,
92 | hintText: hintText,
93 | onSubmit: (txt) {
94 | Navigator.pop(context, txt); //传回输入值
95 | },
96 | onChanged: (txt) {
97 | myTxt = txt; //传回输入值
98 | },
99 | );
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/lib/app/widget/myBottomSheet.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class MyBottomSheet extends PopupRoute {
4 | final Duration _duration = Duration(milliseconds: 300);
5 | Widget child;
6 |
7 | MyBottomSheet({@required this.child});
8 |
9 | @override
10 | Color get barrierColor => null;
11 |
12 | @override
13 | bool get barrierDismissible => true;
14 |
15 | @override
16 | String get barrierLabel => null;
17 |
18 | @override
19 | Widget buildPage(BuildContext context, Animation animation,
20 | Animation secondaryAnimation) {
21 | return child;
22 | }
23 |
24 | @override
25 | Duration get transitionDuration => _duration;
26 | }
27 |
28 |
29 |
--------------------------------------------------------------------------------
/lib/app/widget/myBottomTip.dart:
--------------------------------------------------------------------------------
1 | import 'package:flustars/flustars.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import 'myBottomSheet.dart';
5 | import 'myRoundButton.dart';
6 |
7 | myBottomTip(context, {String title, String desp}) async => await Navigator.push(
8 | context, MyBottomSheet(child: MyBottomTip(title, desp)));
9 |
10 | class MyBottomTip extends StatefulWidget {
11 | final String title, desp;
12 | MyBottomTip(this.title, this.desp);
13 | @override
14 | _MyBottomTipState createState() => _MyBottomTipState();
15 | }
16 |
17 | class _MyBottomTipState extends State {
18 | @override
19 | Widget build(BuildContext context) {
20 | return Scaffold(
21 | backgroundColor: Colors.black45,
22 | body: Column(
23 | children: [
24 | Expanded(
25 | child: GestureDetector(
26 | child: Container(
27 | color: Colors.transparent,
28 | ),
29 | onTap: () {
30 | Navigator.pop(context);
31 | },
32 | )),
33 | myBottomTipLayout(context),
34 | ],
35 | ));
36 | }
37 |
38 | myBottomTipLayout(context) {
39 | double radius = ScreenUtil.getInstance().screenWidth / 20 * 1.5;
40 | return Container(
41 | //弹窗布局
42 | width: ScreenUtil.getInstance().screenWidth,
43 | decoration: BoxDecoration(
44 | color: Colors.white,
45 | borderRadius: BorderRadiusDirectional.only(
46 | topStart: Radius.circular(radius),
47 | topEnd: Radius.circular(radius))),
48 | child: Column(
49 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
50 | children: [
51 | myHead(radius),
52 | Container(
53 | padding: EdgeInsets.only(
54 | left: radius, right: radius, bottom: radius, top: radius / 2),
55 | child: Text(
56 | widget.desp,
57 | maxLines: 20,
58 | overflow: TextOverflow.ellipsis,
59 | style: TextStyle(fontSize: 14),
60 | ),
61 | )
62 | ],
63 | ));
64 | }
65 |
66 | myHead(radius) => Container(
67 | padding: EdgeInsets.only(
68 | left: radius / 2, right: radius / 2, top: radius / 3),
69 | child: Row(
70 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
71 | children: [
72 | myRoundButton('',
73 | colorBack: Colors.transparent,
74 | width: ScreenUtil.getInstance().screenWidth / 20 * 2,
75 | height: ScreenUtil.getInstance().screenWidth / 20 * 2),
76 | Text(
77 | widget.title,
78 | maxLines: 10,
79 | style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
80 | ),
81 | myRoundButton('',
82 | width: ScreenUtil.getInstance().screenWidth / 20 * 2,
83 | height: ScreenUtil.getInstance().screenWidth / 20 * 2,
84 | icon: Icons.close,
85 | colorBack: const Color(0xFFf4f5fa), onTap: () {
86 | Navigator.pop(context);
87 | })
88 | ],
89 | ),
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/lib/app/widget/myImage.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | //封装图片加载控件,增加图片加载失败时加载默认图片
4 | class MyImage extends StatefulWidget {
5 | final String url;
6 | final BoxFit fit;
7 | final Widget error;
8 | final Widget load;
9 | final double width;
10 | final double height;
11 |
12 | MyImage(this.url,
13 | {this.width,
14 | this.height,
15 | this.fit = BoxFit.cover,
16 | this.error = const Placeholder(),
17 | this.load = const Placeholder()});
18 |
19 | @override
20 | State createState() {
21 | return _StateMyImage();
22 | }
23 | }
24 |
25 | class _StateMyImage extends State {
26 | Widget _image;
27 |
28 | @override
29 | void setState(fn) {
30 | if (mounted) {
31 | super.setState(fn);
32 | }
33 | }
34 |
35 | @override
36 | void initState() {
37 | super.initState();
38 | _image = widget.load;
39 | Image _imagex = Image.network(
40 | widget.url,
41 | width: widget.width,
42 | height: widget.height,
43 | fit: widget.fit,
44 | );
45 | var resolve = _imagex.image.resolve(ImageConfiguration.empty);
46 | resolve.addListener(ImageStreamListener((_, __) {
47 | //加载成功
48 | setState(() {
49 | _image = _imagex;
50 | });
51 | }, onChunk: (ImageChunkEvent event) {
52 | //加载中
53 | setState(() {
54 | _image = widget.load;
55 | });
56 | }, onError: (dynamic exception, StackTrace stackTrace) {
57 | //加载失败
58 | setState(() {
59 | _image = widget.error;
60 | });
61 | }));
62 | }
63 |
64 | @override
65 | Widget build(BuildContext context) {
66 | return _image;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lib/app/widget/myRoundButton.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'myText.dart';
3 |
4 | myRoundButton(String txt,
5 | {IconData icon,
6 | void Function() onTap,
7 | double height = 60,
8 | double size,
9 | double width,
10 | double radius,
11 | Color colorText = Colors.black,
12 | Color colorBack = const Color(0xFFf4f5fa)}) {
13 | return GestureDetector(
14 | onTap: onTap ??
15 | () {
16 | print('点击了$txt');
17 | },
18 | child: Container(
19 | //弹窗布局
20 | width: width ?? height * 2 / 0.7,
21 | height: height,
22 | decoration: BoxDecoration(
23 | color: colorBack,
24 | borderRadius: BorderRadiusDirectional.all(
25 | Radius.circular(radius ?? height / 2))),
26 | child: Row(
27 | mainAxisAlignment: MainAxisAlignment.center,
28 | children: [
29 | icon != null
30 | ? Icon(icon, color: colorText, size: size ?? height / 3)
31 | : SizedBox(width: 0, height: 0),
32 | myText(txt, color: colorText, size: size ?? height / 3),
33 | ],
34 | )),
35 | );
36 | }
37 |
38 | myRoundText(String txt,
39 | {IconData icon,
40 | void Function() onTap,
41 | double height = 60,
42 | double width,
43 | double radius,
44 | double size,
45 | Color colorText = Colors.black,
46 | Color colorBack = Colors.white,
47 | List gradient}) {
48 | return GestureDetector(
49 | onTap: onTap ??
50 | () {
51 | print('点击了$txt');
52 | },
53 | child: Container(
54 | //弹窗布局
55 | width: width ?? height * 2 / 0.7,
56 | height: height,
57 | alignment: Alignment.center,
58 | padding: EdgeInsets.symmetric(horizontal: radius ?? height / 2),
59 | decoration: BoxDecoration(
60 | gradient: LinearGradient(
61 | begin: Alignment.topLeft,
62 | end: Alignment.bottomRight,
63 | colors: gradient ?? [colorBack, colorBack],
64 | ),
65 | borderRadius: BorderRadiusDirectional.all(
66 | Radius.circular(radius ?? height / 2))),
67 | child: myText(txt, color: colorText, size: size ?? height / 3),
68 | ),
69 | );
70 | }
71 |
72 | myIcon(IconData icon,
73 | {void Function() onTap,
74 | double sizeBack = 40,
75 | double sizeIcon = 20,
76 | Color colorIcon = Colors.black,
77 | Color colorBack = Colors.transparent}) {
78 | return GestureDetector(
79 | behavior: HitTestBehavior.opaque,
80 | onTap: onTap ?? () {},
81 | child: Container(
82 | width: sizeBack,
83 | height: sizeBack,
84 | decoration: BoxDecoration(
85 | color: colorBack,
86 | borderRadius:
87 | BorderRadiusDirectional.all(Radius.circular(sizeBack / 2))),
88 | child: Icon(
89 | icon,
90 | size: sizeIcon,
91 | color: colorIcon,
92 | ),
93 | ));
94 | }
95 |
--------------------------------------------------------------------------------
/lib/app/widget/myText.dart:
--------------------------------------------------------------------------------
1 | import 'package:flustars/flustars.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | Widget myText(String txt,
5 | {FontWeight big = FontWeight.w400,
6 | Color color = Colors.black,
7 | double size = 16,
8 | double lSpace = 1,
9 | double wSpace = 1,
10 | TextOverflow overflow = TextOverflow.ellipsis}) {
11 | return Text(txt,
12 | overflow: overflow,
13 | style: TextStyle(
14 | letterSpacing: lSpace,
15 | wordSpacing: wSpace,
16 | fontSize: size,
17 | color: color,
18 | fontWeight: big,
19 | decoration: TextDecoration.none, //禁用字体下划线
20 | ));
21 | }
22 |
23 | Widget myLable(String lable) {
24 | return Row(
25 | children: [
26 | SizedBox(width: ScreenUtil.getInstance().screenWidth / 20),
27 | Icon(
28 | Icons.label,
29 | color: Colors.yellow,
30 | ),
31 | SizedBox(width: ScreenUtil.getInstance().screenWidth / 20),
32 | myText(lable, color: Colors.black, size: 18, big: FontWeight.bold)
33 | ],
34 | );
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/lib/app/widget/myTextField.dart:
--------------------------------------------------------------------------------
1 | import 'package:flustars/flustars.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | class MyTextField extends StatefulWidget {
5 | final Null Function(String) onSubmit;
6 | final Null Function(String) onChanged;
7 | final Function() focused;
8 | final Function() nofocus;
9 | final String hintText;
10 | final String value;
11 | final double height;
12 | final double width;
13 | final Color colorText;
14 | final Color colorBack;
15 | final bool autofocus;
16 | final TextAlign textAlign;
17 | final TextEditingController controller;
18 |
19 | MyTextField(
20 | {this.onSubmit,
21 | this.onChanged,
22 | this.focused,
23 | this.nofocus,
24 | this.hintText = '请输入...',
25 | this.height,
26 | this.width,
27 | this.textAlign = TextAlign.center,
28 | this.colorBack = const Color(0xFFf4f5fa),
29 | this.colorText = const Color(0xFFa8b3cf),
30 | this.autofocus = false,
31 | this.value,
32 | this.controller});
33 |
34 | @override
35 | _MyTextFieldState createState() => _MyTextFieldState();
36 | }
37 |
38 | class _MyTextFieldState extends State {
39 | double height;
40 | double width;
41 | TextEditingController controller = TextEditingController();
42 | bool isFocused = false;
43 | FocusNode focusNode = FocusNode();
44 | bool firstBuild = true;
45 | String nowValue;
46 |
47 | @override
48 | void initState() {
49 | super.initState();
50 | height = widget.height ?? ScreenUtil.getInstance().screenWidth/20 * 3;
51 | width = widget.width ?? ScreenUtil.getInstance().screenWidth - ScreenUtil.getInstance().screenWidth/20 * 2;
52 | focusNode.addListener(() {
53 | if (focusNode.hasFocus) {
54 | isFocused = true;
55 | if (widget.focused != null) widget.focused();
56 | print('聚焦');
57 | setState(() {});
58 | } else {
59 | isFocused = false;
60 | print('失焦');
61 | if (widget.nofocus != null) widget.nofocus();
62 | setState(() {});
63 | }
64 | });
65 | if (widget.controller == null && widget.value != null) {
66 | controller.text = widget.value;
67 | }
68 | }
69 |
70 | @override
71 | void dispose() {
72 | // widget.controller?.dispose();
73 | controller?.dispose();
74 | focusNode?.dispose();
75 | super.dispose();
76 | }
77 |
78 | @override
79 | Widget build(BuildContext context) {
80 | return Container(
81 | alignment: Alignment.center,
82 | height: height,
83 | width: width,
84 | decoration: BoxDecoration(
85 | color: widget.colorBack,
86 | borderRadius: BorderRadius.all(Radius.circular(height / 2)),
87 | ),
88 | padding: EdgeInsets.symmetric(horizontal: height / 2),
89 | child: TextField(
90 | controller: widget.controller ?? controller,
91 | textAlign: widget.textAlign ?? TextAlign.center, //文字居中
92 | autofocus: widget.autofocus, //自动打开软键盘
93 | focusNode: focusNode,
94 | onChanged: widget.onChanged ??
95 | (String txt) async {
96 | print(txt);
97 | },
98 | onSubmitted: widget.onSubmit ??
99 | (String txt) async {
100 | print(txt);
101 | },
102 | keyboardType: TextInputType.text,
103 | obscureText: false, //是否输入密码
104 | textInputAction: TextInputAction.done, //完成
105 | style: TextStyle(
106 | //输入文字样式,决定光标的高度
107 | fontSize: height / 3,
108 | fontWeight: FontWeight.w500,
109 | color: Colors.black),
110 | decoration: InputDecoration(
111 | border: InputBorder.none, //无边框
112 | hintText: isFocused ? '' : widget.hintText,
113 | hintStyle: TextStyle(
114 | fontSize: height / 3,
115 | fontWeight: FontWeight.w400,
116 | color: widget.colorText),
117 | ),
118 | //光标颜色
119 | cursorColor: Colors.black,
120 | ),
121 | );
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/lib/app/widget/myTips.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | //弹窗-->消息提示
4 |
5 | Future myTips(context, title, words) async {
6 | return showDialog(
7 | context: context,
8 | child: new AlertDialog(
9 | title: Text(title,style: TextStyle(color: Colors.red),),
10 | content: Text(words),
11 | actions: [
12 | FlatButton(
13 | onPressed: () {
14 | Navigator.of(context).pop();
15 | },
16 | child: Text("确定",style: TextStyle(color: Colors.blue))),
17 | ],
18 | ));
19 | }
20 |
--------------------------------------------------------------------------------
/lib/app/widget/myToast.dart:
--------------------------------------------------------------------------------
1 |
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:fluttertoast/fluttertoast.dart';
5 |
6 | void myToast(String name) {
7 | Fluttertoast.showToast(
8 | msg: name,
9 | toastLength: Toast.LENGTH_SHORT,
10 | gravity: ToastGravity.CENTER,
11 | backgroundColor: Colors.blue,
12 | textColor: Colors.white,
13 | fontSize: 12.0);
14 | }
--------------------------------------------------------------------------------
/lib/generated_plugin_registrant.dart:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | // ignore: unused_import
6 | import 'dart:ui';
7 |
8 | import 'package:fluttertoast/fluttertoast_web.dart';
9 | import 'package:shared_preferences_web/shared_preferences_web.dart';
10 | import 'package:video_player_web/video_player_web.dart';
11 |
12 | import 'package:flutter_web_plugins/flutter_web_plugins.dart';
13 |
14 | void registerPlugins(PluginRegistry registry) {
15 | FluttertoastWebPlugin.registerWith(registry.registrarFor(FluttertoastWebPlugin));
16 | SharedPreferencesPlugin.registerWith(registry.registrarFor(SharedPreferencesPlugin));
17 | VideoPlayerPlugin.registerWith(registry.registrarFor(VideoPlayerPlugin));
18 | registry.registerMessageHandler();
19 | }
20 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:feiyu/pages/home.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter/services.dart';
4 |
5 | void main() {
6 | runApp(MyApp());
7 | //透明状态栏
8 | SystemUiOverlayStyle style = SystemUiOverlayStyle(
9 | statusBarColor: Colors.transparent,
10 | systemNavigationBarDividerColor: null,
11 | systemNavigationBarColor: Colors.transparent,
12 | systemNavigationBarIconBrightness: Brightness.light,
13 | statusBarIconBrightness: Brightness.light,
14 | );
15 | SystemChrome.setSystemUIOverlayStyle(style);
16 | }
17 |
18 | class MyApp extends StatelessWidget {
19 | @override
20 | Widget build(BuildContext context) {
21 | return MaterialApp(
22 | title: '飞鱼',
23 | theme: ThemeData(
24 | primaryColor: Colors.white,
25 | ),
26 | home: Home());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/pages/detail.dart:
--------------------------------------------------------------------------------
1 | import 'package:feiyu/app/model/movie.dart';
2 | import 'package:feiyu/app/model/player.dart';
3 | import 'package:feiyu/app/widget/myBlurButton.dart';
4 | import 'package:feiyu/app/widget/myImage.dart';
5 | import 'package:feiyu/app/widget/myRoundButton.dart';
6 | import 'package:feiyu/app/widget/myText.dart';
7 | import 'package:feiyu/pages/playx.dart';
8 | import 'package:feiyu/pages/share.dart';
9 | import 'package:flustars/flustars.dart';
10 | import 'package:flutter/material.dart';
11 |
12 | class Detail extends StatefulWidget {
13 | final Movie movie;
14 | final Player player;
15 | Detail(this.movie, {@required this.player});
16 | @override
17 | _DetailState createState() => _DetailState(this.movie);
18 | }
19 |
20 | class _DetailState extends State {
21 | Movie movie;
22 | Playlist selected;
23 |
24 | double screenWidth = ScreenUtil.getInstance().screenWidth;
25 | // 屏幕高
26 | double screenHeight = ScreenUtil.getInstance().screenHeight;
27 | _DetailState(this.movie);
28 |
29 | @override
30 | void initState() {
31 | super.initState();
32 | selected = movie.playlist[0];
33 | }
34 |
35 | void share() async {
36 | await Navigator.push(
37 | context,
38 | MaterialPageRoute(
39 | builder: (BuildContext context) => Share(
40 | title: movie.title,
41 | cover: movie.cover,
42 | m3u8: selected.m3u8,
43 | name: selected.name,
44 | player: widget.player),
45 | ),
46 | );
47 | }
48 |
49 | void play() async {
50 | await Navigator.push(
51 | context,
52 | MaterialPageRoute(
53 | builder: (BuildContext context) =>
54 | PlayX(movie, selected, player: widget.player),
55 | ),
56 | );
57 | //从播放页返回时直接返回上一页
58 | Navigator.pop(context);
59 | }
60 |
61 | Widget header() {
62 | return Container(
63 | padding: EdgeInsets.all(screenWidth / 20),
64 | child: Row(
65 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
66 | children: [
67 | myBlurButton(Icons.keyboard_backspace, onTap: () {
68 | Navigator.pop(context);
69 | }),
70 | myBlurButton(Icons.share, onTap: share),
71 | ],
72 | ));
73 | }
74 |
75 | Widget myItem(Playlist playlist) {
76 | var flag = selected == playlist;
77 | return GestureDetector(
78 | onTap: () {
79 | setState(() {
80 | //播放
81 | selected = playlist;
82 | });
83 | play();
84 | },
85 | child: Container(
86 | padding: EdgeInsets.all(10),
87 | decoration: BoxDecoration(
88 | color: flag ? Colors.yellow : const Color(0xFFf4f5fa),
89 | borderRadius: BorderRadiusDirectional.all(Radius.circular(10))),
90 | child: myText(playlist.name,
91 | size: 12,
92 | color: flag ? Colors.black : Colors.black,
93 | big: flag ? FontWeight.bold : null)),
94 | );
95 | }
96 |
97 | @override
98 | Widget build(BuildContext context) {
99 | return Scaffold(
100 | backgroundColor: Colors.white,
101 | body: Stack(children: [
102 | //背景图
103 | Hero(
104 | tag: movie.cover + 'bg',
105 | child: Container(
106 | color: Colors.black54,
107 | width: screenWidth,
108 | child: MyImage(
109 | movie.cover,
110 | fit: BoxFit.cover,
111 | ),
112 | )),
113 | ListView(
114 | children: [
115 | Container(
116 | width: screenWidth,
117 | height: screenWidth * 1.6 - screenWidth * 0.15,
118 | child: Stack(children: [
119 | //标题
120 | Positioned(
121 | top: screenWidth * 1 - screenWidth * 0.0,
122 | left: screenWidth / 10 + screenWidth / 3,
123 | child: Container(
124 | color: Colors.black54,
125 | width: screenWidth * 0.52 + 20,
126 | padding: EdgeInsets.symmetric(horizontal: 10),
127 | child: Text('${movie.title}',
128 | maxLines: 2,
129 | overflow: TextOverflow.ellipsis,
130 | style: TextStyle(
131 | color: Colors.white,
132 | fontSize: 26,
133 | fontWeight: FontWeight.bold,
134 | )),
135 | )),
136 | //异形底部
137 | Positioned(
138 | top: screenWidth - screenWidth * 0.0,
139 | child: ClipPath(
140 | clipper: ArcClipper(),
141 | child: Container(
142 | width: screenWidth,
143 | height: screenWidth * 0.3,
144 | color: Colors.white,
145 | ),
146 | )),
147 | //底部背景色
148 | Positioned(
149 | top: screenWidth + screenWidth * 0.3 - screenWidth * 0.0,
150 | child: Container(
151 | width: screenWidth,
152 | height: screenWidth / 6,
153 | color: Colors.white,
154 | child: SizedBox()),
155 | ),
156 | //播放按钮
157 | Positioned(
158 | top: screenWidth * 1.21 - screenWidth * 0.0,
159 | left: screenWidth / 20 * 13,
160 | child: Material(
161 | elevation: 18,
162 | shadowColor: Colors.yellowAccent,
163 | color: Colors.transparent,
164 | borderRadius: BorderRadiusDirectional.all(
165 | Radius.circular(screenWidth / 20 * 3 / 2)),
166 | child: myIcon(Icons.play_arrow,
167 | onTap: play,
168 | colorIcon: Colors.white,
169 | colorBack: Colors.yellow,
170 | sizeIcon: screenWidth / 20 * 2,
171 | sizeBack: screenWidth / 20 * 3),
172 | ),
173 | ),
174 | //完整封面
175 | Positioned(
176 | top: screenWidth * 1 - screenWidth * 0.0,
177 | left: screenWidth / 10,
178 | child: Material(
179 | elevation: 10,
180 | child: Hero(
181 | tag: movie.cover,
182 | child: Container(
183 | width: screenWidth / 4,
184 | height: screenWidth / 4 / 0.7,
185 | child: MyImage(
186 | movie.cover,
187 | fit: BoxFit.cover,
188 | ),
189 | ))),
190 | ),
191 | ]),
192 | ),
193 | Container(
194 | color: Colors.white,
195 | child: Column(
196 | children: [
197 | myLable('选集'),
198 | SizedBox(height: screenWidth / 20),
199 | Container(
200 | padding:
201 | EdgeInsets.symmetric(horizontal: screenWidth / 10),
202 | child: Wrap(
203 | spacing: 10,
204 | runSpacing: 10,
205 | children:
206 | movie.playlist.map((e) => myItem(e)).toList(),
207 | ),
208 | ),
209 | SizedBox(height: screenWidth / 20),
210 | myLable('简介'),
211 | SizedBox(height: screenWidth / 20),
212 | Container(
213 | padding:
214 | EdgeInsets.symmetric(horizontal: screenWidth / 10),
215 | child: Text(
216 | movie.desp,
217 | maxLines: 10,
218 | overflow: TextOverflow.ellipsis,
219 | style: TextStyle(
220 | color: Colors.black,
221 | fontSize: 14,
222 | letterSpacing: 1.5),
223 | ),
224 | ),
225 | SizedBox(height: screenWidth / 10),
226 | ],
227 | )),
228 | ],
229 | ),
230 | SafeArea(child: header()),
231 | ]));
232 | }
233 | }
234 |
235 | class ArcClipper extends CustomClipper {
236 | @override
237 | Path getClip(Size size) {
238 | var path = Path();
239 | path.lineTo(0.0, size.height);
240 | path.lineTo(size.width, size.height);
241 | var firstControlPoint = Offset(size.width / 3, size.height);
242 | var firstPoint = Offset(0, 0);
243 | path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy,
244 | firstPoint.dx, firstPoint.dy);
245 | path.close();
246 |
247 | return path;
248 | }
249 |
250 | @override
251 | bool shouldReclip(CustomClipper oldClipper) => false;
252 | }
253 |
--------------------------------------------------------------------------------
/lib/pages/home.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'dart:ui';
4 | import 'package:sensors/sensors.dart';
5 |
6 | import 'package:feiyu/app/config/config.dart';
7 | import 'package:feiyu/app/model/movie.dart';
8 | import 'package:feiyu/app/model/player.dart';
9 | import 'package:feiyu/app/model/push.dart';
10 | import 'package:feiyu/app/model/site.dart';
11 | import 'package:feiyu/app/tool/http.dart';
12 | import 'package:feiyu/app/tool/random.dart';
13 | import 'package:feiyu/app/widget/myBlurButton.dart';
14 | import 'package:feiyu/app/widget/myBottomInput.dart';
15 | import 'package:feiyu/app/widget/myBottomTip.dart';
16 | import 'package:feiyu/app/widget/myImage.dart';
17 | import 'package:feiyu/app/widget/myText.dart';
18 | import 'package:feiyu/app/widget/myTips.dart';
19 | import 'package:feiyu/pages/searchList.dart';
20 | import 'package:flustars/flustars.dart';
21 | import 'package:flutter/material.dart';
22 |
23 | import 'detail.dart';
24 |
25 | class Home extends StatefulWidget {
26 | final Http http;
27 | final Push push;
28 | final Player player;
29 | final List sites;
30 | final List movies;
31 | final bool loading;
32 |
33 | Home(
34 | {this.http,
35 | this.push,
36 | this.player,
37 | this.sites,
38 | this.movies,
39 | this.loading});
40 |
41 | @override
42 | _HomeState createState() => _HomeState();
43 | }
44 |
45 | class _HomeState extends State {
46 | Http http;
47 | double screenWidth, screenHeight;
48 |
49 | Push push;
50 | Player player;
51 | List sites = [];
52 | List movies = [];
53 | bool loading = true;
54 |
55 | List _accelerometerValues;
56 | List _userAccelerometerValues;
57 | List _gyroscopeValues;
58 | List> _streamSubscriptions =
59 | >[];
60 |
61 | @override
62 | void dispose() {
63 | super.dispose();
64 | for (StreamSubscription subscription in _streamSubscriptions) {
65 | subscription.cancel();
66 | }
67 | }
68 |
69 | @override
70 | void initState() {
71 | super.initState();
72 | screenWidth = ScreenUtil.getInstance().screenWidth;
73 | screenHeight = ScreenUtil.getInstance().screenHeight;
74 | _streamSubscriptions
75 | .add(accelerometerEvents.listen((AccelerometerEvent event) {
76 | setState(() {
77 | _accelerometerValues = [event.x, event.y, event.z];
78 | });
79 | }));
80 | _streamSubscriptions.add(gyroscopeEvents.listen((GyroscopeEvent event) {
81 | setState(() {
82 | _gyroscopeValues = [event.x, event.y, event.z];
83 | });
84 | }));
85 | _streamSubscriptions
86 | .add(userAccelerometerEvents.listen((UserAccelerometerEvent event) {
87 | setState(() {
88 | _userAccelerometerValues = [event.x, event.y, event.z];
89 | });
90 | }));
91 | if (widget.movies == null) {
92 | Future.delayed(Duration.zero, () async {
93 | await load(context); //获取推送通知
94 | });
95 | } else {
96 | reopen();
97 | setState(() {});
98 | }
99 | }
100 |
101 | sensor() {
102 | final List accelerometer =
103 | _accelerometerValues?.map((double v) => v.toStringAsFixed(1))?.toList();
104 | final List gyroscope =
105 | _gyroscopeValues?.map((double v) => v.toStringAsFixed(1))?.toList();
106 | final List userAccelerometer = _userAccelerometerValues
107 | ?.map((double v) => v.toStringAsFixed(1))
108 | ?.toList();
109 | return Column(
110 | children: [
111 | Text('Accelerometer: $accelerometer'),
112 | Text('gyroscope: $gyroscope'),
113 | Text('userAccelerometer: $userAccelerometer'),
114 | ],
115 | );
116 | }
117 |
118 | reopen() {
119 | http = widget.http;
120 | push = widget.push;
121 | player = widget.player;
122 | sites = widget.sites;
123 | movies = widget.movies;
124 | loading = widget.loading;
125 | }
126 |
127 | Future load(context) async {
128 | http = Http();
129 | //获取推送
130 | String result = await http.get('$SERVER/$APP/push.json');
131 | if (result.contains('请求失败')) {
132 | push = Push.fromJson(jsonDecode(PUSH));
133 | await myTips(context, "通知", push.info);
134 | if (push.force.contains('是')) return;
135 | } else {
136 | push = Push.fromJson(jsonDecode(result));
137 | if (push.flag.contains('开')) {
138 | await myTips(context, "通知", push.info);
139 | if (push.force.contains('是')) return;
140 | }
141 | }
142 | //获取网页播放器
143 | result = await http.get('$SERVER/$APP/player.json');
144 | if (result.contains('请求失败')) {
145 | player = Player.fromJson(jsonDecode(PLAYER));
146 | } else {
147 | player = Player.fromJson(jsonDecode(result));
148 | }
149 | //获取首页电影
150 | result = await http.get('$SERVER/$APP/movies.json');
151 | if (result.contains('请求失败')) {
152 | movies = jsonDecode(MOVIES).map((e) => Movie.fromJson(e)).toList();
153 | } else {
154 | movies = jsonDecode(result).map((e) => Movie.fromJson(e)).toList();
155 | }
156 | //获取资源站
157 | result = await http.get('$SERVER/$APP/sites.json');
158 | if (result.contains('请求失败')) {
159 | sites = jsonDecode(SITES).map((e) => Site.fromJson(e)).toList();
160 | } else {
161 | sites = jsonDecode(result).map((e) => Site.fromJson(e)).toList();
162 | }
163 | loading = false;
164 | //随机显示电影
165 | movies = randomMovies(movies);
166 | setState(() {});
167 | }
168 |
169 | void about() async {
170 | await myBottomTip(context,
171 | title: '关于',
172 | desp: push?.about ??
173 | '飞鱼是一个极简的播放器,它是我最近的一个Flutter项目,代码完全开源,仅供学习交流,想要了解更多信息,请关注公众号:乂乂又又。');
174 | }
175 |
176 | void search() async {
177 | var s = await myBottomInput(context,
178 | title: '搜索', hint: '请输入电影名称...', autoFocus: true);
179 | if (s != null && s.length > 0) {
180 | await Navigator.push(
181 | context,
182 | MaterialPageRoute(
183 | builder: (BuildContext context) =>
184 | SearchList(s, player: player, sites: sites),
185 | ),
186 | );
187 | Navigator.of(context).pushReplacement(MaterialPageRoute(
188 | builder: (BuildContext context) => Home(
189 | http: http,
190 | push: push,
191 | player: player,
192 | sites: sites,
193 | movies: randomMovies(movies),
194 | loading: loading,
195 | ),
196 | ));
197 | }
198 | }
199 |
200 | void play(Movie movie) async {
201 | await Navigator.push(
202 | context,
203 | MaterialPageRoute(
204 | builder: (BuildContext context) => Detail(movie, player: player),
205 | ),
206 | );
207 | }
208 |
209 | var max = 10;
210 | var big = 0.2;
211 | Widget moviePage(Movie movie) {
212 | return GestureDetector(
213 | behavior: HitTestBehavior.translucent,
214 | onTap: () {
215 | play(movie);
216 | },
217 | child: Stack(
218 | children: [
219 | AnimatedPositioned(
220 | left: screenWidth * big / 2 / max * _accelerometerValues[0],
221 | right:
222 | screenWidth * big / 2 / max * _accelerometerValues[0] * -1,
223 | top:
224 | screenHeight * big / 2 / max * _accelerometerValues[1] * -1,
225 | bottom: screenHeight * big / 2 / max * _accelerometerValues[1],
226 | duration: const Duration(milliseconds: 90),
227 | curve: Curves.linear,
228 | child: Hero(
229 | tag: movie.cover,
230 | child: Container(
231 | width: screenWidth * (1 + big),
232 | height: screenHeight * (1 + big),
233 | child: MyImage(movie.cover, fit: BoxFit.cover),
234 | ))),
235 | Align(
236 | alignment: Alignment.center,
237 | child: Container(
238 | width: screenWidth,
239 | height: screenHeight,
240 | alignment: Alignment.center,
241 | padding: EdgeInsets.all(screenWidth / 20),
242 | child: Column(
243 | mainAxisAlignment: MainAxisAlignment.center,
244 | children: [
245 | Expanded(flex: 1, child: SizedBox(width: 0, height: 0)),
246 | // sensor(),
247 | Container(
248 | margin: EdgeInsets.all(screenWidth / 20),
249 | child: Icon(Icons.play_circle_filled,
250 | size: 64, color: Colors.white),
251 | ),
252 | Expanded(flex: 1, child: SizedBox(width: 0, height: 0)),
253 | GestureDetector(
254 | behavior: HitTestBehavior.translucent,
255 | onTap: () async {
256 | await myBottomTip(context,
257 | title: movie.title, desp: movie.desp);
258 | },
259 | child: Icon(Icons.keyboard_arrow_up,
260 | size: screenWidth / 10 * 1, color: Colors.white)),
261 | GestureDetector(
262 | behavior: HitTestBehavior.translucent,
263 | onTap: () async {
264 | await myBottomTip(context,
265 | title: movie.title, desp: movie.desp);
266 | },
267 | child: ClipRRect(
268 | borderRadius:
269 | BorderRadius.circular(screenWidth / 20),
270 | child: BackdropFilter(
271 | filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
272 | child: Container(
273 | padding: EdgeInsets.only(
274 | bottom: screenWidth / 20,
275 | left: screenWidth / 20,
276 | right: screenWidth / 20,
277 | top: screenWidth / 20 / 2),
278 | decoration: BoxDecoration(
279 | color: Colors.white.withOpacity(0.1),
280 | borderRadius: BorderRadius.circular(
281 | screenWidth / 20)),
282 | child: Column(
283 | mainAxisSize: MainAxisSize.min,
284 | crossAxisAlignment:
285 | CrossAxisAlignment.center,
286 | children: [
287 | Text(
288 | movie.title,
289 | maxLines: 1,
290 | overflow: TextOverflow.ellipsis,
291 | style: TextStyle(
292 | color: Colors.white,
293 | fontSize: 20,
294 | fontWeight: FontWeight.bold),
295 | ),
296 | SizedBox(height: 5),
297 | Text(
298 | movie.desp,
299 | maxLines: 3,
300 | overflow: TextOverflow.ellipsis,
301 | style: TextStyle(
302 | color: Colors.white,
303 | fontSize: 12),
304 | )
305 | ],
306 | ))))),
307 | ],
308 | ),
309 | ),
310 | )
311 | ],
312 | ));
313 | }
314 |
315 | Widget header() {
316 | return Container(
317 | padding: EdgeInsets.all(screenWidth / 20),
318 | child: Row(
319 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
320 | children: [
321 | myBlurButton(Icons.sort, onTap: about),
322 | myBlurButton(Icons.search, onTap: search),
323 | ],
324 | ));
325 | }
326 |
327 | Widget loadingPage() {
328 | return Container(
329 | width: screenWidth,
330 | color: Colors.white,
331 | child: Column(
332 | children: [
333 | Expanded(child: SizedBox()),
334 | CircularProgressIndicator(),
335 | SizedBox(height: 24),
336 | myText('初始化...', color: Colors.black),
337 | Expanded(child: SizedBox()),
338 | ],
339 | ));
340 | }
341 |
342 | @override
343 | Widget build(BuildContext context) {
344 | return Scaffold(
345 | backgroundColor: Colors.white,
346 | body: Container(
347 | color: Colors.black54,
348 | child: loading
349 | ? loadingPage()
350 | : Stack(children: [
351 | OverflowBox(
352 | maxWidth: screenWidth * (1 + big),
353 | maxHeight: screenHeight * (1 + big),
354 | child: PageView(
355 | physics: BouncingScrollPhysics(),
356 | scrollDirection: Axis.vertical,
357 | children: movies
358 | .map((movie) => moviePage(movie))
359 | .toList(),
360 | )),
361 | SafeArea(child: header()),
362 | ]),
363 | ));
364 | }
365 |
366 | List randomMovies(List old) {
367 | int len = old.length;
368 | List newList = new List()..length = len;
369 | List order = old.map((e) => old.indexOf(e)).toList()..sort();
370 | for (var i = 0; i < len; i++) {
371 | var index = randomListItem(order);
372 | newList[i] = old[index];
373 | order.remove(index);
374 | }
375 | return newList;
376 | }
377 | }
378 |
--------------------------------------------------------------------------------
/lib/pages/play.dart:
--------------------------------------------------------------------------------
1 | import 'package:feiyu/app/model/movie.dart';
2 | import 'package:feiyu/app/widget/myBottomTip.dart';
3 | import 'package:flutter/material.dart';
4 | import 'video/video_player_UI.dart';
5 |
6 | class Play extends StatelessWidget {
7 | final Playlist playlist;
8 | Play(this.playlist);
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return Scaffold(
13 | backgroundColor: Colors.black,
14 | body: VideoPlayerUI.network(
15 | url: playlist.m3u8,
16 | title: playlist.name,
17 | share: () async {
18 | await myBottomTip(context,
19 | title: '关于', desp: '飞鱼是一个极简的播放器,它是我最近的一个Flutter项目。');
20 | },
21 | full: (bool full) async {
22 | await myBottomTip(context,
23 | title: '关于', desp: full ? '全屏--》未全屏' : '未全屏--》全屏');
24 | },
25 | ),
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/pages/playx.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 | import 'package:feiyu/app/model/movie.dart';
3 | import 'package:feiyu/app/model/player.dart';
4 | import 'package:feiyu/app/widget/myBottomSheet.dart';
5 | import 'package:feiyu/app/widget/myText.dart';
6 | import 'package:feiyu/pages/share.dart';
7 | import 'package:feiyu/pages/tv.dart';
8 | import 'package:flustars/flustars.dart';
9 | import 'package:flutter/material.dart';
10 | import 'package:screen/screen.dart';
11 | import 'package:video_player/video_player.dart';
12 | import 'package:feiyu/pages/video/controller_widget.dart';
13 | import 'package:feiyu/pages/video/video_player_control.dart';
14 | import 'package:feiyu/pages/video/video_player_pan.dart';
15 |
16 | class PlayX extends StatefulWidget {
17 | final Movie movie;
18 | final Playlist playlist;
19 | final Player player;
20 | PlayX(this.movie, this.playlist, {@required this.player});
21 |
22 | @override
23 | _PlayXState createState() => _PlayXState();
24 | }
25 |
26 | class _PlayXState extends State {
27 | Playlist selected;
28 | final GlobalKey _key =
29 | GlobalKey();
30 |
31 | ///指示video资源是否加载完成,加载完成后会获得总时长和视频长宽比等信息
32 | bool _videoInit = false;
33 | bool _videoError = false;
34 | bool changing = false;
35 |
36 | VideoPlayerController _controller; // video控件管理器
37 |
38 | /// 记录是否全屏
39 | bool get _isFullScreen =>
40 | MediaQuery.of(context).orientation == Orientation.landscape;
41 |
42 | Size get _window => MediaQueryData.fromWindow(window).size;
43 |
44 | DateTime _tempTime = DateTime.now();
45 |
46 | share() async {
47 | if (changing) return;
48 | await _controller?.pause();
49 | await Navigator.push(
50 | context,
51 | MaterialPageRoute(
52 | builder: (BuildContext context) => Share(
53 | title: widget.movie.title,
54 | cover: widget.movie.cover,
55 | m3u8: selected.m3u8,
56 | name: selected.name,
57 | player: widget.player),
58 | ),
59 | );
60 | await _controller?.play();
61 | }
62 |
63 | shareTV() async {
64 | if (changing) return;
65 | await _controller?.pause();
66 | await Navigator.push(context, MyBottomSheet(child: TV(playlist: selected)));
67 | await _controller?.play();
68 | }
69 |
70 | full(bool fullx) {
71 | setState(() {}); //刷新界面
72 | }
73 |
74 | @override
75 | Widget build(BuildContext context) {
76 | var screen = ScreenUtil.getInstance();
77 | var screenWidth = screen.screenWidth;
78 | return Scaffold(
79 | backgroundColor: Colors.white,
80 | body: ListView(padding: EdgeInsets.zero, children: [
81 | Offstage(
82 | offstage: _isFullScreen,
83 | child: SafeArea(
84 | child: AppBar(
85 | elevation: 0,
86 | centerTitle: true,
87 | title: Text(
88 | widget.movie.title,
89 | overflow: TextOverflow.ellipsis,
90 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
91 | ),
92 | actions: [
93 | IconButton(
94 | icon: Icon(Icons.share),
95 | onPressed: share,
96 | ),
97 | ],
98 | ))),
99 | Container(
100 | color: Colors.black,
101 | width: !_isFullScreen ? _window.width : _window.width,
102 | height: !_isFullScreen ? _window.width / 16 * 9 : _window.height,
103 | child: _isHadUrl(),
104 | ),
105 | Offstage(
106 | offstage: _isFullScreen,
107 | child: Container(
108 | height: _isFullScreen
109 | ? 0
110 | : _window.height -
111 | _window.width / 16 * 9 -
112 | screen.appBarHeight -
113 | screen.statusBarHeight,
114 | child: ListView(
115 | physics: BouncingScrollPhysics(),
116 | padding: EdgeInsets.zero,
117 | children: [
118 | SizedBox(height: screenWidth / 20),
119 | myLable('选集'),
120 | SizedBox(height: screenWidth / 20),
121 | Container(
122 | alignment: Alignment.center,
123 | padding:
124 | EdgeInsets.symmetric(horizontal: screenWidth / 10),
125 | child: Wrap(
126 | spacing: 10,
127 | runSpacing: 10,
128 | children: widget.movie.playlist
129 | .map((e) => myItem(e))
130 | .toList(),
131 | ),
132 | ),
133 | SizedBox(height: screenWidth / 20),
134 | myLable('简介'),
135 | SizedBox(height: screenWidth / 20),
136 | Container(
137 | padding:
138 | EdgeInsets.symmetric(horizontal: screenWidth / 10),
139 | child: Text(
140 | widget.movie.desp,
141 | maxLines: 10,
142 | overflow: TextOverflow.ellipsis,
143 | style: TextStyle(
144 | color: Colors.black,
145 | fontSize: 14,
146 | letterSpacing: 1.5),
147 | ),
148 | ),
149 | SizedBox(height: screenWidth / 10),
150 | ]),
151 | )),
152 | ]),
153 | );
154 | }
155 |
156 | Widget myItem(Playlist playlist) {
157 | var flag = selected == playlist;
158 | return GestureDetector(
159 | onTap: () async {
160 | setState(() {
161 | selected = playlist;
162 | });
163 | await _urlChange();
164 | },
165 | child: Container(
166 | padding: EdgeInsets.all(10),
167 | decoration: BoxDecoration(
168 | color: flag ? Colors.yellow : const Color(0xFFf4f5fa),
169 | borderRadius: BorderRadiusDirectional.all(Radius.circular(10))),
170 | child: myText(playlist.name,
171 | size: 12,
172 | color: flag ? Colors.black : Colors.black,
173 | big: flag ? FontWeight.bold : null)),
174 | );
175 | }
176 |
177 | // 判断是否有url
178 | Widget _isHadUrl() {
179 | return ControllerWidget(
180 | controlKey: _key,
181 | controller: _controller,
182 | videoInit: _videoInit,
183 | title: selected.name,
184 | child: VideoPlayerPan(
185 | share: shareTV,
186 | full: full,
187 | child: Container(
188 | alignment: Alignment.center,
189 | width: double.infinity,
190 | height: double.infinity,
191 | color: Colors.black,
192 | child: _isVideoInit(),
193 | ),
194 | ),
195 | );
196 | }
197 |
198 | // 加载url成功时,根据视频比例渲染播放器
199 | Widget _isVideoInit() {
200 | if (_videoInit) {
201 | if (_videoError) {
202 | return Text(
203 | '加载失败,请重试~',
204 | style: TextStyle(color: Colors.white),
205 | );
206 | }
207 | return AspectRatio(
208 | aspectRatio: _controller?.value?.aspectRatio,
209 | child: VideoPlayer(_controller),
210 | );
211 | } else if (_controller != null && !changing) {
212 | return Text(
213 | '加载失败,请重试~',
214 | style: TextStyle(color: Colors.white),
215 | );
216 | } else {
217 | return Column(
218 | mainAxisAlignment: MainAxisAlignment.center,
219 | children: [
220 | SizedBox(
221 | width: 30,
222 | height: 30,
223 | child: CircularProgressIndicator(),
224 | ),
225 | SizedBox(height: 30),
226 | Text(
227 | '加载中...',
228 | style: TextStyle(color: Colors.white),
229 | )
230 | ],
231 | );
232 | }
233 | }
234 |
235 | void _videoListener() async {
236 | if (_controller?.value?.hasError ?? false) {
237 | _videoError = true;
238 | changing = false;
239 | print(_controller?.value?.errorDescription);
240 | }
241 | if (_controller?.value?.initialized ?? false) {
242 | changing = false;
243 | }
244 | if (_controller?.value?.isBuffering ?? false) {}
245 | if (DateTime.now().difference(_tempTime).inMilliseconds > 999) {
246 | // Duration res = await _controller?.position;
247 | if (_controller?.value?.isPlaying ?? false) {
248 | _key.currentState?.setPosition(
249 | position: _controller?.value?.position ?? Duration.zero,
250 | totalDuration: _controller?.value?.duration ?? Duration.zero,
251 | );
252 | }
253 | _tempTime = DateTime.now();
254 | }
255 | setState(() {});
256 | }
257 |
258 | @override
259 | void initState() {
260 | super.initState();
261 | selected = widget.playlist;
262 | _urlChange(); // 初始进行一次url加载
263 | Screen.keepOn(true); // 设置屏幕常亮
264 | }
265 |
266 | @override
267 | void dispose() {
268 | super.dispose();
269 | _controller?.removeListener(_videoListener);
270 | _controller?.dispose();
271 | Screen.keepOn(false);
272 | }
273 |
274 | _urlChange() async {
275 | setState(() {
276 | changing = true;
277 | });
278 | var old = _controller;
279 | if (_controller != null) {
280 | /// 如果控制器存在,清理掉重新创建
281 | _controller?.removeListener(_videoListener);
282 | await _controller?.pause();
283 | }
284 | _controller = VideoPlayerController.network(selected.m3u8);
285 | setState(() {
286 | /// 重置组件参数
287 | _videoInit = false;
288 | _videoError = false;
289 | });
290 |
291 | /// 加载资源完成时,监听播放进度,并且标记_videoInit=true加载完成
292 | _controller?.addListener(_videoListener);
293 | try {
294 | await _controller?.initialize();
295 | _key.currentState?.setPosition(
296 | position: Duration(seconds: 0),
297 | totalDuration: _controller?.value?.duration ?? Duration(seconds: 0),
298 | );
299 | setState(() {
300 | _videoInit = true;
301 | _videoError = false;
302 | _controller?.play();
303 | });
304 | await old?.dispose();
305 | } catch (e) {
306 | setState(() {
307 | _videoInit = true;
308 | _videoError = true;
309 | _controller?.pause();
310 | });
311 | }
312 | }
313 | }
314 |
--------------------------------------------------------------------------------
/lib/pages/searchList.dart:
--------------------------------------------------------------------------------
1 | import 'package:feiyu/app/model/movie.dart';
2 | import 'package:feiyu/app/model/player.dart';
3 | import 'package:feiyu/app/model/site.dart';
4 | import 'package:feiyu/app/tool/search.dart';
5 | import 'package:feiyu/app/widget/myBottomInput.dart';
6 | import 'package:feiyu/app/widget/myImage.dart';
7 | import 'package:feiyu/app/widget/myText.dart';
8 | import 'package:flustars/flustars.dart';
9 | import 'package:flutter/material.dart';
10 |
11 | import 'detail.dart';
12 |
13 | class SearchList extends StatefulWidget {
14 | final String title;
15 | final Player player;
16 | final List sites;
17 | SearchList(this.title, {@required this.sites, @required this.player});
18 | @override
19 | _SearchListState createState() => _SearchListState();
20 | }
21 |
22 | class _SearchListState extends State
23 | with SingleTickerProviderStateMixin {
24 | TabController _tabController;
25 | List _tabs = [];
26 | bool loading = false;
27 | bool api = true;
28 |
29 | List movies = [];
30 | String keyword = '';
31 | List> tabMovies = [];
32 | double screenWidth = ScreenUtil.getInstance().screenWidth;
33 | double screenHeight = ScreenUtil.getInstance().screenHeight;
34 |
35 | @override
36 | void initState() {
37 | super.initState();
38 | keyword = widget.title;
39 | _tabs = widget.sites.map((site) => site.name).toList();
40 | _tabs.sort(); //排序
41 | tabMovies = new List>(_tabs.length);
42 | //tab切换controller
43 | _tabController = new TabController(vsync: this, length: _tabs.length);
44 | setState(() {}); //刷新界面
45 | setListener();
46 | search(); //首次搜索
47 | }
48 |
49 | @override
50 | void dispose() {
51 | super.dispose();
52 | _tabController.dispose();
53 | }
54 |
55 | void setListener() {
56 | _tabController.addListener(() async {
57 | var i = _tabController.index;
58 | //监听页面被切换时
59 | if (tabMovies[i] != null) {
60 | //再次切换
61 | movies = tabMovies[i];
62 | setState(() {});
63 | return;
64 | }
65 | //首次切换
66 | await search();
67 | });
68 |
69 | ///监听TabController的动画,实时刷新,这样选中背景就能跟随移动了
70 | _tabController.animation.addListener(() {
71 | if (!(_tabController.animation.value % 1 == 0)) {
72 | loading = true;
73 | } else {
74 | loading = false;
75 | }
76 | setState(() {});
77 | });
78 | }
79 |
80 | Future search() async {
81 | loading = true;
82 | setState(() {});
83 | String siteName = _tabs[_tabController.index];
84 | if (api) {
85 | movies = await Search.api(keyword,
86 | widget.sites[widget.sites.indexWhere((s) => s.name == siteName)]);
87 | } else {
88 | movies = await Search.web(keyword,
89 | widget.sites[widget.sites.indexWhere((s) => s.name == siteName)]);
90 | }
91 | tabMovies[_tabController.index] = movies;
92 | loading = false;
93 | setState(() {});
94 | }
95 |
96 | onTapSearch() async {
97 | keyword = '';
98 | var s = await myBottomInput(context,
99 | title: '搜索', hint: '请输入电影名称...', autoFocus: true);
100 | if (s != null) {
101 | keyword = s;
102 | //重置搜索结果
103 | tabMovies = new List>(_tabs.length);
104 | await search();
105 | }
106 | }
107 |
108 | void play(Movie movie) async {
109 | Navigator.push(
110 | context,
111 | MaterialPageRoute(
112 | builder: (BuildContext context) => Detail(movie, player: widget.player),
113 | ),
114 | );
115 | }
116 |
117 | Widget myMovieItem(Movie movie) {
118 | return GestureDetector(
119 | behavior: HitTestBehavior.translucent,
120 | onTap: () {
121 | play(movie);
122 | },
123 | child: Container(
124 | margin: EdgeInsets.all(screenWidth / 20 * 0.5),
125 | child: ListTile(
126 | leading: Hero(
127 | tag: movie.cover + 'bg',
128 | child: Container(
129 | width: screenWidth / 20 * 2.4,
130 | height: screenWidth / 20 * 4,
131 | child: MyImage(movie.cover, fit: BoxFit.cover))),
132 | title: Text(
133 | movie.title,
134 | maxLines: 1,
135 | overflow: TextOverflow.ellipsis,
136 | style: TextStyle(
137 | color: Colors.black, fontSize: 15, fontWeight: FontWeight.bold),
138 | ),
139 | subtitle: Text(
140 | movie.desp,
141 | maxLines: 2,
142 | overflow: TextOverflow.ellipsis,
143 | style: TextStyle(color: Colors.black54, fontSize: 12),
144 | ),
145 | trailing: Container(
146 | margin: EdgeInsets.all(screenWidth / 20),
147 | child:
148 | Icon(Icons.play_circle_filled, size: 32, color: Colors.yellow),
149 | ),
150 | ),
151 | ),
152 | );
153 | }
154 |
155 | Widget loadingPage() {
156 | return Container(
157 | width: screenWidth,
158 | color: Colors.white,
159 | child: Column(
160 | children: [
161 | Expanded(child: SizedBox()),
162 | Offstage(
163 | offstage: !loading,
164 | child: CircularProgressIndicator(),
165 | ),
166 | SizedBox(height: 24),
167 | myText(loading ? '搜索中...' : '啥都没搜到~', color: Colors.black),
168 | Expanded(flex: 2, child: SizedBox()),
169 | ],
170 | ));
171 | }
172 |
173 | Widget myBody() {
174 | return (loading || movies.length < 1)
175 | ? loadingPage()
176 | : RefreshIndicator(
177 | onRefresh: search,
178 | child: ListView.builder(
179 | physics: BouncingScrollPhysics(),
180 | itemCount: movies.length,
181 | itemBuilder: (BuildContext context, int index) {
182 | if (index == movies.length - 1)
183 | return Container(
184 | height: screenHeight,
185 | alignment: Alignment.topCenter,
186 | child: myMovieItem(movies[index]));
187 | return myMovieItem(movies[index]);
188 | }));
189 | }
190 |
191 | @override
192 | Widget build(BuildContext context) {
193 | return DefaultTabController(
194 | length: _tabs.length, // This is the number of tabs.
195 | child: Scaffold(
196 | appBar: AppBar(
197 | elevation: 0,
198 | centerTitle: true,
199 | title: myText(keyword,
200 | size: 18, overflow: TextOverflow.ellipsis, big: FontWeight.bold),
201 | actions: [
202 | IconButton(
203 | icon: myText(api ? 'A' : 'W', size: 16, big: FontWeight.bold),
204 | onPressed: () async {
205 | setState(() {
206 | api = !api;
207 | });
208 | //重新搜索
209 | await search();
210 | })
211 | ],
212 | bottom: TabBar(
213 | isScrollable: true,
214 | indicatorColor: Colors.yellow,
215 | indicatorSize: TabBarIndicatorSize.label,
216 | indicatorPadding: EdgeInsets.all(20),
217 | indicatorWeight: 10,
218 | labelColor: Colors.black,
219 | labelStyle: TextStyle(fontWeight: FontWeight.bold),
220 | unselectedLabelColor: Colors.black54,
221 | controller: this._tabController,
222 | tabs: _tabs.map((String name) => Tab(text: name)).toList(),
223 | ),
224 | ),
225 | body: TabBarView(
226 | controller: this._tabController,
227 | children: _tabs.map((String name) {
228 | return SafeArea(
229 | top: false,
230 | bottom: false,
231 | child: Builder(
232 | builder: (BuildContext context) {
233 | return myBody();
234 | },
235 | ),
236 | );
237 | }).toList(),
238 | ),
239 | floatingActionButton: FloatingActionButton(
240 | backgroundColor: Colors.black,
241 | onPressed: onTapSearch,
242 | child: Icon(Icons.search,color: Colors.white),
243 | ),
244 | ),
245 | );
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/lib/pages/share.dart:
--------------------------------------------------------------------------------
1 | import 'package:feiyu/app/model/player.dart';
2 | import 'package:feiyu/app/tool/decode.dart';
3 | import 'package:feiyu/app/widget/myImage.dart';
4 | import 'package:feiyu/app/widget/myText.dart';
5 | import 'package:feiyu/app/widget/myToast.dart';
6 | import 'package:flustars/flustars.dart';
7 | import 'package:flutter/material.dart';
8 | import 'package:flutter/services.dart';
9 | import 'package:qr_flutter/qr_flutter.dart';
10 |
11 | class Share extends StatelessWidget {
12 | final Player player; //在线播放器
13 | final String title; //标题
14 | final String cover; //封面
15 | final String name; //剧集
16 | final String m3u8;
17 |
18 | Share({this.title, this.name, this.m3u8, this.cover, this.player});
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | var screenWidth = ScreenUtil.getInstance().screenWidth;
23 | var screenHeight = ScreenUtil.getInstance().screenHeight;
24 | var wh = 1 / 0.7;
25 | var imgW = screenWidth * 0.6;
26 | var imgH = imgW * wh;
27 | var pSize = screenWidth / 20;
28 | var pBottom = (screenHeight - imgH - imgW) / 2 + imgW - pSize;
29 | var pMargin = (screenWidth - imgW) / 2 - pSize;
30 | var pColor = Colors.yellow;
31 | var flag = player.public.contains('是');
32 | var qrstr =
33 | '${player.server}?${player.m3u8}=${flag ? m3u8 : encodex(m3u8)}&${player.title}=${flag ? title : encodex(title)}';
34 | return Scaffold(
35 | backgroundColor: Colors.yellow,
36 | body: Container(
37 | child: Stack(
38 | children: [
39 | //返回
40 | Align(
41 | alignment: Alignment.topLeft,
42 | child: SafeArea(
43 | child: IconButton(
44 | iconSize: 36,
45 | padding: EdgeInsets.all(screenWidth / 20),
46 | icon: Icon(
47 | Icons.keyboard_backspace,
48 | color: Colors.white,
49 | size: 36,
50 | ),
51 | onPressed: () {
52 | Navigator.pop(context);
53 | })),
54 | ),
55 | //分享
56 | Align(
57 | alignment: Alignment.topRight,
58 | child: SafeArea(
59 | child: IconButton(
60 | iconSize: 36,
61 | padding: EdgeInsets.all(screenWidth / 20),
62 | icon: Icon(
63 | Icons.share,
64 | color: Colors.white,
65 | size: 36,
66 | ),
67 | onPressed: () {
68 | Clipboard.setData(
69 | ClipboardData(text: '$title $name\n播放地址:$qrstr'));
70 | myToast('已复制');
71 | })),
72 | ),
73 | //body
74 | Align(
75 | alignment: Alignment.center,
76 | child: Column(
77 | // shrinkWrap: true,
78 | mainAxisAlignment: MainAxisAlignment.center,
79 | children: [
80 | ClipRRect(
81 | borderRadius: BorderRadius.circular(pSize / 2),
82 | child: Container(
83 | width: imgW, height: imgH, child: MyImage(cover))),
84 | Container(
85 | width: imgW,
86 | height: imgW,
87 | padding: EdgeInsets.all(screenWidth / 20),
88 | decoration: BoxDecoration(
89 | color: Colors.white,
90 | borderRadius: BorderRadius.circular(pSize / 2)),
91 | child: Column(
92 | mainAxisSize: MainAxisSize.min,
93 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
94 | children: [
95 | myText(title, big: FontWeight.bold, size: 16),
96 | QrImage(
97 | data: qrstr,
98 | version: QrVersions.auto,
99 | size: screenWidth * 0.36,
100 | errorStateBuilder: (cxt, err) {
101 | return Container(
102 | child: Center(
103 | child: Text(
104 | "糟糕,出错了...",
105 | textAlign: TextAlign.center,
106 | ),
107 | ),
108 | );
109 | },
110 | ),
111 | myText('扫码播放→' + name,
112 | big: FontWeight.bold, size: 14),
113 | ]),
114 | )
115 | ],
116 | ),
117 | ),
118 | Positioned(
119 | bottom: pBottom,
120 | left: pMargin,
121 | child: Container(
122 | width: pSize * 2,
123 | height: pSize * 2,
124 | decoration: BoxDecoration(
125 | color: pColor,
126 | borderRadius: BorderRadius.circular(pSize)),
127 | )),
128 | Positioned(
129 | bottom: pBottom,
130 | right: pMargin,
131 | child: Container(
132 | width: pSize * 2,
133 | height: pSize * 2,
134 | decoration: BoxDecoration(
135 | color: pColor,
136 | borderRadius: BorderRadius.circular(pSize)),
137 | ))
138 | ],
139 | ),
140 | ),
141 | );
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/lib/pages/tv.dart:
--------------------------------------------------------------------------------
1 | import 'package:dlna/dlna.dart';
2 | import 'package:feiyu/app/model/movie.dart';
3 | import 'package:feiyu/app/widget/myRoundButton.dart';
4 | import 'package:feiyu/app/widget/myText.dart';
5 | import 'package:feiyu/app/widget/myToast.dart';
6 | import 'package:flustars/flustars.dart';
7 | import 'package:flutter/material.dart';
8 |
9 | class TV extends StatefulWidget {
10 | final Playlist playlist;
11 |
12 | TV({this.playlist});
13 | @override
14 | _TVState createState() => _TVState();
15 | }
16 |
17 | class _TVState extends State {
18 | bool searching = true;
19 | var screenWidth = ScreenUtil.getInstance().screenWidth;
20 | var screenHeight = ScreenUtil.getInstance().screenHeight;
21 |
22 | DLNAManager dlnaManager;
23 | List _devices = [];
24 | VideoObject _didlObject;
25 | DLNADevice _dlnaDevice;
26 | String actionMessage = '';
27 |
28 | @override
29 | void initState() {
30 | super.initState();
31 | Future.delayed(Duration.zero).then((value) async => await init());
32 | }
33 |
34 | Future init() async {
35 | //初始化DLNAManager
36 | dlnaManager = DLNAManager();
37 | // dlnaManager.enableCache();
38 | //设置视频对象
39 | _didlObject = VideoObject(
40 | widget.playlist?.name ?? '爱的迫降',
41 | widget.playlist?.m3u8 ??
42 | 'https://txxs.mahua-yongjiu.com/20191214/7845_6fcff130/index.m3u8',
43 | VideoObject.VIDEO_MP4);
44 | _didlObject.refreshPosition = true;
45 | //监听DLNAManager
46 | dlnaManager.setRefresher(DeviceRefresher(onDeviceAdd: (dlnaDevice) {
47 | if (!_devices.contains(dlnaDevice)) {
48 | print('add ' + dlnaDevice.toString());
49 | _devices.add(dlnaDevice);
50 | searching = false;
51 | }
52 | setState(() {});
53 | }, onDeviceRemove: (dlnaDevice) {
54 | print('remove ' + dlnaDevice.toString());
55 | _devices.remove(dlnaDevice);
56 | setState(() {});
57 | }, onDeviceUpdate: (dlnaDevice) {
58 | print('update ' + dlnaDevice.toString());
59 | setState(() {});
60 | }, onSearchError: (error) {
61 | print('error ' + error);
62 | }, onPlayProgress: (positionInfo) {
63 | print(_time2Str(DateTime.now().millisecondsSinceEpoch) +
64 | ' current play progress ' +
65 | positionInfo.relTime);
66 | }));
67 | // 开始搜索
68 | dlnaManager.startSearch();
69 | setState(() {});
70 | }
71 |
72 | play(index) async {
73 | //选择设备
74 | _dlnaDevice = _devices[index];
75 | dlnaManager.setDevice(_dlnaDevice);
76 | //设置链接
77 | var result = await dlnaManager.actSetVideoUrl(_didlObject);
78 | if (!result.success) {
79 | myToast('投屏失败');
80 | Navigator.pop(context);
81 | }
82 | //播放
83 | var re = await dlnaManager.actPlay();
84 | if (!re.success) {
85 | myToast('投屏失败');
86 | Navigator.pop(context);
87 | }
88 | myToast('投屏成功');
89 | Navigator.pop(context);
90 | }
91 |
92 | @override
93 | void dispose() {
94 | super.dispose();
95 | dlnaManager?.release(); //关闭连接
96 | }
97 |
98 | @override
99 | Widget build(BuildContext context) {
100 | var flag = screenHeight <= screenWidth;
101 | return Scaffold(
102 | backgroundColor: Colors.black54,
103 | body: GestureDetector(
104 | behavior: HitTestBehavior.translucent,
105 | onTap: () {
106 | Navigator.pop(context);
107 | },
108 | child: Container(
109 | color: Colors.black54,
110 | child: Stack(
111 | children: [
112 | //body
113 | Align(
114 | alignment: Alignment.center,
115 | child: Material(
116 | borderRadius: BorderRadius.circular(screenWidth / 20),
117 | child: GestureDetector(
118 | behavior: HitTestBehavior.translucent,
119 | onTap: () {},
120 | child: Container(
121 | width:
122 | (flag ? screenHeight * 1 : screenWidth) * 0.8,
123 | height: (flag ? screenHeight * 0.8 : screenWidth) *
124 | (0.8 / 9 * 11),
125 | padding: EdgeInsets.all(
126 | (flag ? screenHeight : screenWidth) / 20),
127 | decoration: BoxDecoration(
128 | color: Colors.white,
129 | borderRadius:
130 | BorderRadius.circular(screenWidth / 20)),
131 | child: Column(
132 | children: [
133 | myText('投屏', big: FontWeight.bold),
134 | Container(
135 | padding: EdgeInsets.all(
136 | (flag ? screenHeight : screenWidth) / 20),
137 | child: Text(
138 | "*请选择一个要投屏的设备",
139 | textAlign: TextAlign.center,
140 | style: TextStyle(
141 | color: Colors.orange,
142 | ),
143 | ),
144 | ),
145 | Expanded(
146 | child: searching || _devices.length < 1
147 | ? searchW()
148 | : ListView.builder(
149 | padding: EdgeInsets.zero,
150 | physics: BouncingScrollPhysics(),
151 | itemCount: _devices.length,
152 | itemBuilder: (_, index) {
153 | return GestureDetector(
154 | behavior:
155 | HitTestBehavior.translucent,
156 | onTap: () async {
157 | await play(index);
158 | },
159 | child: ListTile(
160 | title: Text(
161 | _devices[index].deviceName,
162 | textAlign: TextAlign.center,
163 | style: TextStyle(
164 | fontSize: 14,
165 | fontWeight:
166 | FontWeight.bold),
167 | ),
168 | trailing: myIcon(
169 | Icons.play_circle_filled,
170 | colorIcon: Colors.yellow,
171 | onTap: () async {
172 | await play(index);
173 | }),
174 | ),
175 | );
176 | })),
177 | SizedBox(
178 | height:
179 | (flag ? screenHeight : screenWidth) /
180 | 20),
181 | Row(
182 | mainAxisAlignment:
183 | MainAxisAlignment.spaceAround,
184 | children: [
185 | myRoundButton('取消', onTap: () {
186 | Navigator.pop(context);
187 | },
188 | colorText: Colors.red,
189 | size: 16,
190 | height: 36),
191 | myRoundButton('确定', onTap: () {
192 | Navigator.pop(context);
193 | },
194 | colorText: Colors.blue,
195 | size: 16,
196 | height: 36)
197 | ],
198 | )
199 | ],
200 | ),
201 | ))),
202 | ),
203 | ],
204 | ),
205 | )),
206 | );
207 | }
208 |
209 | Widget searchW() {
210 | return Center(
211 | child: Container(
212 | child: Column(
213 | mainAxisAlignment: MainAxisAlignment.center,
214 | children: [
215 | SizedBox(
216 | width: 30,
217 | height: 30,
218 | child: CircularProgressIndicator(),
219 | ),
220 | SizedBox(height: 30),
221 | Text(
222 | '搜索中...',
223 | style: TextStyle(color: Colors.black),
224 | )
225 | ],
226 | ),
227 | ),
228 | );
229 | }
230 |
231 | String _time2Str(int intTime) {
232 | var time = DateTime.fromMillisecondsSinceEpoch(intTime);
233 | return "${time.year.toString()}-${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}";
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/lib/pages/video/after_layout.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | mixin AfterLayoutMixin on State {
4 | @override
5 | void initState() {
6 | super.initState();
7 | WidgetsBinding.instance
8 | .addPostFrameCallback((_) => afterFirstLayout(context));
9 | }
10 |
11 | void afterFirstLayout(BuildContext context);
12 | }
--------------------------------------------------------------------------------
/lib/pages/video/controller_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:video_player/video_player.dart';
3 |
4 | import 'video_player_control.dart';
5 |
6 | class ControllerWidget extends InheritedWidget {
7 | ControllerWidget({
8 | this.controlKey,
9 | this.child,
10 | this.controller,
11 | this.videoInit,
12 | this.title
13 | });
14 |
15 | final String title;
16 | final GlobalKey controlKey;
17 | final Widget child;
18 | final VideoPlayerController controller;
19 | final bool videoInit;
20 |
21 | //定义一个便捷方法,方便子树中的widget获取共享数据
22 | static ControllerWidget of(BuildContext context) {
23 | return context.dependOnInheritedWidgetOfExactType();
24 | }
25 |
26 | @override
27 | bool updateShouldNotify(InheritedWidget oldWidget) {
28 | return false;
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/lib/pages/video/video_player_UI.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 | import 'dart:ui';
3 | import 'package:flutter/material.dart';
4 | import 'package:screen/screen.dart';
5 | import 'package:video_player/video_player.dart';
6 | import 'controller_widget.dart';
7 | import 'video_player_control.dart';
8 | import 'video_player_pan.dart';
9 |
10 | enum VideoPlayerType { network, asset, file }
11 |
12 | class VideoPlayerUI extends StatefulWidget {
13 | final Function share;
14 | final Function(bool) full;
15 | VideoPlayerUI.network({
16 | Key key,
17 | this.share,
18 | this.full,
19 | @required String url, // 当前需要播放的地址
20 | this.width: double.infinity, // 播放器尺寸(大于等于视频播放区域)
21 | this.height: double.infinity,
22 | this.title = '', // 视频需要显示的标题
23 | }) : type = VideoPlayerType.network,
24 | url = url,
25 | super(key: key);
26 |
27 | VideoPlayerUI.asset({
28 | Key key,
29 | this.share,
30 | this.full,
31 | @required String dataSource, // 当前需要播放的地址
32 | this.width: double.infinity, // 播放器尺寸(大于等于视频播放区域)
33 | this.height: double.infinity,
34 | this.title = '', // 视频需要显示的标题
35 | }) : type = VideoPlayerType.asset,
36 | url = dataSource,
37 | super(key: key);
38 |
39 | VideoPlayerUI.file({
40 | Key key,
41 | this.share,
42 | this.full,
43 | @required File file, // 当前需要播放的地址
44 | this.width: double.infinity, // 播放器尺寸(大于等于视频播放区域)
45 | this.height: double.infinity,
46 | this.title = '', // 视频需要显示的标题
47 | }) : type = VideoPlayerType.file,
48 | url = file,
49 | super(key: key);
50 |
51 | final url;
52 | final VideoPlayerType type;
53 | final double width;
54 | final double height;
55 | final String title;
56 |
57 | @override
58 | _VideoPlayerUIState createState() => _VideoPlayerUIState();
59 | }
60 |
61 | class _VideoPlayerUIState extends State {
62 | final GlobalKey _key =
63 | GlobalKey();
64 |
65 | ///指示video资源是否加载完成,加载完成后会获得总时长和视频长宽比等信息
66 | bool _videoInit = false;
67 | bool _videoError = false;
68 |
69 | VideoPlayerController _controller; // video控件管理器
70 |
71 | /// 记录是否全屏
72 | bool get _isFullScreen =>
73 | MediaQuery.of(context).orientation == Orientation.landscape;
74 |
75 | Size get _window => MediaQueryData.fromWindow(window).size;
76 |
77 | DateTime _tempTime = DateTime.now();
78 |
79 | void _videoListener() async {
80 | if (_controller.value.hasError) {
81 | setState(() {
82 | _videoError = true;
83 | });
84 | } else if (DateTime.now().difference(_tempTime).inMilliseconds > 999) {
85 | _tempTime = DateTime.now();
86 | Duration res = await _controller.position;
87 | if (res >= _controller.value.duration) {
88 | await _controller.seekTo(Duration(seconds: 0));
89 | await _controller.pause();
90 | }
91 | if (_controller.value.isPlaying && _key.currentState != null) {
92 | _key.currentState.setPosition(
93 | position: res,
94 | totalDuration: _controller.value.duration,
95 | );
96 | }
97 | }
98 | }
99 |
100 | @override
101 | void initState() {
102 | super.initState();
103 | _urlChange(); // 初始进行一次url加载
104 | Screen.keepOn(true); // 设置屏幕常亮
105 | }
106 |
107 | @override
108 | void didUpdateWidget(VideoPlayerUI oldWidget) {
109 | if (oldWidget.url != widget.url) {
110 | _urlChange(); // url变化时重新执行一次url加载
111 | }
112 | super.didUpdateWidget(oldWidget);
113 | }
114 |
115 | @override
116 | void dispose() async {
117 | super.dispose();
118 | if (_controller != null) {
119 | _controller.removeListener(_videoListener);
120 | _controller.dispose();
121 | }
122 | Screen.keepOn(false);
123 | }
124 |
125 | @override
126 | Widget build(BuildContext context) {
127 | return SafeArea(
128 | top: !_isFullScreen,
129 | bottom: !_isFullScreen,
130 | left: !_isFullScreen,
131 | right: !_isFullScreen,
132 | child: Container(
133 | width: _isFullScreen ? _window.width : widget.width,
134 | height: _isFullScreen ? _window.height : widget.height,
135 | child: _isHadUrl(),
136 | ),
137 | );
138 | }
139 |
140 | // 判断是否有url
141 | Widget _isHadUrl() {
142 | if (widget.url != null) {
143 | return ControllerWidget(
144 | controlKey: _key,
145 | controller: _controller,
146 | videoInit: _videoInit,
147 | title: widget.title,
148 | child: VideoPlayerPan(
149 | share: widget.share,
150 | full: widget.full,
151 | child: Container(
152 | alignment: Alignment.center,
153 | width: double.infinity,
154 | height: double.infinity,
155 | color: Colors.black,
156 | child: _isVideoInit(),
157 | ),
158 | ),
159 | );
160 | } else {
161 | return Center(
162 | child: Text(
163 | '暂无视频信息',
164 | style: TextStyle(color: Colors.white),
165 | ),
166 | );
167 | }
168 | }
169 |
170 | // 加载url成功时,根据视频比例渲染播放器
171 | Widget _isVideoInit() {
172 | if (_videoInit) {
173 | return AspectRatio(
174 | aspectRatio: _controller.value.aspectRatio,
175 | child: VideoPlayer(_controller),
176 | );
177 | } else if (_controller != null && _videoError) {
178 | return Text(
179 | '加载出错',
180 | style: TextStyle(color: Colors.white),
181 | );
182 | } else {
183 | return SizedBox(
184 | width: 30,
185 | height: 30,
186 | child: CircularProgressIndicator(
187 | strokeWidth: 2,
188 | ),
189 | );
190 | }
191 | }
192 |
193 | void _urlChange() async {
194 | if (widget.url == null || widget.url == '') return;
195 | if (_controller != null) {
196 | /// 如果控制器存在,清理掉重新创建
197 | _controller.removeListener(_videoListener);
198 | _controller.dispose();
199 | }
200 | setState(() {
201 | /// 重置组件参数
202 | _videoInit = false;
203 | _videoError = false;
204 | });
205 | if (widget.type == VideoPlayerType.file) {
206 | _controller = VideoPlayerController.file(widget.url);
207 | } else if (widget.type == VideoPlayerType.asset) {
208 | _controller = VideoPlayerController.asset(widget.url);
209 | } else {
210 | _controller = VideoPlayerController.network(widget.url);
211 | }
212 |
213 | /// 加载资源完成时,监听播放进度,并且标记_videoInit=true加载完成
214 | _controller.addListener(_videoListener);
215 | await _controller.initialize();
216 | setState(() {
217 | _videoInit = true;
218 | _videoError = false;
219 | _controller.play();
220 | });
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/lib/pages/video/video_player_control.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:auto_orientation/auto_orientation.dart';
4 | import 'package:feiyu/app/tool/time.dart';
5 | import 'package:flutter/material.dart';
6 | import 'package:flutter/services.dart';
7 | import 'package:video_player/video_player.dart';
8 |
9 | import 'controller_widget.dart';
10 | import 'video_player_slider.dart';
11 |
12 | class VideoPlayerControl extends StatefulWidget {
13 | final Function share;
14 | final Function(bool) full;
15 | VideoPlayerControl({
16 | Key key,
17 | this.share,
18 | this.full,
19 | }) : super(key: key);
20 |
21 | @override
22 | VideoPlayerControlState createState() => VideoPlayerControlState();
23 | }
24 |
25 | class VideoPlayerControlState extends State {
26 | VideoPlayerController get controller =>
27 | ControllerWidget.of(context).controller;
28 | bool get videoInit => ControllerWidget.of(context).videoInit;
29 | String get title => ControllerWidget.of(context).title;
30 | // 记录video播放进度
31 | Duration _position = Duration(seconds: 0);
32 | Duration _totalDuration = Duration(seconds: 0);
33 | Timer _timer; // 计时器,用于延迟隐藏控件ui
34 | bool _hidePlayControl = true; // 控制是否隐藏控件ui
35 | double _playControlOpacity = 0; // 通过透明度动画显示/隐藏控件ui
36 | /// 记录是否全屏
37 | bool get _isFullScreen =>
38 | MediaQuery.of(context).orientation == Orientation.landscape;
39 | bool lock = false; //是否锁定
40 |
41 | @override
42 | void dispose() {
43 | super.dispose();
44 | if (_timer != null) {
45 | _timer.cancel();
46 | }
47 | }
48 |
49 | @override
50 | Widget build(BuildContext context) {
51 | return GestureDetector(
52 | onDoubleTap: _playOrPause,
53 | onTap: _togglePlayControl,
54 | child: Container(
55 | width: double.infinity,
56 | height: double.infinity,
57 | color: Colors.transparent,
58 | child: WillPopScope(
59 | child: Offstage(
60 | offstage: _hidePlayControl,
61 | child: AnimatedOpacity(
62 | // 加入透明度动画
63 | opacity: _playControlOpacity,
64 | duration: Duration(milliseconds: 300),
65 | child: Column(
66 | children: [
67 | Offstage(offstage: lock, child: _top()),
68 | _middle(),
69 | Offstage(offstage: lock, child: _bottom(context))
70 | ],
71 | ),
72 | ),
73 | ),
74 | onWillPop: _onWillPop,
75 | ),
76 | ),
77 | );
78 | }
79 |
80 | // 拦截返回键
81 | Future _onWillPop() async {
82 | if (_isFullScreen) {
83 | _toggleFullScreen();
84 | return false;
85 | }
86 | return true;
87 | }
88 |
89 | // 供父组件调用刷新页面,减少父组件的build
90 | void setPosition({position, totalDuration}) {
91 | setState(() {
92 | _position = position;
93 | _totalDuration = totalDuration;
94 | });
95 | }
96 |
97 | Widget _bottom(BuildContext context) {
98 | return Container(
99 | // 底部控件的容器
100 | width: double.infinity,
101 | height: 40,
102 | decoration: BoxDecoration(
103 | gradient: LinearGradient(
104 | // 来点黑色到透明的渐变优雅一下
105 | begin: Alignment.bottomCenter,
106 | end: Alignment.topCenter,
107 | colors: [Color.fromRGBO(0, 0, 0, .7), Color.fromRGBO(0, 0, 0, .1)],
108 | ),
109 | ),
110 | child: Row(
111 | // 加载完成时才渲染,flex布局
112 | children: [
113 | IconButton(
114 | // 播放按钮
115 | padding: EdgeInsets.zero,
116 | iconSize: 26,
117 | icon: Icon(
118 | // 根据控制器动态变化播放图标还是暂停
119 | controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
120 | color: Colors.white,
121 | ),
122 | onPressed: _playOrPause,
123 | ),
124 | Expanded(
125 | // 相当于前端的flex: 1
126 | child: VideoPlayerSlider(
127 | startPlayControlTimer: _startPlayControlTimer,
128 | timer: _timer,
129 | ),
130 | ),
131 | Container(
132 | // 播放时间
133 | margin: EdgeInsets.only(left: 10),
134 | child: Text(
135 | '${_position == null ? '00:00' : videoTime(_position.inMilliseconds)}/${_totalDuration == null ? '00:00' : videoTime(_totalDuration.inMilliseconds)}',
136 | style: TextStyle(color: Colors.white),
137 | ),
138 | ),
139 | IconButton(
140 | // 全屏/横屏按钮
141 | padding: EdgeInsets.zero,
142 | iconSize: 26,
143 | icon: Icon(
144 | // 根据当前屏幕方向切换图标
145 | _isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen,
146 | color: Colors.white,
147 | ),
148 | onPressed: () {
149 | // 点击切换是否全屏
150 | _toggleFullScreen();
151 | },
152 | ),
153 | ],
154 | ),
155 | );
156 | }
157 |
158 | Widget myIcon(IconData icon, {Function onTap, double size = 36}) {
159 | return Container(
160 | margin: EdgeInsets.all(10),
161 | padding: EdgeInsets.all(10),
162 | decoration: BoxDecoration(
163 | color: Colors.black54,
164 | borderRadius: BorderRadiusDirectional.all(Radius.circular(size))),
165 | child: GestureDetector(
166 | onTap: onTap ?? () {},
167 | child: Icon(
168 | icon,
169 | size: size,
170 | color: Colors.white,
171 | ),
172 | ),
173 | );
174 | }
175 |
176 | Widget _middle() {
177 | return Expanded(
178 | child: Row(
179 | children: [
180 | myIcon(lock ? Icons.lock_open : Icons.lock_outline, size: 24,
181 | onTap: () {
182 | setState(() {
183 | lock = !lock;
184 | });
185 | }),
186 | Expanded(child: SizedBox(width: 0)),
187 | Offstage(
188 | offstage: lock,
189 | child: myIcon(
190 | // 根据控制器动态变化播放图标还是暂停
191 | controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
192 | onTap: _playOrPause,
193 | )),
194 | Expanded(child: SizedBox(width: 0)),
195 | Offstage(
196 | offstage: lock,
197 | child: myIcon(Icons.live_tv,
198 | size: 24, onTap: widget.share ?? () {})),
199 | ],
200 | ),
201 | );
202 | }
203 |
204 | Widget _top() {
205 | return Container(
206 | width: double.infinity,
207 | height: 40,
208 | decoration: BoxDecoration(
209 | gradient: LinearGradient(
210 | // 来点黑色到透明的渐变优雅一下
211 | begin: Alignment.bottomCenter,
212 | end: Alignment.topCenter,
213 | colors: [Color.fromRGBO(0, 0, 0, .7), Color.fromRGBO(0, 0, 0, .1)],
214 | ),
215 | ),
216 | child: Row(
217 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
218 | children: [
219 | //在最上层或者不是横屏则隐藏按钮
220 | ModalRoute.of(context).isFirst && !_isFullScreen
221 | ? Container()
222 | : IconButton(
223 | icon: Icon(
224 | Icons.arrow_back,
225 | color: Colors.white,
226 | ),
227 | onPressed: backPress),
228 | Text(
229 | title,
230 | style: TextStyle(color: Colors.white),
231 | ),
232 | //在最上层或者不是横屏则隐藏按钮
233 | ModalRoute.of(context).isFirst && !_isFullScreen
234 | ? Container()
235 | : IconButton(
236 | icon: Icon(
237 | Icons.arrow_back,
238 | color: Colors.transparent,
239 | ),
240 | onPressed: () {},
241 | ),
242 | ],
243 | ),
244 | );
245 | }
246 |
247 | void backPress() {
248 | print(_isFullScreen);
249 | // 如果是全屏,点击返回键则关闭全屏,如果不是,则系统返回键
250 | if (_isFullScreen) {
251 | _toggleFullScreen();
252 | } else if (ModalRoute.of(context).isFirst) {
253 | SystemNavigator.pop();
254 | } else {
255 | Navigator.pop(context);
256 | }
257 | }
258 |
259 | void _playOrPause() async {
260 | if (lock) return;
261 |
262 | /// 同样的,点击动态播放或者暂停
263 | if (videoInit) {
264 | controller.value.isPlaying
265 | ? await controller.pause()
266 | : await controller.play();
267 | setState(() {}); //更新界面
268 | _startPlayControlTimer(); // 操作控件后,重置延迟隐藏控件的timer
269 | }
270 | }
271 |
272 | void _togglePlayControl() {
273 | setState(() {
274 | if (_hidePlayControl) {
275 | /// 如果隐藏则显示
276 | _hidePlayControl = false;
277 | _playControlOpacity = 1;
278 | _startPlayControlTimer(); // 开始计时器,计时后隐藏
279 | } else {
280 | /// 如果显示就隐藏
281 | if (_timer != null) _timer.cancel(); // 有计时器先移除计时器
282 | _playControlOpacity = 0;
283 | Future.delayed(Duration(milliseconds: 500)).whenComplete(() {
284 | _hidePlayControl = true; // 延迟500ms(透明度动画结束)后,隐藏
285 | });
286 | }
287 | });
288 | }
289 |
290 | void _startPlayControlTimer() {
291 | /// 计时器,用法和前端js的大同小异
292 | _timer?.cancel();
293 | _timer = Timer(Duration(seconds: 3), () {
294 | setState(() {
295 | _playControlOpacity = 0;
296 | _hidePlayControl = true;
297 | });
298 | });
299 | }
300 |
301 | void _toggleFullScreen() {
302 | if (widget.full != null) widget.full(_isFullScreen);
303 | setState(() {
304 | if (_isFullScreen) {
305 | /// 如果是全屏就切换竖屏
306 | AutoOrientation.portraitAutoMode();
307 |
308 | ///显示状态栏,与底部虚拟操作按钮
309 | SystemChrome.setEnabledSystemUIOverlays(
310 | [SystemUiOverlay.top, SystemUiOverlay.bottom]);
311 | } else {
312 | AutoOrientation.landscapeAutoMode();
313 |
314 | ///关闭状态栏,与底部虚拟操作按钮
315 | SystemChrome.setEnabledSystemUIOverlays([]);
316 | }
317 | _startPlayControlTimer(); // 操作完控件开始计时隐藏
318 | });
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/lib/pages/video/video_player_pan.dart:
--------------------------------------------------------------------------------
1 | import 'package:feiyu/app/tool/time.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:screen/screen.dart';
4 |
5 | //import 'package:screen/screen.dart';
6 | import 'package:video_player/video_player.dart';
7 | import 'package:sys_volume/flutter_volume.dart';
8 |
9 | import 'after_layout.dart';
10 | import 'controller_widget.dart';
11 | import 'video_player_control.dart';
12 |
13 | class VideoPlayerPan extends StatefulWidget {
14 | final Function share;
15 | final Function(bool) full;
16 | VideoPlayerPan({
17 | // this.controlKey,
18 | this.child,
19 | this.share,
20 | this.full,
21 | });
22 |
23 | // final GlobalKey controlKey;
24 | final Widget child;
25 |
26 | @override
27 | _VideoPlayerPanState createState() => _VideoPlayerPanState();
28 | }
29 |
30 | class _VideoPlayerPanState extends State
31 | with AfterLayoutMixin {
32 | Offset startPosition; // 起始位置
33 | double movePan; // 偏移量累计总和
34 | double layoutWidth; // 组件宽度
35 | double layoutHeight; // 组件高度
36 | String volumePercentage = ''; // 组件位移描述
37 | double playDialogOpacity = 0.0;
38 | bool allowHorizontal = false; // 是否允许快进
39 | Duration position = Duration(seconds: 0); // 当前时间
40 | double volume = 0.0;
41 | double brightness = 0.0; //亮度
42 | bool brightnessOk = false; // 是否允许调节亮度
43 |
44 | VideoPlayerController get controller =>
45 | ControllerWidget.of(context).controller;
46 | bool get videoInit => ControllerWidget.of(context).videoInit;
47 | String get title => ControllerWidget.of(context).title;
48 |
49 | @override
50 | void afterFirstLayout(BuildContext context) {
51 | _reset(context);
52 | Future.delayed(Duration.zero).then((value) async {
53 | FlutterVolume.disableUI();
54 | brightness = await Screen.brightness;
55 | volume = await FlutterVolume.get();
56 | setState(() {});
57 | });
58 | }
59 |
60 | @override
61 | void dispose() {
62 | super.dispose();
63 | brightnessOk = false;
64 | allowHorizontal = false;
65 | }
66 |
67 | @override
68 | Widget build(BuildContext context) {
69 | return GestureDetector(
70 | onVerticalDragStart: _onVerticalDragStart,
71 | onVerticalDragUpdate: _onVerticalDragUpdate,
72 | onVerticalDragEnd: _onVerticalDragEnd,
73 | onHorizontalDragStart: _onHorizontalDragStart,
74 | onHorizontalDragUpdate: _onHorizontalDragUpdate,
75 | onHorizontalDragEnd: _onHorizontalDragEnd,
76 | child: Container(
77 | child: Stack(
78 | children: [
79 | widget.child,
80 | Center(
81 | child: AnimatedOpacity(
82 | opacity: playDialogOpacity,
83 | duration: Duration(milliseconds: 500),
84 | child: Container(
85 | padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 6.0),
86 | decoration: BoxDecoration(
87 | color: Colors.black87,
88 | borderRadius: BorderRadius.all(Radius.circular(5.0))),
89 | child: Text(
90 | volumePercentage,
91 | style: TextStyle(color: Colors.white, fontSize: 12),
92 | ),
93 | ),
94 | ),
95 | ),
96 | VideoPlayerControl(
97 | share: widget.share,
98 | full: widget.full,
99 | key: ControllerWidget.of(context).controlKey,
100 | )
101 | ],
102 | ),
103 | ),
104 | );
105 | }
106 |
107 | void _onVerticalDragStart(details) async {
108 | _reset(context);
109 | startPosition = details.globalPosition;
110 | if (startPosition.dx < (layoutWidth / 2)) {
111 | /// 左边触摸
112 | brightnessOk = true;
113 | }
114 | }
115 |
116 | double start = 0;
117 | void _onVerticalDragUpdate(details) {
118 | if (!videoInit) {
119 | return;
120 | }
121 |
122 | /// 累计计算偏移量(下滑减少百分比,上滑增加百分比)
123 | movePan += (-details.delta.dy);
124 | if (startPosition.dx < (layoutWidth / 2)) {
125 | /// 左边触摸
126 | if (brightnessOk = true) {
127 | setState(() {
128 | volumePercentage = '亮度:${(_setBrightnessValue() * 100).toInt()}%';
129 | playDialogOpacity = 1.0;
130 | });
131 | }
132 | } else {
133 | /// 右边触摸
134 | setState(() {
135 | volumePercentage = '音量:${(_setVerticalValue() * 100).toInt()}%';
136 | playDialogOpacity = 1.0;
137 | });
138 | }
139 | onDrag();
140 | }
141 |
142 | void _onVerticalDragEnd(_) async {
143 | brightness = await Screen.brightness;
144 | volume = await FlutterVolume.get();
145 | brightnessOk = false;
146 | playDialogOpacity = 0.0;
147 | setState(() {});
148 | }
149 |
150 | void onDrag() async {
151 | if (!videoInit) {
152 | return;
153 | }
154 | if (startPosition.dx < (layoutWidth / 2)) {
155 | if (brightnessOk) {
156 | await Screen.setBrightness(_setBrightnessValue());
157 | }
158 | } else {
159 | await FlutterVolume.set(_setVerticalValue());
160 | }
161 | }
162 |
163 | double _setBrightnessValue() {
164 | // 亮度百分控制
165 | double value =
166 | double.parse((movePan / layoutHeight + brightness).toStringAsFixed(2));
167 | if (value >= 1.00) {
168 | value = 1.00;
169 | } else if (value <= 0.00) {
170 | value = 0.00;
171 | }
172 | return value;
173 | }
174 |
175 | double _setVerticalValue() {
176 | print(movePan / layoutHeight * 100);
177 | // 声音百分控制
178 | double value =
179 | double.parse((movePan / layoutHeight + volume).toStringAsFixed(2));
180 | if (value >= 1.0) {
181 | value = 1.0;
182 | } else if (value <= 0.0) {
183 | value = 0.0;
184 | }
185 | return value;
186 | }
187 |
188 | void _reset(BuildContext context) {
189 | startPosition = Offset(0, 0);
190 | movePan = 0;
191 | layoutHeight = context.size.height;
192 | layoutWidth = context.size.width;
193 | volumePercentage = '';
194 | }
195 |
196 | void _onHorizontalDragStart(DragStartDetails details) async {
197 | _reset(context);
198 | if (!videoInit) {
199 | return;
200 | }
201 | // 获取当前时间
202 | position = controller.value.position;
203 | // 暂停成功后才允许快进手势
204 | allowHorizontal = true;
205 | await controller?.pause();
206 | }
207 |
208 | void _onHorizontalDragUpdate(DragUpdateDetails details) {
209 | if (!videoInit && !allowHorizontal) {
210 | return;
211 | }
212 | // 累计计算偏移量
213 | movePan += details.delta.dx;
214 | double value = _setHorizontalValue();
215 | // 用百分比计算出当前的秒数
216 | String currentSecond =
217 | videoTime((value * controller.value.duration?.inMilliseconds).toInt());
218 | if (value >= 0) {
219 | setState(() {
220 | volumePercentage = '快进至:$currentSecond';
221 | playDialogOpacity = 1.0;
222 | });
223 | } else {
224 | setState(() {
225 | volumePercentage = '快退至:${(value * 100).toInt()}%';
226 | playDialogOpacity = 1.0;
227 | });
228 | }
229 | }
230 |
231 | void _onHorizontalDragEnd(DragEndDetails details) async {
232 | if (!videoInit && !allowHorizontal) {
233 | return;
234 | }
235 | double value = _setHorizontalValue();
236 | int current = (value * controller.value.duration?.inMilliseconds).toInt();
237 | await controller?.seekTo(Duration(milliseconds: current));
238 | await controller?.play();
239 | allowHorizontal = false;
240 | setState(() {
241 | playDialogOpacity = 0.0;
242 | });
243 | }
244 |
245 | double _setHorizontalValue() {
246 | // 进度条百分控制
247 | double valueHorizontal =
248 | double.parse((movePan / layoutWidth).toStringAsFixed(2));
249 | // 当前进度条百分比
250 | double currentValue =
251 | position.inMilliseconds / controller.value.duration.inMilliseconds;
252 | double value =
253 | double.parse((currentValue + valueHorizontal).toStringAsFixed(2));
254 | if (value >= 1.00) {
255 | value = 1.00;
256 | } else if (value <= 0.00) {
257 | value = 0.00;
258 | }
259 | return value;
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/lib/pages/video/video_player_slider.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'package:feiyu/app/tool/time.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:video_player/video_player.dart';
5 |
6 | import 'controller_widget.dart';
7 |
8 | class VideoPlayerSlider extends StatefulWidget {
9 | final Function startPlayControlTimer;
10 | final Timer timer;
11 |
12 | VideoPlayerSlider({this.startPlayControlTimer, this.timer});
13 |
14 | @override
15 | _VideoPlayerSliderState createState() => _VideoPlayerSliderState();
16 | }
17 |
18 | class _VideoPlayerSliderState extends State {
19 | VideoPlayerController get controller =>
20 | ControllerWidget.of(context).controller;
21 |
22 | bool get videoInit => ControllerWidget.of(context).videoInit;
23 | double progressValue; //进度
24 | String labelProgress; //tip内容
25 | bool handle = false; //判断是否在滑动的标识
26 |
27 | @override
28 | void initState() {
29 | super.initState();
30 | progressValue = 0.0;
31 | labelProgress = '00:00';
32 | }
33 |
34 | @override
35 | void didUpdateWidget(VideoPlayerSlider oldWidget) {
36 | super.didUpdateWidget(oldWidget);
37 | var v = controller.value;
38 | if (!handle && videoInit && v != null) {
39 | int position = v.position == null ? 0 : v.position.inMilliseconds;
40 | int duration = v.duration == null ? 0 : v.duration.inMilliseconds;
41 | if (position >= duration) {
42 | position = duration;
43 | }
44 | setState(() {
45 | progressValue = duration == 0 ? 0 : position / duration * 100;
46 | labelProgress = videoTime(position.toInt());
47 | });
48 | }
49 | }
50 |
51 | @override
52 | Widget build(BuildContext context) {
53 | return SliderTheme(
54 | data: SliderTheme.of(context).copyWith(
55 | //提示进度的气泡文本的颜色
56 | valueIndicatorTextStyle: TextStyle(
57 | color: Colors.black,
58 | ),
59 | ),
60 | child: Slider(
61 | value: progressValue,
62 | label: labelProgress,
63 | divisions: 100,
64 | onChangeStart: _onChangeStart,
65 | onChangeEnd: _onChangeEnd,
66 | onChanged: _onChanged,
67 | min: 0,
68 | max: 100,
69 | activeColor: Colors.yellow,
70 | inactiveColor: Colors.white,
71 | ));
72 | }
73 |
74 | void _onChangeEnd(_) async{
75 | if (!videoInit) return;
76 | widget.startPlayControlTimer(); //开始计时隐藏控制组件
77 | handle = false; //未在滑动
78 | // 跳转到滑动时间
79 | int duration = controller.value.duration.inMilliseconds;
80 | controller.seekTo(
81 | Duration(milliseconds: (progressValue / 100 * duration).toInt()),
82 | );
83 | await controller.play();//开始播放
84 | }
85 |
86 | void _onChangeStart(_) async {
87 | if (!videoInit) return;
88 | widget.timer?.cancel(); //停止计时
89 | handle = true; //滑动中
90 | await controller.pause();//暂停
91 | }
92 |
93 | void _onChanged(double value) {
94 | if (!videoInit) return;
95 | widget.timer?.cancel(); //停止计时
96 | int duration = controller.value.duration.inMilliseconds;
97 | setState(() {
98 | //滑动进度
99 | progressValue = value;
100 | //气泡进度
101 | labelProgress = videoTime((value / 100 * duration).toInt());
102 | });
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: feiyu
2 | description: A new Flutter project.
3 |
4 | version: 1.0.0+1
5 |
6 | environment:
7 | sdk: ">=2.1.0 <3.0.0"
8 |
9 | dependencies:
10 | flutter:
11 | sdk: flutter
12 | cupertino_icons: ^0.1.3
13 | dio: ^3.0.9
14 | video_player: ^0.10.11+1
15 | auto_orientation: ^1.0.6
16 | screen: ^0.0.5
17 | flustars: ^0.3.2
18 | dlna: ^0.0.5
19 | fluttertoast: ^4.0.1
20 | xml2json: ^4.2.0
21 | qr_flutter: ^3.2.0
22 | sensors: ^0.4.2+2
23 |
24 | sys_volume:
25 | git:
26 | url: https://github.com/befovy/flutter_volume.git
27 |
28 |
29 | dev_dependencies:
30 | flutter_test:
31 | sdk: flutter
32 |
33 | flutter:
34 | uses-material-design: true
35 |
36 | # To add assets to your application, add an assets section, like this:
37 | # assets:
38 | # - images/a_dot_burr.jpeg
39 | # - images/a_dot_ham.jpeg
40 |
41 | # fonts:
42 | # - family: Schyler
43 | # fonts:
44 | # - asset: fonts/Schyler-Regular.ttf
45 | # - asset: fonts/Schyler-Italic.ttf
46 | # style: italic
47 | # - family: Trajan Pro
48 | # fonts:
49 | # - asset: fonts/TrajanPro.ttf
50 | # - asset: fonts/TrajanPro_Bold.ttf
51 | # weight: 700
52 | #
53 | # For details regarding fonts from package dependencies,
54 | # see https://flutter.dev/custom-fonts/#from-packages
55 |
--------------------------------------------------------------------------------
/screenshots/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/1.jpg
--------------------------------------------------------------------------------
/screenshots/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/2.jpg
--------------------------------------------------------------------------------
/screenshots/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/3.jpg
--------------------------------------------------------------------------------
/screenshots/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/4.jpg
--------------------------------------------------------------------------------
/screenshots/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/5.jpg
--------------------------------------------------------------------------------
/screenshots/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/6.jpg
--------------------------------------------------------------------------------
/screenshots/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/7.jpg
--------------------------------------------------------------------------------
/screenshots/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/8.jpg
--------------------------------------------------------------------------------
/screenshots/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/idootop/feiyu_flutter/ae046c46cff6d4ec6cf6120d0f6bff37953bdaf7/screenshots/logo.png
--------------------------------------------------------------------------------