Compare commits

...

14 Commits

Author SHA1 Message Date
bggRGjQaUbCoE
81713a6bc4 mod: article: add action panel
related #235

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-16 15:00:49 +08:00
bggRGjQaUbCoE
959bcfaa30 mod: keep pgc index page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-16 13:37:34 +08:00
bggRGjQaUbCoE
fa465f792d opt: video width
Closes #267

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-16 13:37:34 +08:00
bggRGjQaUbCoE
74bf78b9cd feat: pgc index page
Closes #216

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-16 13:15:00 +08:00
bggRGjQaUbCoE
8c408e59f6 opt: post segment panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-16 09:25:33 +08:00
bggRGjQaUbCoE
25d27e42ed fix: #263
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-16 09:22:09 +08:00
bggRGjQaUbCoE
0f2b0cc5f2 Revert "fix: #263"
This reverts commit 84ed34f3a7.
2025-02-16 01:01:01 +08:00
bggRGjQaUbCoE
00ea34f45d opt: video bs
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-15 23:42:02 +08:00
bggRGjQaUbCoE
ec936c1821 opt: video bs
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-15 23:00:47 +08:00
bggRGjQaUbCoE
2ff84857e7 refa: video bottom sheet
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-15 22:19:43 +08:00
bggRGjQaUbCoE
84ed34f3a7 fix: #263
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-15 21:07:56 +08:00
bggRGjQaUbCoE
f0508e1bc2 mod: disable version check when debug
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-15 20:40:39 +08:00
bggRGjQaUbCoE
8ea7bf36d7 fix: dyn detail: repost btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-15 20:22:47 +08:00
bggRGjQaUbCoE
8819461eed mod: dyn detail: add action panel
Closes #235

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-15 18:04:43 +08:00
19 changed files with 3388 additions and 2321 deletions

View File

@@ -275,145 +275,144 @@ class _ListSheetContentState extends State<ListSheetContent>
@override
Widget build(BuildContext context) {
return Material(
return ColoredBox(
color: Theme.of(context).colorScheme.surface,
child: SizedBox(
height: Utils.getSheetHeight(context),
child: Column(
children: [
Container(
height: 45,
padding: EdgeInsets.symmetric(
horizontal: widget.showTitle != false ? 14 : 6),
child: Row(
children: [
if (widget.showTitle != false)
Text(
'合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})',
style: Theme.of(context).textTheme.titleMedium,
),
StreamBuilder(
stream: _favStream?.stream,
builder: (context, snapshot) => snapshot.hasData
? mediumButton(
tooltip: _seasonFav == 1 ? '取消订阅' : '订阅',
icon: _seasonFav == 1
? Icons.notifications_off_outlined
: Icons.notifications_active_outlined,
onPressed: () async {
dynamic result = await VideoHttp.seasonFav(
isFav: _seasonFav == 1,
seasonId: widget.season.id,
);
if (result['status']) {
SmartDialog.showToast(
'${_seasonFav == 1 ? '取消' : ''}订阅成功');
_seasonFav = _seasonFav == 1 ? 0 : 1;
_favStream?.add(_seasonFav);
} else {
SmartDialog.showToast(result['msg']);
}
},
)
: const SizedBox.shrink(),
child: Column(
children: [
Container(
height: 45,
padding: EdgeInsets.symmetric(
horizontal: widget.showTitle != false ? 14 : 6),
child: Row(
children: [
if (widget.showTitle != false)
Text(
'合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})',
style: Theme.of(context).textTheme.titleMedium,
),
mediumButton(
tooltip: '跳至顶部',
icon: Icons.vertical_align_top,
onPressed: () {
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse[_ctr?.index ?? 0]
? 0
: _isList
? widget.season.sections[_ctr?.index].episodes
.length -
1
: episodes.length - 1,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
mediumButton(
tooltip: '跳至底部',
icon: Icons.vertical_align_bottom,
onPressed: () {
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse[_ctr?.index ?? 0]
? _isList
? widget.season.sections[_ctr?.index].episodes
.length -
1
: episodes.length - 1
: 0,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
mediumButton(
tooltip: '跳至当前',
icon: Icons.my_location,
onPressed: () async {
if (_ctr != null && _ctr?.index != (_index)) {
_ctr?.animateTo(_index);
await Future.delayed(const Duration(milliseconds: 225));
}
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: currentIndex,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
if (widget.isSupportReverse == true)
if (!_isList)
_reverseButton
else
StreamBuilder(
stream: _indexStream?.stream,
initialData: _index,
builder: (context, snapshot) {
return snapshot.data == _index
? _reverseButton
: const SizedBox.shrink();
},
),
const Spacer(),
StreamBuilder(
stream: _indexStream?.stream,
initialData: _index,
builder: (context, snapshot) => mediumButton(
tooltip: reverse[snapshot.data] ? '顺序' : '倒序',
icon: !reverse[snapshot.data]
? MdiIcons.sortNumericAscending
: MdiIcons.sortNumericDescending,
onPressed: () {
setState(() {
reverse[_ctr?.index ?? 0] =
!reverse[_ctr?.index ?? 0];
});
StreamBuilder(
stream: _favStream?.stream,
builder: (context, snapshot) => snapshot.hasData
? mediumButton(
tooltip: _seasonFav == 1 ? '取消订阅' : '订阅',
icon: _seasonFav == 1
? Icons.notifications_off_outlined
: Icons.notifications_active_outlined,
onPressed: () async {
dynamic result = await VideoHttp.seasonFav(
isFav: _seasonFav == 1,
seasonId: widget.season.id,
);
if (result['status']) {
SmartDialog.showToast(
'${_seasonFav == 1 ? '取消' : ''}订阅成功');
_seasonFav = _seasonFav == 1 ? 0 : 1;
_favStream?.add(_seasonFav);
} else {
SmartDialog.showToast(result['msg']);
}
},
)
: const SizedBox.shrink(),
),
mediumButton(
tooltip: '跳至顶部',
icon: Icons.vertical_align_top,
onPressed: () {
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse[_ctr?.index ?? 0]
? 0
: _isList
? widget.season.sections[_ctr?.index].episodes
.length -
1
: episodes.length - 1,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
mediumButton(
tooltip: '跳至底部',
icon: Icons.vertical_align_bottom,
onPressed: () {
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse[_ctr?.index ?? 0]
? _isList
? widget.season.sections[_ctr?.index].episodes
.length -
1
: episodes.length - 1
: 0,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
mediumButton(
tooltip: '跳至当前',
icon: Icons.my_location,
onPressed: () async {
if (_ctr != null && _ctr?.index != (_index)) {
_ctr?.animateTo(_index);
await Future.delayed(const Duration(milliseconds: 225));
}
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: currentIndex,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
if (widget.isSupportReverse == true)
if (!_isList)
_reverseButton
else
StreamBuilder(
stream: _indexStream?.stream,
initialData: _index,
builder: (context, snapshot) {
return snapshot.data == _index
? _reverseButton
: const SizedBox.shrink();
},
),
const Spacer(),
StreamBuilder(
stream: _indexStream?.stream,
initialData: _index,
builder: (context, snapshot) => mediumButton(
tooltip: reverse[snapshot.data] ? '顺序' : '倒序',
icon: !reverse[snapshot.data]
? MdiIcons.sortNumericAscending
: MdiIcons.sortNumericDescending,
onPressed: () {
setState(() {
reverse[_ctr?.index ?? 0] = !reverse[_ctr?.index ?? 0];
});
},
),
if (widget.onClose != null)
mediumButton(
tooltip: '关闭',
icon: Icons.close,
onPressed: widget.onClose,
),
],
),
),
if (widget.onClose != null)
mediumButton(
tooltip: '关闭',
icon: Icons.close,
onPressed: widget.onClose,
),
],
),
Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
if (_isList)
TabBar(
),
Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
if (_isList)
Material(
color: Theme.of(context).colorScheme.surface,
child: TabBar(
controller: _ctr,
padding: const EdgeInsets.only(right: 60),
isScrollable: true,
@@ -423,20 +422,26 @@ class _ListSheetContentState extends State<ListSheetContent>
dividerHeight: 1,
dividerColor: Theme.of(context).dividerColor.withOpacity(0.1),
),
Expanded(
child: _isList
? TabBarView(
),
Expanded(
child: _isList
? Material(
color: Theme.of(context).colorScheme.surface,
child: TabBarView(
controller: _ctr,
children: List.generate(
widget.season.sections.length,
(index) => _buildBody(
index, widget.season.sections[index].episodes),
),
)
: _buildBody(null, episodes),
),
],
),
),
)
: Material(
color: Theme.of(context).colorScheme.surface,
child: _buildBody(null, episodes),
),
),
],
),
);
}

View File

@@ -39,7 +39,13 @@ class _SelfSizedHorizontalListState extends State<SelfSizedHorizontalList> {
WidgetsBinding.instance.addPostFrameCallback((v) => setState(() {}));
}
if (widget.itemCount == 0) return const SizedBox();
if (isInit) return Container(key: infoKey, child: widget.childBuilder(0));
if (isInit) {
return Container(
key: infoKey,
padding: widget.padding,
child: widget.childBuilder(0),
);
}
return SizedBox(
height: height,

View File

@@ -718,4 +718,8 @@ class Api {
/// 我的关注 - 正在直播
static const String getFollowingLive =
'${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following';
static const String pgcIndexCondition = '/pgc/season/index/condition';
static const String pgcIndexResult = '/pgc/season/index/result';
}

View File

@@ -1,9 +1,55 @@
import 'package:PiliPlus/http/loading_state.dart';
import '../models/bangumi/list.dart';
import '../models/bangumi/pgc_index/condition.dart';
import 'index.dart';
class BangumiHttp {
static Future<LoadingState> pgcIndexResult({
required int page,
required Map<String, dynamic> params,
seasonType,
type,
indexType,
}) async {
dynamic res = await Request().get(
Api.pgcIndexResult,
queryParameters: {
...params,
if (seasonType != null) 'season_type': seasonType,
if (type != null) 'type': type,
if (indexType != null) 'index_type': indexType,
'page': page,
'pagesize': 21,
},
);
if (res.data['code'] == 0) {
return LoadingState.success(res.data['data']);
} else {
return LoadingState.error(res.data['message']);
}
}
static Future<LoadingState> pgcIndexCondition({
seasonType,
type,
indexType,
}) async {
dynamic res = await Request().get(
Api.pgcIndexCondition,
queryParameters: {
if (seasonType != null) 'season_type': seasonType,
if (type != null) 'type': type,
if (indexType != null) 'index_type': indexType,
},
);
if (res.data['code'] == 0) {
return LoadingState.success(Condition.fromJson(res.data['data']));
} else {
return LoadingState.error(res.data['message']);
}
}
static Future<LoadingState> bangumiList({
int? page,
int? indexType,

View File

@@ -0,0 +1,70 @@
class Condition {
List<Filter>? filter;
List<Order>? order;
Condition({
this.filter,
this.order,
});
Condition.fromJson(Map json) {
filter = (json['filter'] as List?)
?.map((item) => Filter.fromJson(item))
.toList();
order =
(json['order'] as List?)?.map((item) => Order.fromJson(item)).toList();
}
}
class Order {
String? field;
String? name;
String? sort;
Order({
this.field,
this.name,
this.sort,
});
Order.fromJson(Map json) {
field = json['field'];
name = json['name'];
sort = json['sort'];
}
}
class Filter {
String? field;
String? name;
List<Values>? values;
Filter({
this.field,
this.name,
this.values,
});
Filter.fromJson(Map json) {
field = json['field'];
name = json['name'];
values = (json['values'] as List?)
?.map((item) => Values.fromJson(item))
.toList();
}
}
class Values {
String? keyword;
String? name;
Values({
this.keyword,
this.name,
});
Values.fromJson(Map json) {
keyword = json['keyword'];
name = json['name'];
}
}

View File

@@ -0,0 +1,69 @@
import 'package:PiliPlus/http/bangumi.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/bangumi/pgc_index/condition.dart';
import 'package:PiliPlus/pages/common/common_controller.dart';
import 'package:get/get.dart' hide Condition;
class PgcIndexController extends CommonController {
PgcIndexController(this.indexType);
int? indexType;
Rx<LoadingState> conditionState = LoadingState.loading().obs;
late final RxBool isExpand = false.obs;
RxMap<String, dynamic> indexParams = <String, dynamic>{}.obs;
@override
void onInit() {
super.onInit();
getPgcIndexCondition();
}
Future getPgcIndexCondition() async {
dynamic res = await BangumiHttp.pgcIndexCondition(
seasonType: indexType == null ? 1 : null,
type: 0,
indexType: indexType,
);
if (res is Success) {
Condition data = res.response;
if (data.order?.isNotEmpty == true) {
indexParams['order'] = data.order!.first.field;
}
if (data.filter?.isNotEmpty == true) {
for (Filter item in data.filter!) {
indexParams['${item.field}'] = item.values?.firstOrNull?.keyword;
}
}
queryData();
}
conditionState.value = res;
}
@override
Future<LoadingState> customGetData() => BangumiHttp.pgcIndexResult(
page: currentPage,
params: indexParams,
seasonType: indexType == null ? 1 : null,
type: 0,
indexType: indexType,
);
@override
bool customHandleResponse(Success response) {
if (response.response['has_next'] == null ||
response.response['has_next'] == 0) {
isEnd = true;
}
if (response.response['list'] == null ||
(response.response['list'] as List?)?.isEmpty == true) {
isEnd = true;
}
if (currentPage != 1 && loadingState.value is Success) {
response.response['list']
?.insertAll(0, (loadingState.value as Success).response);
}
loadingState.value = LoadingState.success(response.response['list']);
return true;
}
}

View File

@@ -0,0 +1,242 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/http_error.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/self_sized_horizontal_list.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/bangumi/pgc_index/pgc_index_controller.dart';
import 'package:PiliPlus/pages/bangumi/widgets/bangumi_card_v_pgc_index.dart';
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart' hide Condition;
import '../../../models/bangumi/pgc_index/condition.dart';
class PgcIndexPage extends StatefulWidget {
const PgcIndexPage({super.key, this.indexType});
final int? indexType;
@override
State<PgcIndexPage> createState() => _PgcIndexPageState();
}
class _PgcIndexPageState extends State<PgcIndexPage>
with AutomaticKeepAliveClientMixin {
late final _ctr = Get.put(
PgcIndexController(widget.indexType),
tag: '${widget.indexType}',
);
@override
bool get wantKeepAlive => widget.indexType != null;
@override
Widget build(BuildContext context) {
super.build(context);
return widget.indexType == null
? Scaffold(
appBar: AppBar(title: const Text('索引')),
body: Obx(() => _buildBody(_ctr.conditionState.value)),
)
: Obx(() => _buildBody(_ctr.conditionState.value));
}
Widget _buildBody(LoadingState loadingState) {
return switch (loadingState) {
Loading() => loadingWidget,
Success() => Builder(builder: (context) {
Condition data = loadingState.response;
int count = (data.order?.isNotEmpty == true ? 1 : 0) +
(data.filter?.length ?? 0);
if (count == 0) return const SizedBox.shrink();
return CustomScrollView(
slivers: [
if (widget.indexType != null)
SliverToBoxAdapter(child: const SizedBox(height: 12)),
SliverToBoxAdapter(
child: AnimatedSize(
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
duration: const Duration(milliseconds: 200),
child: count > 5
? Obx(() => _buildSortWidget(count, data))
: _buildSortWidget(count, data),
),
),
SliverPadding(
padding: EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
top: 12,
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: Obx(() => _buildList(_ctr.loadingState.value)),
),
],
);
}),
Error() => scrollErrorWidget(
errMsg: loadingState.errMsg,
callback: () {
_ctr.conditionState.value = LoadingState.loading();
_ctr.getPgcIndexCondition();
},
),
LoadingState() => throw UnimplementedError(),
};
}
Widget _buildSortWidget(count, data) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...List.generate(
count > 5
? _ctr.isExpand.value
? count
: count ~/ 2
: count,
(index) {
List? item = data.order?.isNotEmpty == true
? index == 0
? data.order
: data.filter![index - 1].values
: data.filter![index].values;
return item?.isNotEmpty == true
? Padding(
padding: EdgeInsets.only(
top: index == 0 ? 0 : 10,
),
child: SelfSizedHorizontalList(
gapSize: 12,
padding: const EdgeInsets.symmetric(
horizontal: 12,
),
childBuilder: (childIndex) => Obx(
() => SearchText(
bgColor: (item[childIndex] is Order
? _ctr.indexParams['order']
: _ctr.indexParams[data
.filter![
data.order?.isNotEmpty == true
? index - 1
: index]
.field]) ==
(item[childIndex] is Order
? item[childIndex].field
: item[childIndex].keyword)
? Theme.of(context)
.colorScheme
.secondaryContainer
: Colors.transparent,
textColor: (item[childIndex] is Order
? _ctr.indexParams['order']
: _ctr.indexParams[data
.filter![
data.order?.isNotEmpty == true
? index - 1
: index]
.field]) ==
(item[childIndex] is Order
? item[childIndex].field
: item[childIndex].keyword)
? Theme.of(context)
.colorScheme
.onSecondaryContainer
: Theme.of(context)
.colorScheme
.onSurfaceVariant,
text: item[childIndex].name,
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 3,
),
onTap: (_) {
String name = item[childIndex] is Order
? 'order'
: data
.filter![data.order?.isNotEmpty == true
? index - 1
: index]
.field!;
_ctr.indexParams[name] =
(item[childIndex] is Order
? item[childIndex].field
: item[childIndex].keyword);
_ctr.onReload();
},
),
),
itemCount: item!.length,
),
)
: const SizedBox.shrink();
},
),
if (count > 5) ...[
const SizedBox(height: 8),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
_ctr.isExpand.value = _ctr.isExpand.value.not;
},
child: Container(
width: double.infinity,
alignment: Alignment.center,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_ctr.isExpand.value ? '收起' : '展开',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
Icon(
_ctr.isExpand.value
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
color: Theme.of(context).colorScheme.outline,
),
],
),
),
),
],
],
);
Widget _buildList(LoadingState loadingState) {
return switch (loadingState) {
Loading() => HttpError(errMsg: '加载中'),
Success() => (loadingState.response as List?)?.isNotEmpty == true
? SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
mainAxisSpacing: StyleString.cardSpace,
crossAxisSpacing: StyleString.cardSpace,
maxCrossAxisExtent: Grid.smallCardWidth / 3 * 2,
childAspectRatio: 0.75,
mainAxisExtent: MediaQuery.textScalerOf(context).scale(50),
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if (index == loadingState.response.length - 1) {
_ctr.onLoadMore();
}
return BangumiCardVPgcIndex(
bangumiItem: loadingState.response[index]);
},
childCount: loadingState.response.length,
),
)
: HttpError(callback: _ctr.onReload),
Error() => HttpError(
errMsg: loadingState.errMsg,
callback: _ctr.onReload,
),
LoadingState() => throw UnimplementedError(),
};
}
}

View File

@@ -4,6 +4,7 @@ import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/tab_type.dart';
import 'package:PiliPlus/pages/bangumi/pgc_index/pgc_index_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
@@ -128,7 +129,12 @@ class _BangumiPageState extends State<BangumiPage>
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 10, bottom: 10, left: 16),
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
left: 16,
right: 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -136,6 +142,56 @@ class _BangumiPageState extends State<BangumiPage>
'推荐',
style: Theme.of(context).textTheme.titleMedium,
),
GestureDetector(
onTap: () {
if (widget.tabType == TabType.bangumi) {
Get.to(PgcIndexPage());
} else {
List titles = const ['全部', '电影', '电视剧', '纪录片', '综艺'];
List types = const [102, 2, 5, 3, 7];
Get.to(
Scaffold(
appBar: AppBar(title: const Text('索引')),
body: DefaultTabController(
length: types.length,
child: Column(
children: [
TabBar(
tabs: titles
.map((title) => Tab(text: title))
.toList()),
Expanded(
child: TabBarView(
children: types
.map((type) =>
PgcIndexPage(indexType: type))
.toList()),
)
],
),
),
),
);
}
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'查看更多',
strutStyle: StrutStyle(leading: 0, height: 1),
style: TextStyle(
height: 1,
color: Theme.of(context).colorScheme.secondary,
),
),
Icon(
Icons.chevron_right,
color: Theme.of(context).colorScheme.secondary,
),
],
),
),
],
),
),

View File

@@ -0,0 +1,127 @@
import 'package:PiliPlus/common/widgets/image_save.dart';
import 'package:flutter/material.dart';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class BangumiCardVPgcIndex extends StatelessWidget {
const BangumiCardVPgcIndex({
super.key,
required this.bangumiItem,
});
final dynamic bangumiItem;
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: InkWell(
onLongPress: () => imageSaveDialog(
context: context,
title: bangumiItem['title'],
cover: bangumiItem['cover'],
),
onTap: () {
Utils.viewBangumi(seasonId: bangumiItem['season_id']);
},
child: Column(
children: [
ClipRRect(
borderRadius: StyleString.mdRadius,
child: AspectRatio(
aspectRatio: 0.75,
child: LayoutBuilder(builder: (context, boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
NetworkImgLayer(
src: bangumiItem['cover'],
width: maxWidth,
height: maxHeight,
),
if (bangumiItem['badge'] != null &&
bangumiItem['badge'] != '')
PBadge(
text: bangumiItem['badge'],
top: 6,
right: 6,
bottom: null,
left: null,
),
if (bangumiItem['order'] != null &&
bangumiItem['order'] != '')
PBadge(
text: bangumiItem['order'],
top: null,
right: null,
bottom: 6,
left: 6,
type: 'gray',
),
],
);
}),
),
),
bagumiContent(context)
],
),
),
);
}
Widget bagumiContent(context) {
return Expanded(
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(4, 5, 0, 3),
// 单列
// padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Expanded(
child: Text(
bangumiItem['title'],
textAlign: TextAlign.start,
style: const TextStyle(
letterSpacing: 0.3,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
],
),
const SizedBox(height: 1),
if (bangumiItem['index_show'] != null)
Text(
bangumiItem['index_show'],
maxLines: 1,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
// if (bangumiItem.progress != null)
// Text(
// bangumiItem.progress,
// maxLines: 1,
// style: TextStyle(
// fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
// color: Theme.of(context).colorScheme.outline,
// ),
// ),
],
),
),
);
}
}

View File

@@ -23,7 +23,7 @@ class DynamicDetailController extends ReplyController {
item = Get.arguments['item'];
floor = Get.arguments['floor'];
if (floor == 1) {
count.value = int.parse(item!.modules!.moduleStat!.comment!.count ?? '0');
count.value = int.parse(item.modules!.moduleStat!.comment!.count ?? '0');
}
if (oid != 0) {

View File

@@ -4,8 +4,10 @@ import 'dart:math';
import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/reply_sort_type.dart';
import 'package:PiliPlus/pages/dynamics/repost_dyn_panel.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/utils/extension.dart';
@@ -15,6 +17,7 @@ import 'package:PiliPlus/utils/utils.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/common/skeleton/video_reply.dart';
import 'package:PiliPlus/common/widgets/http_error.dart';
@@ -25,6 +28,7 @@ import 'package:PiliPlus/pages/dynamics/widgets/author_panel.dart';
import 'package:PiliPlus/pages/video/detail/reply_reply/index.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import 'package:share_plus/share_plus.dart';
import '../../../utils/grid.dart';
import '../widgets/dynamic_panel.dart';
@@ -354,7 +358,10 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(left: padding / 4),
padding: EdgeInsets.only(
left: padding / 4,
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
sliver: SliverToBoxAdapter(
child: DynamicPanel(
item: _dynamicDetailController.item,
@@ -402,30 +409,204 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
),
if (_fabAnimationCtr != null)
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 14,
right: 14,
left: 0,
right: 0,
bottom: 0,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 2),
begin: const Offset(0, 1),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: _fabAnimationCtr!,
curve: Curves.easeInOut,
)),
child: FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
dynamic oid = _dynamicDetailController.oid ??
IdUtils.bv2av(Get.parameters['bvid']!);
_dynamicDetailController.onReply(
context,
oid: oid,
replyType: ReplyType.values[replyType],
);
},
tooltip: '评论动态',
child: const Icon(Icons.reply),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.only(right: 14, bottom: 14),
child: FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
dynamic oid = _dynamicDetailController.oid ??
IdUtils.bv2av(Get.parameters['bvid']!);
_dynamicDetailController.onReply(
context,
oid: oid,
replyType: ReplyType.values[replyType],
);
},
tooltip: '评论动态',
child: const Icon(Icons.reply),
),
),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withOpacity(0.08),
),
),
),
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: Builder(
builder: (btnContext) => TextButton.icon(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => RepostPanel(
item: _dynamicDetailController.item,
callback: () {
int count = int.tryParse(
_dynamicDetailController
.item
.modules
.moduleStat
.forward
?.count ??
'0') ??
0;
_dynamicDetailController
.item
.modules
.moduleStat
.forward!
.count = (count + 1).toString();
if (btnContext.mounted) {
(btnContext as Element?)
?.markNeedsBuild();
}
},
),
);
},
icon: Icon(
FontAwesomeIcons.shareFromSquare,
size: 16,
color: Theme.of(context).colorScheme.outline,
semanticLabel: "转发",
),
style: TextButton.styleFrom(
padding:
const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor:
Theme.of(context).colorScheme.outline,
),
label: Text(
_dynamicDetailController.item.modules
.moduleStat.forward!.count !=
null
? Utils.numFormat(_dynamicDetailController
.item
.modules
.moduleStat
.forward!
.count)
: '转发',
),
),
),
),
Expanded(
child: TextButton.icon(
onPressed: () {
Share.share(
'${HttpString.dynamicShareBaseUrl}/${_dynamicDetailController.item.idStr}');
},
icon: Icon(
FontAwesomeIcons.shareNodes,
size: 16,
color: Theme.of(context).colorScheme.outline,
semanticLabel: "分享",
),
style: TextButton.styleFrom(
padding:
const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor:
Theme.of(context).colorScheme.outline,
),
label: const Text('分享'),
),
),
Expanded(
child: Builder(
builder: (context) => TextButton.icon(
onPressed: () => Utils.onLikeDynamic(
_dynamicDetailController.item,
() {
if (context.mounted) {
(context as Element?)?.markNeedsBuild();
}
},
),
icon: Icon(
_dynamicDetailController
.item.modules.moduleStat.like!.status!
? FontAwesomeIcons.solidThumbsUp
: FontAwesomeIcons.thumbsUp,
size: 16,
color: _dynamicDetailController
.item.modules.moduleStat.like!.status!
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
semanticLabel: _dynamicDetailController
.item.modules.moduleStat.like!.status!
? "已赞"
: "点赞",
),
style: TextButton.styleFrom(
padding:
const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor:
Theme.of(context).colorScheme.outline,
),
label: AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child,
Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
_dynamicDetailController.item.modules
.moduleStat.like!.count !=
null
? Utils.numFormat(
_dynamicDetailController.item
.modules.moduleStat.like!.count)
: '点赞',
style: TextStyle(
color: _dynamicDetailController.item
.modules.moduleStat.like!.status!
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.outline,
),
),
),
),
),
),
],
),
),
],
),
),
),

View File

@@ -3,6 +3,7 @@ import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/models/user/fav_detail.dart';
import 'package:PiliPlus/models/user/fav_folder.dart';
import 'package:PiliPlus/pages/common/multi_select_controller.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
@@ -35,23 +36,24 @@ class FavDetailController extends MultiSelectController {
@override
bool customHandleResponse(Success response) {
FavDetailData data = response.response;
if (currentPage == 1) {
item.value = response.response.info;
isOwner.value = response.response.info.mid == mid;
item.value = data.info ?? FavFolderItemData();
isOwner.value = data.info?.mid == mid;
}
if (response.response.medias.isEmpty) {
if (data.medias.isNullOrEmpty) {
isEnd = true;
}
if (currentPage != 1 && loadingState.value is Success) {
response.response.medias?.insertAll(
data.medias?.insertAll(
0,
List<FavDetailItemData>.from((loadingState.value as Success).response),
);
}
if (response.response.medias.length >= response.response.info.mediaCount) {
if ((data.medias?.length ?? 0) >= (data.info?.mediaCount ?? 0)) {
isEnd = true;
}
loadingState.value = LoadingState.success(response.response.medias);
loadingState.value = LoadingState.success(data.medias);
return true;
}

View File

@@ -1,8 +1,12 @@
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/common/reply_controller.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/url_utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/http/html.dart';
import 'package:PiliPlus/http/reply.dart';
@@ -16,6 +20,8 @@ class HtmlRenderController extends ReplyController {
late Map response;
int? floor;
Rx<DynamicItemModel> item = DynamicItemModel().obs;
RxBool loaded = false.obs;
late final horizontalPreview = GStorage.horizontalPreview;
@@ -26,10 +32,29 @@ class HtmlRenderController extends ReplyController {
id = Get.parameters['id']!;
dynamicType = Get.parameters['dynamicType']!;
type = dynamicType == 'picture' ? 11 : 12;
if (RegExp(r'^cv', caseSensitive: false).hasMatch(id)) {
UrlUtils.parseRedirectUrl('https://www.bilibili.com/read/$id/')
.then((url) {
if (url != null) {
_queryDyn(url.split('/').last);
}
});
} else {
_queryDyn(id);
}
reqHtml();
}
_queryDyn(id) {
DynamicsHttp.dynamicDetail(id: id).then((res) {
if (res['status']) {
item.value = res['data'];
} else {
debugPrint('${res['msg']}');
}
});
}
// 请求动态内容
Future reqHtml() async {
late dynamic res;

View File

@@ -3,8 +3,10 @@ import 'dart:math';
import 'package:PiliPlus/common/widgets/article_content.dart';
import 'package:PiliPlus/common/widgets/http_error.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/reply_sort_type.dart';
import 'package:PiliPlus/pages/dynamics/repost_dyn_panel.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/utils/extension.dart';
@@ -14,6 +16,7 @@ import 'package:PiliPlus/utils/utils.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/common/skeleton/video_reply.dart';
import 'package:PiliPlus/common/widgets/html_render.dart';
@@ -323,7 +326,11 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
SliverPadding(
padding: orientation == Orientation.portrait
? EdgeInsets.symmetric(horizontal: padding)
: EdgeInsets.only(left: padding / 4),
: EdgeInsets.only(
left: padding / 4,
bottom:
MediaQuery.paddingOf(context).bottom + 80,
),
sliver: _buildContent,
),
if (orientation == Orientation.portrait) ...[
@@ -388,28 +395,265 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
},
),
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 14,
right: 14,
left: 0,
right: 0,
bottom: 0,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 2),
begin: const Offset(0, 1),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: fabAnimationCtr,
curve: Curves.easeInOut,
)),
child: FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
_htmlRenderCtr.onReply(
context,
oid: _htmlRenderCtr.oid.value,
replyType: ReplyType.values[type],
);
},
tooltip: '评论动态',
child: const Icon(Icons.reply),
child: Obx(
() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Padding(
padding: EdgeInsets.only(
right: 14,
bottom: 14 +
(_htmlRenderCtr.item.value.idStr != null
? 0
: MediaQuery.of(context).padding.bottom),
),
child: FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
_htmlRenderCtr.onReply(
context,
oid: _htmlRenderCtr.oid.value,
replyType: ReplyType.values[type],
);
},
tooltip: '评论动态',
child: const Icon(Icons.reply),
),
),
_htmlRenderCtr.item.value.idStr != null
? Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withOpacity(0.08),
),
),
),
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: Builder(
builder: (btnContext) => TextButton.icon(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => RepostPanel(
item: _htmlRenderCtr.item.value,
callback: () {
int count = int.tryParse(
_htmlRenderCtr
.item
.value
.modules
?.moduleStat
?.forward
?.count ??
'0') ??
0;
_htmlRenderCtr
.item
.value
.modules
?.moduleStat
?.forward!
.count =
(count + 1).toString();
if (btnContext.mounted) {
(btnContext as Element?)
?.markNeedsBuild();
}
},
),
);
},
icon: Icon(
FontAwesomeIcons.shareFromSquare,
size: 16,
color: Theme.of(context)
.colorScheme
.outline,
semanticLabel: "转发",
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(
15, 0, 15, 0),
foregroundColor: Theme.of(context)
.colorScheme
.outline,
),
label: Text(
_htmlRenderCtr
.item
.value
.modules
?.moduleStat
?.forward!
.count !=
null
? Utils.numFormat(_htmlRenderCtr
.item
.value
.modules
?.moduleStat
?.forward!
.count)
: '转发',
),
),
),
),
Expanded(
child: TextButton.icon(
onPressed: () {
Share.share(
'${HttpString.dynamicShareBaseUrl}/${_htmlRenderCtr.item.value.idStr}');
},
icon: Icon(
FontAwesomeIcons.shareNodes,
size: 16,
color:
Theme.of(context).colorScheme.outline,
semanticLabel: "分享",
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(
15, 0, 15, 0),
foregroundColor:
Theme.of(context).colorScheme.outline,
),
label: const Text('分享'),
),
),
Expanded(
child: Builder(
builder: (context) => TextButton.icon(
onPressed: () => Utils.onLikeDynamic(
_htmlRenderCtr.item.value,
() {
if (context.mounted) {
(context as Element?)
?.markNeedsBuild();
}
},
),
icon: Icon(
_htmlRenderCtr
.item
.value
.modules
?.moduleStat
?.like
?.status ==
true
? FontAwesomeIcons.solidThumbsUp
: FontAwesomeIcons.thumbsUp,
size: 16,
color: _htmlRenderCtr
.item
.value
.modules
?.moduleStat
?.like
?.status ==
true
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.outline,
semanticLabel: _htmlRenderCtr
.item
.value
.modules
?.moduleStat
?.like
?.status ==
true
? "已赞"
: "点赞",
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(
15, 0, 15, 0),
foregroundColor: Theme.of(context)
.colorScheme
.outline,
),
label: AnimatedSwitcher(
duration:
const Duration(milliseconds: 400),
transitionBuilder: (Widget child,
Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
_htmlRenderCtr
.item
.value
.modules
?.moduleStat
?.like
?.count !=
null
? Utils.numFormat(_htmlRenderCtr
.item
.value
.modules!
.moduleStat!
.like!
.count)
: '点赞',
style: TextStyle(
color: _htmlRenderCtr
.item
.value
.modules
?.moduleStat
?.like
?.status ==
true
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.outline,
),
),
),
),
),
),
],
),
)
: const SizedBox.shrink(),
],
),
),
),
),

View File

@@ -126,8 +126,6 @@ class VideoDetailController extends GetxController
PlayerStatus? playerStatus;
StreamSubscription<Duration>? positionSubscription;
PersistentBottomSheetController? bsController;
bool imageStatus = false;
void onViewImage() {
@@ -1229,9 +1227,9 @@ class VideoDetailController extends GetxController
);
}
if (plPlayerController.isFullScreen.value) {
bsController = scaffoldKey.currentState?.showBottomSheet(
enableDrag: false,
(context) => _postPanel(false),
Utils.showFSSheet(
child: _postPanel(),
isFullScreen: plPlayerController.isFullScreen.value,
);
} else {
childKey.currentState?.showBottomSheet(
@@ -1241,7 +1239,7 @@ class VideoDetailController extends GetxController
}
}
Widget _postPanel([bool isChild = true]) => StatefulBuilder(
Widget _postPanel() => StatefulBuilder(
builder: (context, setState) {
void updateSegment({
required bool isFirst,
@@ -1361,378 +1359,389 @@ class VideoDetailController extends GetxController
];
}
return SizedBox(
height: isChild ? null : Utils.getSheetHeight(context),
child: Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
automaticallyImplyLeading: false,
titleSpacing: 16,
title: const Text('提交片段'),
actions: [
iconButton(
context: context,
tooltip: '添加片段',
onPressed: () {
setState(() {
list?.insert(
0,
PostSegmentModel(
segment: Pair(
first: 0,
second: plPlayerController.positionSeconds.value,
),
category: SegmentType.sponsor,
actionType: ActionType.skip,
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
automaticallyImplyLeading: false,
titleSpacing: 16,
title: const Text('提交片段'),
actions: [
iconButton(
context: context,
tooltip: '添加片段',
onPressed: () {
setState(() {
list?.insert(
0,
PostSegmentModel(
segment: Pair(
first: 0,
second: plPlayerController.positionSeconds.value,
),
);
});
},
icon: Icons.add,
),
const SizedBox(width: 10),
iconButton(
context: context,
tooltip: '关闭',
onPressed: () {
if (bsController != null) {
bsController!.close();
bsController = null;
} else {
Get.back();
}
},
icon: Icons.close,
),
const SizedBox(width: 16),
],
),
body: list?.isNotEmpty == true
? Stack(
children: [
SingleChildScrollView(
child: Column(
children: [
...List.generate(
list!.length,
(index) => Stack(
children: [
Container(
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 5,
),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (list![index].actionType !=
ActionType.full) ...[
Row(
children: [
...segmentWidget(
category: SegmentType.sponsor,
actionType: ActionType.skip,
),
);
});
},
icon: Icons.add,
),
const SizedBox(width: 10),
iconButton(
context: context,
tooltip: '关闭',
onPressed: Get.back,
icon: Icons.close,
),
const SizedBox(width: 16),
],
),
body: list?.isNotEmpty == true
? Stack(
children: [
SingleChildScrollView(
child: Column(
children: [
...List.generate(
list!.length,
(index) => Stack(
children: [
Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 5,
),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
if (list![index].actionType !=
ActionType.full) ...[
Wrap(
runSpacing: 8,
spacing: 16,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: segmentWidget(
isFirst: true,
index: index,
),
if (list![index].category !=
SegmentType
.poi_highlight) ...[
const SizedBox(width: 16),
...segmentWidget(
),
if (list![index].category !=
SegmentType.poi_highlight)
Row(
mainAxisSize:
MainAxisSize.min,
children: segmentWidget(
isFirst: false,
index: index,
),
],
),
],
),
const SizedBox(height: 8),
],
Wrap(
runSpacing: 8,
spacing: 16,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('分类: '),
PopupMenuButton(
initialValue:
list![index].category,
onSelected: (item) async {
list![index].category =
item;
List<ActionType>
constraintList =
_segmentType2ActionType(
item);
if (constraintList
.contains(list![index]
.actionType)
.not) {
list![index].actionType =
constraintList.first;
}
switch (item) {
case SegmentType
.poi_highlight:
updateSegment(
isFirst: false,
index: index,
value: list![index]
.segment
.first,
);
break;
case SegmentType
.exclusive_access:
updateSegment(
isFirst: true,
index: index,
value: 0,
);
break;
case _:
}
setState(() {});
},
itemBuilder: (context) =>
SegmentType.values
.map((item) =>
PopupMenuItem<
SegmentType>(
value: item,
child: Text(
item.title),
))
.toList(),
child: Row(
mainAxisSize:
MainAxisSize.min,
children: [
Text(
list![index]
.category
.title,
style: TextStyle(
height: 1,
fontSize: 14,
color:
Theme.of(context)
.colorScheme
.secondary,
),
strutStyle: StrutStyle(
height: 1,
leading: 0,
),
),
Icon(
MdiIcons
.unfoldMoreHorizontal,
size: MediaQuery
.textScalerOf(
context)
.scale(14),
color: Theme.of(context)
.colorScheme
.secondary,
),
],
),
),
],
),
const SizedBox(height: 8),
],
Row(
children: [
const Text('分类: '),
PopupMenuButton(
initialValue:
list![index].category,
onSelected: (item) async {
list![index].category = item;
List<ActionType>
constraintList =
_segmentType2ActionType(
item);
if (constraintList
.contains(list![index]
.actionType)
.not) {
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('行为类别: '),
PopupMenuButton(
initialValue:
list![index].actionType,
onSelected: (item) async {
list![index].actionType =
constraintList.first;
}
switch (item) {
case SegmentType
.poi_highlight:
updateSegment(
isFirst: false,
index: index,
value: list![index]
.segment
.first,
);
break;
case SegmentType
.exclusive_access:
item;
if (item ==
ActionType.full) {
updateSegment(
isFirst: true,
index: index,
value: 0,
);
break;
case _:
}
setState(() {});
},
itemBuilder: (context) =>
SegmentType.values
.map((item) =>
PopupMenuItem<
SegmentType>(
}
setState(() {});
},
itemBuilder: (context) =>
ActionType.values
.map(
(item) =>
PopupMenuItem<
ActionType>(
enabled: _segmentType2ActionType(
list![index]
.category)
.contains(
item),
value: item,
child: Text(
item.title),
))
.toList(),
child: Row(
mainAxisSize:
MainAxisSize.min,
children: [
Text(
list![index]
.category
.title,
style: TextStyle(
height: 1,
fontSize: 14,
),
)
.toList(),
child: Row(
mainAxisSize:
MainAxisSize.min,
children: [
Text(
list![index]
.actionType
.title,
style: TextStyle(
height: 1,
fontSize: 14,
color:
Theme.of(context)
.colorScheme
.secondary,
),
strutStyle: StrutStyle(
height: 1,
leading: 0,
),
),
Icon(
MdiIcons
.unfoldMoreHorizontal,
size: MediaQuery
.textScalerOf(
context)
.scale(14),
color: Theme.of(context)
.colorScheme
.secondary,
),
strutStyle: StrutStyle(
height: 1,
leading: 0,
),
),
Icon(
MdiIcons
.unfoldMoreHorizontal,
size: MediaQuery
.textScalerOf(
context)
.scale(14),
color: Theme.of(context)
.colorScheme
.secondary,
),
],
),
),
const SizedBox(width: 16),
const Text('行为类别: '),
PopupMenuButton(
initialValue:
list![index].actionType,
onSelected: (item) async {
list![index].actionType =
item;
if (item == ActionType.full) {
updateSegment(
isFirst: true,
index: index,
value: 0,
);
}
setState(() {});
},
itemBuilder: (context) =>
ActionType.values
.map(
(item) =>
PopupMenuItem<
ActionType>(
enabled: _segmentType2ActionType(
list![index]
.category)
.contains(item),
value: item,
child: Text(
item.title),
),
)
.toList(),
child: Row(
mainAxisSize:
MainAxisSize.min,
children: [
Text(
list![index]
.actionType
.title,
style: TextStyle(
height: 1,
fontSize: 14,
color: Theme.of(context)
.colorScheme
.secondary,
),
strutStyle: StrutStyle(
height: 1,
leading: 0,
),
),
Icon(
MdiIcons
.unfoldMoreHorizontal,
size: MediaQuery
.textScalerOf(
context)
.scale(14),
color: Theme.of(context)
.colorScheme
.secondary,
),
],
),
),
],
),
],
),
),
Positioned(
top: 10,
right: 21,
child: iconButton(
context: context,
size: 26,
tooltip: '移除',
icon: Icons.clear,
onPressed: () {
setState(() {
list!.removeAt(index);
});
},
),
),
],
),
),
SizedBox(
height:
88 + MediaQuery.paddingOf(context).bottom,
),
],
),
),
Positioned(
right: 16,
bottom: 16 + MediaQuery.paddingOf(context).bottom,
child: FloatingActionButton(
tooltip: '提交',
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确定无误再提交'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
Request()
.post(
'${GStorage.blockServer}/api/skipSegments',
queryParameters: {
'videoID': bvid,
'cid': cid.value,
'userID': GStorage.blockUserID,
'userAgent': Constants.userAgent,
'videoDuration': plPlayerController
.durationSeconds
.value
.inSeconds,
},
data: {
'segments': list!
.map(
(item) => {
'segment': [
item.segment.first,
item.segment.second,
],
'category':
item.category.name,
'actionType':
item.actionType.name,
},
)
.toList(),
},
options: _options,
)
.then(
(res) {
if (res.statusCode == 200) {
Get.back();
SmartDialog.showToast('提交成功');
list?.clear();
_handleSBData(res);
plPlayerController
.segmentList.value =
_segmentProgressList ??
<Segment>[];
if (positionSubscription ==
null) {
_initSkip();
}
} else {
SmartDialog.showToast(
'提交失败: ${{
400: '参数错误',
403: '被自动审核机制拒绝',
429: '重复提交太快',
409: '重复提交'
}[res.statusCode]}',
);
}
},
);
},
child: const Text('确定提交'),
),
),
],
),
],
),
],
),
],
),
);
},
child: Icon(Icons.check),
),
)
],
)
: errorWidget(),
),
),
Positioned(
top: 10,
right: 21,
child: iconButton(
context: context,
size: 26,
tooltip: '移除',
icon: Icons.clear,
onPressed: () {
setState(() {
list!.removeAt(index);
});
},
),
),
],
),
),
SizedBox(
height: 88 + MediaQuery.paddingOf(context).bottom,
),
],
),
),
Positioned(
right: 16,
bottom: 16 + MediaQuery.paddingOf(context).bottom,
child: FloatingActionButton(
tooltip: '提交',
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确定无误再提交'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
Request()
.post(
'${GStorage.blockServer}/api/skipSegments',
queryParameters: {
'videoID': bvid,
'cid': cid.value,
'userID': GStorage.blockUserID,
'userAgent': Constants.userAgent,
'videoDuration': plPlayerController
.durationSeconds.value.inSeconds,
},
data: {
'segments': list!
.map(
(item) => {
'segment': [
item.segment.first,
item.segment.second,
],
'category':
item.category.name,
'actionType':
item.actionType.name,
},
)
.toList(),
},
options: _options,
)
.then(
(res) {
if (res.statusCode == 200) {
Get.back();
SmartDialog.showToast('提交成功');
list?.clear();
_handleSBData(res);
plPlayerController
.segmentList.value =
_segmentProgressList ??
<Segment>[];
if (positionSubscription == null) {
_initSkip();
}
} else {
SmartDialog.showToast(
'提交失败: ${{
400: '参数错误',
403: '被自动审核机制拒绝',
429: '重复提交太快',
409: '重复提交'
}[res.statusCode]}',
);
}
},
);
},
child: const Text('确定提交'),
),
],
),
);
},
child: Icon(Icons.check),
),
)
],
)
: errorWidget(),
);
},
);

View File

@@ -822,8 +822,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
],
);
}
final double videoWidth =
double videoWidth =
max(context.height / context.width * 1.04, 1 / 2) * context.width;
if (context.width >= 560) {
videoWidth = min(videoWidth, context.width - 280);
}
final double videoHeight = videoWidth * 9 / 16;
return Row(
children: [
@@ -1799,14 +1802,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoDetailController.videoType == SearchType.media_bangumi
? bangumiIntroController.changeSeasonOrbangu
: videoIntroController.changeSeasonOrbangu,
onClose: () {
if (videoDetailController.bsController != null) {
videoDetailController.bsController!.close();
videoDetailController.bsController = null;
} else {
Get.back();
}
},
onClose: Get.back,
onReverse: () {
Get.back();
onReversePlay(
@@ -1817,10 +1813,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
},
);
if (isFullScreen) {
videoDetailController.bsController =
videoDetailController.scaffoldKey.currentState?.showBottomSheet(
(context) => listSheetContent(),
);
Utils.showFSSheet(child: listSheetContent(), isFullScreen: isFullScreen);
} else {
videoDetailController.childKey.currentState?.showBottomSheet(
(context) => listSheetContent(),
@@ -1899,157 +1892,138 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}
void showViewPoints() {
Widget listSheetContent(context, [bool isFS = false]) {
Widget listSheetContent() {
int currentIndex = -1;
return StatefulBuilder(
builder: (context, setState) => SizedBox(
height: isFS ? Utils.getSheetHeight(context) : null,
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
titleSpacing: 16,
title: const Text('分段信息'),
actions: [
Text(
'分段进度条',
style: TextStyle(fontSize: 16),
),
Obx(
() => Transform.scale(
alignment: Alignment.centerLeft,
scale: 0.8,
child: Switch(
thumbIcon:
WidgetStateProperty.resolveWith<Icon?>((states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value:
videoDetailController.plPlayerController.showVP.value,
onChanged: (value) {
videoDetailController.plPlayerController.showVP.value =
value;
},
),
builder: (context, setState) => Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
titleSpacing: 16,
title: const Text('分段信息'),
actions: [
Text(
'分段进度条',
style: TextStyle(fontSize: 16),
),
Obx(
() => Transform.scale(
alignment: Alignment.centerLeft,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>((states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value:
videoDetailController.plPlayerController.showVP.value,
onChanged: (value) {
videoDetailController.plPlayerController.showVP.value =
value;
},
),
),
iconButton(
context: context,
size: 30,
icon: Icons.clear,
tooltip: '关闭',
onPressed: () {
if (videoDetailController.bsController != null) {
videoDetailController.bsController!.close();
videoDetailController.bsController = null;
} else {
Get.back();
}
},
),
const SizedBox(width: 16),
],
),
body: SingleChildScrollView(
child: Column(
children: [
...List.generate(
videoDetailController.viewPointList.length * 2 - 1,
(rawIndex) {
if (rawIndex % 2 == 1) {
return Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
);
}
int index = rawIndex ~/ 2;
Segment segment =
videoDetailController.viewPointList[index];
if (currentIndex == -1 &&
segment.from != null &&
segment.to != null) {
if (videoDetailController
.plPlayerController.positionSeconds.value >=
segment.from! &&
videoDetailController
.plPlayerController.positionSeconds.value <
segment.to!) {
currentIndex = index;
}
}
return ListTile(
dense: true,
onTap: segment.from != null
? () {
currentIndex = index;
plPlayerController?.danmakuController?.clear();
plPlayerController?.videoPlayerController
?.seek(Duration(seconds: segment.from!));
if (videoDetailController.bsController != null) {
videoDetailController.bsController!.close();
videoDetailController.bsController = null;
} else {
Get.back();
// setState(() {});
}
}
: null,
leading: segment.url?.isNotEmpty == true
? Container(
margin: const EdgeInsets.symmetric(vertical: 6),
decoration: currentIndex == index
? BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(
width: 1.8,
strokeAlign:
BorderSide.strokeAlignOutside,
color: Theme.of(context)
.colorScheme
.primary,
),
)
: null,
child: LayoutBuilder(
builder: (context, constraints) =>
NetworkImgLayer(
radius: 6,
src: segment.url,
width: constraints.maxHeight *
StyleString.aspectRatio,
height: constraints.maxHeight,
),
),
)
: null,
title: Text(
segment.title ?? '',
style: TextStyle(
fontSize: 14,
fontWeight:
currentIndex == index ? FontWeight.bold : null,
color: currentIndex == index
? Theme.of(context).colorScheme.primary
: null,
),
),
subtitle: Text(
'${segment.from != null ? Utils.timeFormat(segment.from) : ''} - ${segment.to != null ? Utils.timeFormat(segment.to) : ''}',
style: TextStyle(
fontSize: 13,
color: currentIndex == index
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
),
);
}),
SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom),
],
),
iconButton(
context: context,
size: 30,
icon: Icons.clear,
tooltip: '关闭',
onPressed: Get.back,
),
const SizedBox(width: 16),
],
),
body: SingleChildScrollView(
child: Column(
children: [
...List.generate(
videoDetailController.viewPointList.length * 2 - 1,
(rawIndex) {
if (rawIndex % 2 == 1) {
return Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
);
}
int index = rawIndex ~/ 2;
Segment segment = videoDetailController.viewPointList[index];
if (currentIndex == -1 &&
segment.from != null &&
segment.to != null) {
if (videoDetailController
.plPlayerController.positionSeconds.value >=
segment.from! &&
videoDetailController
.plPlayerController.positionSeconds.value <
segment.to!) {
currentIndex = index;
}
}
return ListTile(
dense: true,
onTap: segment.from != null
? () {
currentIndex = index;
plPlayerController?.danmakuController?.clear();
plPlayerController?.videoPlayerController
?.seek(Duration(seconds: segment.from!));
Get.back();
}
: null,
leading: segment.url?.isNotEmpty == true
? Container(
margin: const EdgeInsets.symmetric(vertical: 6),
decoration: currentIndex == index
? BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(
width: 1.8,
strokeAlign:
BorderSide.strokeAlignOutside,
color:
Theme.of(context).colorScheme.primary,
),
)
: null,
child: LayoutBuilder(
builder: (context, constraints) =>
NetworkImgLayer(
radius: 6,
src: segment.url,
width: constraints.maxHeight *
StyleString.aspectRatio,
height: constraints.maxHeight,
),
),
)
: null,
title: Text(
segment.title ?? '',
style: TextStyle(
fontSize: 14,
fontWeight:
currentIndex == index ? FontWeight.bold : null,
color: currentIndex == index
? Theme.of(context).colorScheme.primary
: null,
),
),
subtitle: Text(
'${segment.from != null ? Utils.timeFormat(segment.from) : ''} - ${segment.to != null ? Utils.timeFormat(segment.to) : ''}',
style: TextStyle(
fontSize: 13,
color: currentIndex == index
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
),
);
}),
SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom),
],
),
),
),
@@ -2057,23 +2031,15 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}
if (isFullScreen) {
videoDetailController.bsController =
videoDetailController.scaffoldKey.currentState?.showBottomSheet(
(context) => listSheetContent(context, true),
);
Utils.showFSSheet(child: listSheetContent(), isFullScreen: isFullScreen);
} else {
videoDetailController.childKey.currentState?.showBottomSheet(
(context) => listSheetContent(context),
(context) => listSheetContent(),
);
}
}
void _onPopInvokedWithResult(didPop, result) {
if (videoDetailController.bsController != null) {
videoDetailController.bsController!.close();
videoDetailController.bsController = null;
return;
}
if (plPlayerController?.controlsLock.value == true) {
plPlayerController?.onLockControl(false);
return;

File diff suppressed because it is too large Load Diff

View File

@@ -1110,12 +1110,12 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// 头部、底部控制条
Obx(
() => Positioned.fill(
child: Column(
children: [
if (widget.headerControl != null ||
plPlayerController.headerControl != null)
ClipRect(
child: AppBarAni(
child: ClipRect(
child: Column(
children: [
if (widget.headerControl != null ||
plPlayerController.headerControl != null)
AppBarAni(
controller: animationController,
visible: !plPlayerController.controlsLock.value &&
plPlayerController.showControls.value,
@@ -1123,21 +1123,21 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
child: widget.headerControl ??
plPlayerController.headerControl!,
),
),
const Spacer(),
if (plPlayerController.showControls.value)
AppBarAni(
controller: animationController,
visible: !plPlayerController.controlsLock.value &&
plPlayerController.showControls.value,
position: 'bottom',
child: widget.bottomControl ??
BottomControl(
controller: plPlayerController,
buildBottomControl: buildBottomControl,
),
),
],
const Spacer(),
if (plPlayerController.showControls.value)
AppBarAni(
controller: animationController,
visible: !plPlayerController.controlsLock.value &&
plPlayerController.showControls.value,
position: 'bottom',
child: widget.bottomControl ??
BottomControl(
controller: plPlayerController,
buildBottomControl: buildBottomControl,
),
),
],
),
),
),
),
@@ -1750,7 +1750,9 @@ Widget buildViewPointWidget(
return item.start >= seg;
}).reduce((a, b) => a.start < b.start ? a : b);
if (item.from != null) {
plPlayerController.seekTo(Duration(seconds: item.from!));
plPlayerController.danmakuController?.clear();
plPlayerController.videoPlayerController
?.seek(Duration(seconds: item.from!));
}
// debugPrint('${item.title},,${item.from}');
} catch (e) {

View File

@@ -8,6 +8,7 @@ import 'package:PiliPlus/common/widgets/radio_widget.dart';
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
@@ -33,6 +34,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:get/get_navigation/src/dialog/dialog_route.dart';
import 'package:path_provider/path_provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:html/dom.dart' as dom;
@@ -44,6 +46,76 @@ class Utils {
static const channel = MethodChannel("PiliPlus");
// 动态点赞
static Future onLikeDynamic(item, VoidCallback callback) async {
feedBack();
String dynamicId = item.idStr!;
// 1 已点赞 2 不喜欢 0 未操作
Like like = item.modules.moduleStat.like;
int count = like.count == '点赞' ? 0 : int.parse(like.count ?? '0');
bool status = like.status!;
int up = status ? 2 : 1;
var res = await DynamicsHttp.likeDynamic(dynamicId: dynamicId, up: up);
if (res['status']) {
SmartDialog.showToast(!status ? '点赞成功' : '取消赞');
if (up == 1) {
item.modules.moduleStat.like.count = (count + 1).toString();
item.modules.moduleStat.like.status = true;
} else {
if (count == 1) {
item.modules.moduleStat.like.count = '点赞';
} else {
item.modules.moduleStat.like.count = (count - 1).toString();
}
item.modules.moduleStat.like.status = false;
}
callback();
} else {
SmartDialog.showToast(res['msg']);
}
}
static void showFSSheet({
required Widget child,
required bool isFullScreen,
double? padding,
}) {
Navigator.of(Get.context!).push(
GetDialogRoute(
pageBuilder: (buildContext, animation, secondaryAnimation) {
return MediaQuery.orientationOf(Get.context!) == Orientation.portrait
? Column(
children: [
const Spacer(flex: 3),
Expanded(flex: 7, child: child),
if (isFullScreen && padding != null)
SizedBox(height: padding),
],
)
: Row(
children: [
const Spacer(),
Expanded(child: child),
],
);
},
transitionDuration: const Duration(milliseconds: 350),
transitionBuilder: (context, animation, secondaryAnimation, child) {
Offset begin =
MediaQuery.orientationOf(Get.context!) == Orientation.portrait
? Offset(0.0, 1.0)
: Offset(1.0, 0.0);
var tween = Tween(begin: begin, end: Offset.zero)
.chain(CurveTween(curve: Curves.easeInOut));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
),
);
}
static darkenTheme(ThemeData themeData) {
// return themeData;
Color color = themeData.colorScheme.surfaceContainerHighest.darken(0.7);
@@ -1170,6 +1242,7 @@ class Utils {
// 检查更新
static Future checkUpdate([bool isAuto = true]) async {
if (BuildConfig.isDebug) return;
SmartDialog.dismiss();
try {
dynamic res = await Request().get(Api.latestApp, extra: {'ua': 'mob'});