From 9df809c41e13bb07cadf15ad0f25ab00c64bcd1f Mon Sep 17 00:00:00 2001 From: d1y Date: Thu, 31 Mar 2022 20:23:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(page):=20=E5=AE=8C=E5=96=84=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api/send.dart | 54 +++++++ .../controllers/vod_search_controller.dart | 94 ++++++++++- .../modules/vod_search/views/page_bar.dart | 145 +++++++++++------ .../vod_search/views/vod_search_view.dart | 146 +++++++++++++----- lib/modules/vod_search.dart | 40 +++++ 5 files changed, 383 insertions(+), 96 deletions(-) create mode 100644 lib/modules/vod_search.dart diff --git a/lib/api/send.dart b/lib/api/send.dart index 4a82aff..7bb4265 100644 --- a/lib/api/send.dart +++ b/lib/api/send.dart @@ -4,6 +4,8 @@ import 'package:dan211/api/parse_utils.dart'; import 'package:dan211/modules/art_detail.dart'; import 'package:dan211/modules/vod_detail.dart'; import 'package:dan211/modules/vod_play.dart' as vodplay; +import 'package:dan211/modules/vod_search.dart'; +import 'package:flutter/foundation.dart'; import 'package:html/parser.dart' as parser; import 'package:dan211/modules/vod_movie.dart'; import 'package:dan211/utils/http.dart'; @@ -128,6 +130,7 @@ class SendHttp { return vodplay.MovieVodPlay( player: VodPlayer( url: _data.url, + /// 暂时写死。。。 title: '在线播放', ), @@ -138,4 +141,55 @@ class SendHttp { // throw Error(); // } } + + static Future getSearch(PageQueryStringUtils page) async { + var resp = await XHttp.dio.get(page.toString()); + var document = parser.parseFragment(resp.data); + var listNode = document.querySelectorAll("#list-focus li"); + + List cards = listNode.map((e) { + var playIMG = e.querySelector(".play-img"); + var img = playIMG?.querySelector("img"); + var title = img?.attributes["alt"]?.trim() ?? ""; + var id = + playIMG?.attributes["href"]?.split("/")[2].split(".html")[0] ?? ""; + var image = img?.attributes["src"]?.trim() ?? ""; + return VodCard( + cover: image, + id: id, + title: title, + ); + }).toList(); + + int total = -1; + int current = -1; + int totalPage = -1; + + var pageTip = document.querySelector(".page_tip")?.text.trim() ?? ""; + if (pageTip == "共0条数据,当前/页") { + debugPrint("搜索的内容为空"); + } else { + // 共1443条数据,当前1/145页 + var _pageCache = pageTip.split("条数据"); + total = int.tryParse(_pageCache[0].substring(1)) ?? 0; + var _pageNumberCache1 = _pageCache[1].split(",当前")[1]; + var _pageNumberCache = _pageNumberCache1.substring( + 0, + _pageNumberCache1.length - 1, + ); + var _pageNumberTarget = _pageNumberCache.split("/"); + current = int.tryParse(_pageNumberTarget[0]) ?? 0; + totalPage = int.tryParse(_pageNumberTarget[1]) ?? 0; + debugPrint("total: $total\n"); + debugPrint("current: $current\n"); + debugPrint("total_page: $totalPage\n"); + } + + return VodSearchRespData( + data: cards, + total: total, + current: current, + totalPage: totalPage, + ); + } } diff --git a/lib/app/modules/vod_search/controllers/vod_search_controller.dart b/lib/app/modules/vod_search/controllers/vod_search_controller.dart index 24065a5..90d1d81 100644 --- a/lib/app/modules/vod_search/controllers/vod_search_controller.dart +++ b/lib/app/modules/vod_search/controllers/vod_search_controller.dart @@ -1,20 +1,98 @@ +import 'package:dan211/api/helper.dart'; +import 'package:dan211/api/send.dart'; +import 'package:dan211/app/modules/vod_search/views/page_bar.dart'; +import 'package:dan211/modules/vod_search.dart'; +import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; class VodSearchController extends GetxController { - //TODO: Implement VodSearchController + /// 搜索内容 + VodSearchRespData respData = VodSearchRespData( + current: -1, + total: -1, + totalPage: -1, + data: [], + ); - final count = 0.obs; - @override - void onInit() { - super.onInit(); + /// 是否加载 + bool isLoading = false; + + /// 错误栈 + String errorStack = ""; + + /// 一个 `flag` + /// 用来判断是否是第一次操作 + bool firstRun = true; + + TextEditingController searchController = TextEditingController(); + + handleSearch(String keyword) { + debugPrint("keyword: $keyword"); + pageQuery.data = keyword; + pageQuery.page = 1; + fetchData(); + } + + PageQueryStringUtils pageQuery = PageQueryStringUtils( + data: '', + ); + + bool isShowPageBar = false; + + /// 是否显示错误页面 + /// + /// 判断条件 + /// + /// [respData.data] 内容为空, 并且 [errorStack] 不为空 + bool get canShowErrorCase { + return respData.data.isEmpty && errorStack.isNotEmpty; + } + + fetchData() async { + if (pageQuery.data.isEmpty) return; + isShowPageBar = true; + isLoading = true; + update(); + try { + var data = await SendHttp.getSearch(pageQuery); + if (firstRun) { + firstRun = false; + } + isLoading = false; + respData = data; + errorStack = ""; + update(); + } catch (e) { + isLoading = false; + errorStack = e.toString(); + if (respData.data.isEmpty) { + isShowPageBar = false; + } + update(); + } + } + + handlePrevAndNext(CupertinoPageBarActionType _type) async { + var _p = pageQuery.page; + if (_type == CupertinoPageBarActionType.prev) { + pageQuery.page--; + } else { + pageQuery.page++; + } + await fetchData(); + + /// 从缓存中判断若失败了, 则回退操作的 [pageQuery.page] + if (errorStack.isNotEmpty) { + pageQuery.page = _p; + } } @override - void onReady() { - super.onReady(); + void onInit() { + super.onInit(); + // fetchData(); } @override void onClose() {} - void increment() => count.value++; } diff --git a/lib/app/modules/vod_search/views/page_bar.dart b/lib/app/modules/vod_search/views/page_bar.dart index 7ff0541..b93146b 100644 --- a/lib/app/modules/vod_search/views/page_bar.dart +++ b/lib/app/modules/vod_search/views/page_bar.dart @@ -1,77 +1,122 @@ import 'package:flutter/cupertino.dart'; +enum CupertinoPageBarActionType { + prev, + next, +} + class CupertinoPageBar extends StatelessWidget { const CupertinoPageBar({ Key? key, - this.isInit = false, this.total = 1, this.current = 1, + this.totalPage = 1, + required this.onTap, + this.isLoading = true, + this.loadingText = "搜索中", + this.prevText = "上一页", + this.nextText = "下一页", }) : super(key: key); - /// 是否初始化操作 - /// - /// [true] => 只显示 `搜索中` 文本 - /// - /// [false] => 显示操作按钮 `上一页/下一页/跳转` - final bool isInit; + final bool isLoading; + + final String loadingText; + + final String prevText; + + final String nextText; final int total; final int current; + final int totalPage; + + final ValueChanged onTap; + bool get _isPrev { - return current <= 1; + return current > 1; } bool get _isNext { - return true; + return current < totalPage; + } + + bool get _canShowToolBar => total == -1; + + MainAxisAlignment get _mainAxisAlignment { + if (_canShowToolBar) return MainAxisAlignment.center; + return MainAxisAlignment.spaceBetween; + } + + double get _opacity { + return isLoading ? 1.0 : 0.0; } @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - CupertinoButton( - padding: EdgeInsets.zero, - child: Row( - children: [ - Icon(CupertinoIcons.back), - Text("上一页"), - ], - ), - onPressed: () {}, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - ), - child: Text("1/24"), - ), - CupertinoButton( - padding: EdgeInsets.zero, - child: Row( - children: [ - Text("下一页"), - Icon(CupertinoIcons.right_chevron), - ], - ), - onPressed: () {}, + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: _mainAxisAlignment, + children: [ + Builder(builder: (context) { + if (_canShowToolBar) return const SizedBox.shrink(); + return Row( + children: [ + CupertinoButton( + padding: EdgeInsets.zero, + child: Row( + children: [ + const Icon(CupertinoIcons.back), + Text(prevText), + ], + ), + onPressed: !_isPrev ? null : () { + onTap(CupertinoPageBarActionType.prev); + }, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: Text("$current/$totalPage"), + ), + CupertinoButton( + padding: EdgeInsets.zero, + child: Row( + children: [ + Text(nextText), + const Icon(CupertinoIcons.right_chevron), + ], + ), + onPressed: !_isNext ? null : () { + onTap(CupertinoPageBarActionType.next); + }, + ), + ], + ); + }), + AnimatedOpacity( + opacity: _opacity, + duration: const Duration( + milliseconds: 420, ), - ], - ), - Row( - children: [ - CupertinoActivityIndicator(), - SizedBox( - width: 6, + child: Row( + children: [ + const CupertinoActivityIndicator(), + const SizedBox( + width: 6, + ), + Text(loadingText), + const SizedBox( + width: 3, + ), + ], ), - Text("搜索中"), - ], - ) - ], + ) + ], + ), ); } } diff --git a/lib/app/modules/vod_search/views/vod_search_view.dart b/lib/app/modules/vod_search/views/vod_search_view.dart index 10a3d01..8abb0bc 100644 --- a/lib/app/modules/vod_search/views/vod_search_view.dart +++ b/lib/app/modules/vod_search/views/vod_search_view.dart @@ -1,5 +1,8 @@ import 'package:dan211/app/modules/vod_search/views/page_bar.dart'; import 'package:dan211/app/modules/vod_search/views/w_vod_card.dart'; +import 'package:dan211/app/routes/app_pages.dart'; +import 'package:dan211/modules/vod_movie.dart'; +import 'package:dan211/widget/k_error_stack.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -7,55 +10,122 @@ import 'package:get/get.dart'; import '../controllers/vod_search_controller.dart'; -class VodSearchView extends StatelessWidget { +class VodSearchView extends GetView { const VodSearchView({Key? key}) : super(key: key); String get _placeholder => "搜索"; - String get _img => - 'https://pic.laoyapic.com/upload/vod/20220331-1/c09faee339f0bed74db4cb04b3de3d96.jpg'; + // final VodSearchController controller = Get.put(VodSearchController()); @override Widget build(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: CupertinoSearchTextField( - placeholder: _placeholder, + return GetBuilder(builder: (vodSearch) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: CupertinoSearchTextField( + controller: vodSearch.searchController, + autofocus: true, + placeholder: _placeholder, + onSubmitted: vodSearch.handleSearch, + ), ), - // trailing: CupertinoButton( - // padding: EdgeInsets.symmetric(vertical: 4,), - // onPressed: () { - // /// TODO - // }, - // child: Text("取消"), - // ), - ), - child: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const CupertinoPageBar(), - Expanded( - child: CupertinoScrollbar( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: List.generate( - 42, - (index) => VodCardWidget( - space: 12.0, - imageURL: _img, - onTap: () {}, - title: _placeholder, - ), - ).toList(), + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Builder(builder: (context) { + if (!vodSearch.isShowPageBar) return const SizedBox.shrink(); + return CupertinoPageBar( + isLoading: vodSearch.isLoading, + total: vodSearch.respData.total, + totalPage: vodSearch.respData.totalPage, + current: vodSearch.respData.current, + onTap: vodSearch.handlePrevAndNext, + ); + }), + Expanded( + child: CupertinoScrollbar( + child: SingleChildScrollView( + child: Builder(builder: (context) { + if (vodSearch.firstRun) { + return const SizedBox.shrink(); + // return SizedBox( + // height: Get.height * .66, + // child: const Center( + // child: Text("请搜索"), + // ), + // ); + } + var _data = vodSearch.respData.data; + if (vodSearch.canShowErrorCase) { + return SizedBox( + height: Get.height * .66, + child: Center( + child: KErrorStack( + errorStack: vodSearch.errorStack, + child: CupertinoButton.filled( + child: const Text("重新搜索"), + onPressed: () { + vodSearch.handleSearch( + vodSearch.searchController.text, + ); + }, + ), + ), + ), + ); + } + if (_data.isEmpty && !vodSearch.isLoading) { + return SizedBox( + height: Get.height * .66, + child: const Center( + child: Text("搜索内容为空 :("), + ), + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + _data.length, + (index) => Builder(builder: (context) { + var curr = _data[index]; + return VodCardWidget( + space: 12.0, + imageURL: curr.cover, + onTap: () { + Get.toNamed( + Routes.VOD_DETAIL, + arguments: curr.id, + ); + }, + title: curr.title, + errorBuilder: (context, error, stackTrace) { + return SizedBox( + width: 90, + height: 72, + child: DecoratedBox( + decoration: BoxDecoration( + color: CupertinoColors.inactiveGray, + borderRadius: BorderRadius.circular(12.0), + ), + child: const Center( + child: Text("加载失败 :("), + ), + ), + ); + }, + ); + }), + ).toList(), + ); + }), ), ), ), - ), - ], + ], + ), ), - ), - ); + ); + }); } } diff --git a/lib/modules/vod_search.dart b/lib/modules/vod_search.dart new file mode 100644 index 0000000..a9ef7a9 --- /dev/null +++ b/lib/modules/vod_search.dart @@ -0,0 +1,40 @@ +// To parse this JSON data, do +// +// final vodSearchRespData = vodSearchRespDataFromJson(jsonString); + +import 'dart:convert'; + +import 'package:dan211/modules/vod_movie.dart'; + +VodSearchRespData vodSearchRespDataFromJson(String str) => VodSearchRespData.fromJson(json.decode(str)); + +String vodSearchRespDataToJson(VodSearchRespData data) => json.encode(data.toJson()); + +class VodSearchRespData { + VodSearchRespData({ + required this.total, + required this.current, + required this.totalPage, + required this.data, + }); + + final int total; + final int current; + final int totalPage; + final List data; + + factory VodSearchRespData.fromJson(Map json) => VodSearchRespData( + total: json["total"], + current: json["current"], + totalPage: json["total_page"], + data: List.from( + json["data"].map((x) => VodCard.fromJson(x))), + ); + + Map toJson() => { + "total": total, + "current": current, + "total_page": totalPage, + "data": List.from(data.map((x) => x.toJson())), + }; +}