* fix: 1080p

* opt: import export

* opt: downloader

* opt: skeleton

* opt: parseColor

* tweak

* opt: sb seek

* opt: rxn
This commit is contained in:
My-Responsitories
2026-05-08 12:50:43 +00:00
committed by dom
parent 07843a5e77
commit b7bd516aa7
28 changed files with 253 additions and 370 deletions

View File

@@ -1,188 +1,62 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
class Skeleton extends StatelessWidget {
class Skeleton extends StatefulWidget {
final Widget child;
const Skeleton({
required this.child,
super.key,
});
const Skeleton({super.key, required this.child});
@override
Widget build(BuildContext context) {
final color = Theme.of(context).colorScheme.surface.withAlpha(10);
final shimmerGradient = LinearGradient(
colors: [
Colors.transparent,
color,
color,
Colors.transparent,
],
stops: const [
0.1,
0.3,
0.5,
0.7,
],
begin: const Alignment(-1.0, -0.3),
end: const Alignment(1.0, 0.9),
tileMode: TileMode.clamp,
);
return Shimmer(
linearGradient: shimmerGradient,
child: ShimmerLoading(
isLoading: true,
child: child,
),
);
}
State<Skeleton> createState() => _SkeletonState();
}
class Shimmer extends StatefulWidget {
static ShimmerState? of(BuildContext context) {
return context.findAncestorStateOfType<ShimmerState>();
}
const Shimmer({
super.key,
required this.linearGradient,
this.child,
});
final LinearGradient linearGradient;
final Widget? child;
@override
ShimmerState createState() => ShimmerState();
}
class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
late AnimationController _shimmerController;
class _SkeletonState extends State<Skeleton>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late Color color;
final matrix = Matrix4.identity();
@override
void initState() {
super.initState();
_shimmerController = AnimationController.unbounded(vsync: this)
..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000));
_controller = AnimationController.unbounded(vsync: this)
..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000))
..addListener(_setState);
}
@override
void dispose() {
_shimmerController.dispose();
_controller.dispose();
super.dispose();
}
LinearGradient get gradient => LinearGradient(
colors: widget.linearGradient.colors,
stops: widget.linearGradient.stops,
begin: widget.linearGradient.begin,
end: widget.linearGradient.end,
transform: _SlidingGradientTransform(
slidePercent: _shimmerController.value,
),
);
bool get isSized =>
(context.findRenderObject() as RenderBox?)?.hasSize ?? false;
Size get size => (context.findRenderObject() as RenderBox).size;
Offset getDescendantOffset({
required RenderBox descendant,
Offset offset = Offset.zero,
}) {
final shimmerBox = context.findRenderObject() as RenderBox;
return descendant.localToGlobal(offset, ancestor: shimmerBox);
void _setState() {
setState(() {});
}
Listenable get shimmerChanges => _shimmerController;
@override
Widget build(BuildContext context) {
return widget.child ?? const SizedBox.shrink();
}
}
class _SlidingGradientTransform extends GradientTransform {
const _SlidingGradientTransform({
required this.slidePercent,
});
final double slidePercent;
@override
Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0);
}
}
class ShimmerLoading extends StatefulWidget {
const ShimmerLoading({
super.key,
required this.isLoading,
required this.child,
});
final bool isLoading;
final Widget child;
@override
State<ShimmerLoading> createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<ShimmerLoading> {
Listenable? _shimmerChanges;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_shimmerChanges != null) {
_shimmerChanges!.removeListener(_onShimmerChange);
}
_shimmerChanges = Shimmer.of(context)?.shimmerChanges;
if (_shimmerChanges != null) {
_shimmerChanges!.addListener(_onShimmerChange);
}
}
@override
void dispose() {
_shimmerChanges?.removeListener(_onShimmerChange);
super.dispose();
}
void _onShimmerChange() {
if (widget.isLoading) {
setState(() {});
}
color = ColorScheme.of(context).surface.withAlpha(10);
}
@override
Widget build(BuildContext context) {
if (!widget.isLoading) {
return widget.child;
}
final shimmer = Shimmer.of(context)!;
if (!shimmer.isSized) {
return const SizedBox.shrink();
}
final shimmerSize = shimmer.size;
final gradient = shimmer.gradient;
final offsetWithinShimmer = shimmer.getDescendantOffset(
descendant: context.findRenderObject() as RenderBox,
);
final colors = [Colors.transparent, color, color, Colors.transparent];
return ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (bounds) {
return gradient.createShader(
Rect.fromLTWH(
-offsetWithinShimmer.dx,
-offsetWithinShimmer.dy,
shimmerSize.width,
shimmerSize.height,
),
shaderCallback: (Rect bounds) {
final width = bounds.width;
final height = bounds.height;
matrix[12] = width * _controller.value;
return ui.Gradient.linear(
Offset(0, 0.35 * height),
Offset(width, 0.95 * height),
colors,
const [0.1, 0.3, 0.5, 0.7],
TileMode.clamp,
matrix.storage,
);
},
child: widget.child,

View File

@@ -3,7 +3,7 @@ import 'dart:convert' show utf8, jsonDecode;
import 'dart:io' show File;
import 'package:PiliPlus/common/style.dart';
import 'package:PiliPlus/utils/extension/context_ext.dart';
import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:PiliPlus/utils/storage_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:file_picker/file_picker.dart';
@@ -46,87 +46,91 @@ Future<void> importFromClipBoard<T>(
bool showConfirmDialog = true,
}) async {
final data = await Clipboard.getData('text/plain');
if (data?.text?.isNotEmpty != true) {
SmartDialog.showToast('剪贴板无数据');
return;
}
if (!context.mounted) return;
final text = data!.text!;
late final T json;
late final String formatText;
try {
json = jsonDecode(text);
formatText = Utils.jsonEncoder.convert(json);
} catch (e) {
SmartDialog.showToast('解析json失败$e');
return;
}
bool? executeImport;
if (showConfirmDialog) {
final highlight = Highlight()..registerLanguage('json', langJson);
final result = highlight.highlight(
code: formatText,
language: 'json',
);
late TextSpanRenderer renderer;
bool? isDarkMode;
executeImport = await showDialog(
context: context,
builder: (context) {
final isDark = context.isDarkMode;
if (isDark != isDarkMode) {
isDarkMode = isDark;
renderer = TextSpanRenderer(
const TextStyle(),
isDark ? githubDarkTheme : githubTheme,
);
result.render(renderer);
}
return AlertDialog(
title: Text('是否导入如下$title'),
content: SingleChildScrollView(
child: Text.rich(renderer.span!),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
if (data?.text case final text? when (text.isNotEmpty)) {
if (!context.mounted) return;
final T json;
final String formatText;
try {
json = jsonDecode(text);
formatText = Utils.jsonEncoder.convert(json);
} catch (e) {
SmartDialog.showToast('解析json失败$e');
return;
}
bool? executeImport;
if (showConfirmDialog) {
final highlight = Highlight()..registerLanguage('json', langJson);
final result = highlight.highlight(
code: formatText,
language: 'json',
);
late TextSpanRenderer renderer;
bool? isDarkMode;
executeImport = await showDialog<bool>(
context: context,
builder: (context) {
final theme = Theme.of(context);
final isDark = theme.brightness.isDark;
if (isDark != isDarkMode) {
isDarkMode = isDark;
renderer = TextSpanRenderer(
const TextStyle(),
isDark ? githubDarkTheme : githubTheme,
);
result.render(renderer);
}
return AlertDialog(
title: Text('是否导入如下$title'),
content: SingleChildScrollView(
child: Text.rich(renderer.span!),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: theme.colorScheme.outline,
),
),
),
),
TextButton(
onPressed: () => Get.back(result: true),
child: const Text('确定'),
),
],
);
},
);
} else {
executeImport = true;
}
if (executeImport ?? false) {
try {
await onImport(json);
SmartDialog.showToast('导入成功');
} catch (e) {
SmartDialog.showToast('导入失败:$e');
TextButton(
onPressed: () => Get.back(result: true),
child: const Text('确定'),
),
],
);
},
);
} else {
executeImport = true;
}
if (executeImport ?? false) {
try {
await onImport(json);
SmartDialog.showToast('导入成功');
} catch (e) {
SmartDialog.showToast('导入失败:$e');
}
}
} else {
SmartDialog.showToast('剪贴板无数据');
return;
}
}
Future<void> importFromLocalFile<T>({
required FutureOr<void> Function(T json) onImport,
}) async {
final result = await FilePicker.pickFiles();
final result = await FilePicker.pickFiles(
type: .custom,
allowedExtensions: const ['json', 'txt'],
);
if (result != null) {
final path = result.files.first.path;
if (path != null) {
final data = await File(path).readAsString();
late final T json;
final T json;
try {
json = jsonDecode(data);
} catch (e) {
@@ -172,7 +176,6 @@ void importFromInput<T>(
json = jsonDecode(value!) as T;
return null;
} catch (e) {
if (e is FormatException) {}
return '解析json失败$e';
}
},

View File

@@ -2,7 +2,7 @@ import 'package:PiliPlus/common/style.dart';
import 'package:flutter/material.dart';
Widget selectMask(
ThemeData theme,
ColorScheme colorScheme,
bool checked, {
BorderRadiusGeometry borderRadius = Style.mdRadius,
}) {
@@ -23,12 +23,12 @@ Widget selectMask(
width: 34,
height: 34,
decoration: BoxDecoration(
color: theme.colorScheme.surface.withValues(alpha: 0.8),
color: colorScheme.surface.withValues(alpha: 0.8),
shape: BoxShape.circle,
),
child: Icon(
Icons.done_all_outlined,
color: theme.colorScheme.primary,
color: colorScheme.primary,
semanticLabel: '取消选择',
),
),

View File

@@ -171,6 +171,8 @@ abstract final class VideoHttp {
required VideoType videoType,
String? language,
}) async {
final dmImgStr = Utils.base64EncodeRandomString(16, 64);
final dmCoverImgStr = Utils.base64EncodeRandomString(32, 128);
final params = await WbiSign.makSign({
'avid': ?avid,
'bvid': ?bvid,
@@ -188,6 +190,10 @@ abstract final class VideoHttp {
'web_location': 1315873,
// 免登录查看1080p
if (tryLook) 'try_look': 1,
'dm_img_list': '[]',
'dm_img_str': dmImgStr,
'dm_cover_img_str': dmCoverImgStr,
'dm_img_inter': '{"ds":[],"wh":[0,0,0],"of":[0,0,0]}',
'cur_language': ?language,
});

View File

@@ -36,7 +36,7 @@ class ArticleController extends CommonDynController {
final RxBool isLoaded = false.obs;
DynamicItemModel? opusData; // 标题信息从summary获取, 动态没有favorite
ArticleViewData? articleData;
final Rx<ModuleStatModel?> stats = Rx<ModuleStatModel?>(null);
final stats = Rxn<ModuleStatModel>();
List<ArticleContentModel>? get opus =>
opusData?.modules.moduleContent ?? articleData?.opus?.content;

View File

@@ -17,7 +17,7 @@ class ArticleListController
queryData();
}
Rx<ArticleListInfo?> list = Rx<ArticleListInfo?>(null);
final list = Rxn<ArticleListInfo>();
Owner? author;
@override

View File

@@ -103,16 +103,16 @@ class _DownloadDetailPageState extends State<DownloadDetailPage>
visualDensity: VisualDensity.compact,
),
onPressed: () async {
final allChecked = this.allChecked.toSet();
final futures = allChecked
.map(
(e) => _downloadService.downloadDanmaku(
entry: e,
isUpdate: true,
),
)
.toList();
handleSelect();
final res = await Future.wait(
allChecked.map(
(e) => _downloadService.downloadDanmaku(
entry: e,
isUpdate: true,
),
),
);
final res = await Future.wait(futures);
if (res.every((e) => e)) {
SmartDialog.showToast('更新成功');
} else {
@@ -201,22 +201,20 @@ class _DownloadDetailPageState extends State<DownloadDetailPage>
title: const Text('确定删除选中视频?'),
onConfirm: () async {
SmartDialog.showLoading();
final watchProgress = GStorage.watchProgress;
final allChecked = this.allChecked.toSet();
final allChecked = this.allChecked.toList();
final isDeleteAll = allChecked.length == _downloadItems.length;
if (isDeleteAll) {
await _closeSub();
}
for (final entry in allChecked) {
await watchProgress.deleteAll(
await Future.wait([
if (isDeleteAll) _closeSub(),
GStorage.watchProgress.deleteAll(
allChecked.map((e) => e.cid.toString()),
);
await _downloadService.deleteDownload(
entry: entry,
removeList: true,
refresh: false,
);
}
),
for (final entry in allChecked)
_downloadService.deleteDownload(
entry: entry,
removeList: true,
refresh: false,
),
]);
_downloadService.flagNotifier.refresh();
if (isDeleteAll) {
SmartDialog.dismiss();

View File

@@ -269,7 +269,10 @@ class DetailItem extends StatelessWidget {
type: PBadgeType.gray,
),
Positioned.fill(
child: selectMask(theme, checked ?? entry.checked),
child: selectMask(
theme.colorScheme,
checked ?? entry.checked,
),
),
],
),

View File

@@ -57,16 +57,16 @@ class _DownloadSearchPageState
TextButton(
style: TextButton.styleFrom(visualDensity: VisualDensity.compact),
onPressed: () async {
final allChecked = controller.allChecked.toSet();
final future = controller.allChecked
.map(
(e) => _downloadService.downloadDanmaku(
entry: e,
isUpdate: true,
),
)
.toList();
controller.handleSelect();
final res = await Future.wait(
allChecked.map(
(e) => _downloadService.downloadDanmaku(
entry: e,
isUpdate: true,
),
),
);
final res = await Future.wait(future);
if (res.every((e) => e)) {
SmartDialog.showToast('更新成功');
} else {

View File

@@ -11,13 +11,13 @@ import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/scaffold.dart';
import 'package:PiliPlus/common/widgets/select_mask.dart';
import 'package:PiliPlus/models/common/badge_type.dart';
import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart';
import 'package:PiliPlus/models_new/download/download_info.dart';
import 'package:PiliPlus/pages/download/controller.dart';
import 'package:PiliPlus/pages/download/detail/view.dart';
import 'package:PiliPlus/pages/download/detail/widgets/item.dart';
import 'package:PiliPlus/pages/download/search/view.dart';
import 'package:PiliPlus/services/download/download_service.dart';
import 'package:PiliPlus/utils/cache_manager.dart';
import 'package:PiliPlus/utils/extension/iterable_ext.dart' show IterableExt;
import 'package:PiliPlus/utils/grid.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
@@ -67,20 +67,16 @@ class _DownloadPageState extends State<DownloadPage> {
visualDensity: VisualDensity.compact,
),
onPressed: () async {
final allChecked = _controller.allChecked.toSet();
final future = [
for (final page in _controller.allChecked)
for (final e in page.entries)
_downloadService.downloadDanmaku(
entry: e,
isUpdate: true,
),
];
_controller.handleSelect();
final list = <BiliDownloadEntryInfo>[];
for (final page in allChecked) {
list.addAll(page.entries);
}
final res = await Future.wait(
list.map(
(e) => _downloadService.downloadDanmaku(
entry: e,
isUpdate: true,
),
),
);
final res = await Future.wait(future);
if (res.every((e) => e)) {
SmartDialog.showToast('更新成功');
} else {
@@ -355,7 +351,7 @@ class _DownloadPageState extends State<DownloadPage> {
top: 6.0,
),
Positioned.fill(
child: selectMask(theme, pageInfo.checked),
child: selectMask(theme.colorScheme, pageInfo.checked),
),
],
),
@@ -381,17 +377,14 @@ class _DownloadPageState extends State<DownloadPage> {
crossAxisAlignment: .end,
mainAxisAlignment: .spaceBetween,
children: [
if (first.ownerName case final ownerName?)
Text(
ownerName,
style: TextStyle(
fontSize: 12,
height: 1.6,
color: theme.colorScheme.outline,
),
)
else
const Spacer(),
Text(
'${CacheManager.formatSize(pageInfo.entries.fold(0, (p, n) => p + n.totalBytes))} ${first.ownerName ?? ""}',
style: TextStyle(
fontSize: 12,
height: 1.6,
color: theme.colorScheme.outline,
),
),
pageInfo.entries.first.moreBtn(theme),
],
),

View File

@@ -104,8 +104,8 @@ class _CreateDynPanelState extends CommonRichTextPubPageState<CreateDynPanel> {
late final Rx<Pair<int, String>?> _topic;
late final Rx<ReplyOptionType> _replyOption;
late final TextEditingController _titleEditCtr;
late final Rx<DateTime?> _publishTime = Rx<DateTime?>(null);
final Rx<ReserveInfoData?> _reserveCard = Rx<ReserveInfoData?>(null);
late final _publishTime = Rxn<DateTime>();
final _reserveCard = Rxn<ReserveInfoData>();
@override
void initState() {

View File

@@ -18,13 +18,13 @@ class DynTopicController
int sortBy = 0;
String offset = '';
Rx<TopicSortByConf?> topicSortByConf = Rx<TopicSortByConf?>(null);
final topicSortByConf = Rxn<TopicSortByConf>();
double? appbarOffset;
// top
Rx<bool?> isFav = Rx<bool?>(null);
Rx<bool?> isLike = Rx<bool?>(null);
final isFav = false.obs;
final isLike = false.obs;
Rx<LoadingState<TopDetails?>> topState =
LoadingState<TopDetails?>.loading().obs;
@@ -42,8 +42,8 @@ class DynTopicController
if (topState.value case Success(:final response)) {
final topicItem = response!.topicItem!;
topicName = topicItem.name;
isFav.value = topicItem.isFav;
isLike.value = topicItem.isLike;
isFav.value = topicItem.isFav ?? false;
isLike.value = topicItem.isLike ?? false;
}
}
@@ -96,7 +96,7 @@ class DynTopicController
SmartDialog.showToast('账号未登录');
return;
}
bool isFav = this.isFav.value ?? false;
final isFav = this.isFav.value;
final res = isFav
? await FavHttp.delFavTopic(topicId)
: await FavHttp.addFavTopic(topicId);
@@ -117,7 +117,7 @@ class DynTopicController
SmartDialog.showToast('账号未登录');
return;
}
bool isLike = this.isLike.value ?? false;
final isLike = this.isLike.value;
final res = await FavHttp.likeTopic(topicId, isLike);
if (res.isSuccess) {
if (isLike) {

View File

@@ -46,7 +46,7 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final colorScheme = ColorScheme.of(context);
final padding = MediaQuery.viewPaddingOf(context);
return scaffold(
body: Stack(
@@ -60,7 +60,7 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
slivers: [
Obx(
() => _buildAppBar(
theme,
colorScheme,
padding,
_controller.topState.value,
),
@@ -70,7 +70,7 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
_controller.topicSortByConf.value?.allSortBy;
if (allSortBy != null && allSortBy.isNotEmpty) {
return SliverPinnedHeader(
backgroundColor: theme.colorScheme.surface,
backgroundColor: colorScheme.surface,
child: Padding(
padding: EdgeInsets.only(
left: 12 + padding.left,
@@ -80,9 +80,8 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
child: Builder(
builder: (context) {
return ToggleButtons(
fillColor: theme.colorScheme.secondaryContainer,
selectedColor:
theme.colorScheme.onSecondaryContainer,
fillColor: colorScheme.secondaryContainer,
selectedColor: colorScheme.onSecondaryContainer,
constraints: const BoxConstraints(
minWidth: 54,
minHeight: 24,
@@ -159,7 +158,7 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
}
Widget _buildAppBar(
ThemeData theme,
ColorScheme colorScheme,
EdgeInsets padding,
LoadingState<TopDetails?> topState,
) {
@@ -216,7 +215,7 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
),
Text(
' 发起',
style: TextStyle(color: theme.colorScheme.outline),
style: TextStyle(color: colorScheme.outline),
),
],
),
@@ -232,7 +231,7 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
const SizedBox(height: 6),
SelectableText(
response.topicItem!.description!,
style: TextStyle(color: theme.colorScheme.onSurfaceVariant),
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
const SizedBox(height: 10),
Row(
@@ -241,7 +240,7 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
'${NumUtils.numFormat(response.topicItem!.view)}浏览 · ${NumUtils.numFormat(response.topicItem!.discuss)}讨论',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.outline,
color: colorScheme.outline,
),
),
const Spacer(),
@@ -249,13 +248,11 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
style: OutlinedButton.styleFrom(
side: BorderSide(
width: 1,
color: theme.colorScheme.outline.withValues(
alpha: 0.2,
),
color: colorScheme.outline.withValues(alpha: 0.2),
),
foregroundColor: _controller.isLike.value == true
foregroundColor: _controller.isLike.value
? null
: theme.colorScheme.onSurfaceVariant,
: colorScheme.onSurfaceVariant,
padding: const EdgeInsets.symmetric(horizontal: 10),
visualDensity: const VisualDensity(
horizontal: -4,
@@ -264,7 +261,7 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: _controller.onLike,
icon: _controller.isLike.value == true
icon: _controller.isLike.value
? const Icon(FontAwesomeIcons.solidThumbsUp, size: 13)
: const Icon(FontAwesomeIcons.thumbsUp, size: 13),
label: Text(
@@ -278,22 +275,17 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
style: OutlinedButton.styleFrom(
side: BorderSide(
width: 1,
color: theme.colorScheme.outline.withValues(
alpha: 0.2,
),
color: colorScheme.outline.withValues(alpha: 0.2),
),
foregroundColor: _controller.isFav.value == true
foregroundColor: _controller.isFav.value
? null
: theme.colorScheme.onSurfaceVariant,
: colorScheme.onSurfaceVariant,
padding: const EdgeInsets.symmetric(horizontal: 10),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
visualDensity: const .new(horizontal: -4, vertical: -4),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: _controller.onFav,
icon: _controller.isFav.value == true
icon: _controller.isFav.value
? const Icon(FontAwesomeIcons.solidStar, size: 13)
: const Icon(FontAwesomeIcons.star, size: 13),
label: Text(
@@ -322,7 +314,7 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
PopupMenuItem(
onTap: _controller.onFav,
child: Text(
'${_controller.isFav.value == true ? '取消' : ''}收藏',
'${_controller.isFav.value ? '取消' : ''}收藏',
),
),
PopupMenuItem(
@@ -333,7 +325,7 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
return;
}
WebViewPage.toWebView(
'https://www.bilibili.com/h5/topic-active/topic-report?topic_id=${_controller.topicId}&topic_name=${_controller.topicName}&${ThemeUtils.themeUrl(theme.isDark)}',
'https://www.bilibili.com/h5/topic-active/topic-report?topic_id=${_controller.topicId}&topic_name=${_controller.topicName}&${ThemeUtils.themeUrl(colorScheme.isDark)}',
);
},
),

View File

@@ -29,7 +29,7 @@ class FavNoteItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = ColorScheme.of(context);
return Material(
type: MaterialType.transparency,
child: InkWell(
@@ -68,7 +68,7 @@ class FavNoteItem extends StatelessWidget {
),
Positioned.fill(
child: selectMask(
theme,
colorScheme,
item.checked,
),
),
@@ -97,7 +97,7 @@ class FavNoteItem extends StatelessWidget {
style: TextStyle(
fontSize: 14,
height: 1,
color: theme.colorScheme.outline,
color: colorScheme.outline,
),
),
const Spacer(),
@@ -107,7 +107,7 @@ class FavNoteItem extends StatelessWidget {
style: TextStyle(
fontSize: 13,
height: 1,
color: theme.colorScheme.outline,
color: colorScheme.outline,
),
),
],

View File

@@ -34,7 +34,7 @@ class FavPgcItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = ColorScheme.of(context);
return Material(
type: MaterialType.transparency,
child: Stack(
@@ -86,7 +86,7 @@ class FavPgcItem extends StatelessWidget {
),
Positioned.fill(
child: selectMask(
theme,
colorScheme,
item.checked,
borderRadius: const BorderRadius.all(
Radius.circular(4),
@@ -111,7 +111,7 @@ class FavPgcItem extends StatelessWidget {
'${item.newEp!.indexShow}${item.isFinish == 0 && item.renewalTime?.isNotEmpty == true ? '${item.renewalTime}' : ''}',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.onSurfaceVariant,
color: colorScheme.onSurfaceVariant,
),
),
],
@@ -123,7 +123,7 @@ class FavPgcItem extends StatelessWidget {
item.progress!,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.onSurfaceVariant,
color: colorScheme.onSurfaceVariant,
),
),
],
@@ -141,7 +141,7 @@ class FavPgcItem extends StatelessWidget {
iconSize: 18,
onPressed: onUpdateStatus,
icon: const Icon(Icons.more_vert),
iconColor: theme.colorScheme.outline,
iconColor: colorScheme.outline,
),
),
],

View File

@@ -80,11 +80,11 @@ class FavDetailController
late int mediaId;
late String heroTag;
final Rx<FavFolderInfo> folderInfo = FavFolderInfo().obs;
final Rx<bool?> _isOwner = Rx<bool?>(null);
final RxBool _isOwner = false.obs;
final Rx<FavOrderType> order = FavOrderType.mtime.obs;
@override
bool get isOwner => _isOwner.value ?? false;
bool get isOwner => _isOwner.value;
late final account = Accounts.main;

View File

@@ -39,7 +39,7 @@ class FavVideoCardH extends StatelessWidget {
Widget build(BuildContext context) {
final isOwner = !isSort && ctr!.isOwner;
late final enableMultiSelect = ctr?.enableMultiSelect.value ?? false;
final theme = Theme.of(context);
final colorScheme = ColorScheme.of(context);
final onLongPress = isSort || enableMultiSelect
? null
@@ -135,7 +135,7 @@ class FavVideoCardH extends StatelessWidget {
if (!isSort)
Positioned.fill(
child: selectMask(
theme,
colorScheme,
item.checked,
),
),
@@ -145,7 +145,7 @@ class FavVideoCardH extends StatelessWidget {
),
),
const SizedBox(width: 10),
content(context, theme, isOwner),
content(context, colorScheme, isOwner),
],
),
),
@@ -153,7 +153,7 @@ class FavVideoCardH extends StatelessWidget {
);
}
Widget content(BuildContext context, ThemeData theme, isOwner) {
Widget content(BuildContext context, ColorScheme colorScheme, bool isOwner) {
return Expanded(
child: Stack(
clipBehavior: Clip.none,
@@ -178,7 +178,7 @@ class FavVideoCardH extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.outline,
color: colorScheme.outline,
),
),
const Spacer(),
@@ -189,7 +189,7 @@ class FavVideoCardH extends StatelessWidget {
style: TextStyle(
height: 1,
fontSize: 12,
color: theme.colorScheme.outline,
color: colorScheme.outline,
),
),
if (item.type != 24)
@@ -215,7 +215,7 @@ class FavVideoCardH extends StatelessWidget {
child: iconButton(
icon: const Icon(Icons.clear),
tooltip: '取消收藏',
iconColor: theme.colorScheme.outline,
iconColor: colorScheme.outline,
onPressed: () => showDialog(
context: context,
builder: (context) => AlertDialog(
@@ -226,7 +226,7 @@ class FavVideoCardH extends StatelessWidget {
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: theme.colorScheme.outline),
style: TextStyle(color: colorScheme.outline),
),
),
TextButton(

View File

@@ -177,7 +177,10 @@ class HistoryItem extends StatelessWidget {
),
),
Positioned.fill(
child: selectMask(theme, item.checked),
child: selectMask(
theme.colorScheme,
item.checked,
),
),
],
);

View File

@@ -158,7 +158,7 @@ class VideoCardHLater extends StatelessWidget {
),
Positioned.fill(
child: selectMask(
theme,
theme.colorScheme,
videoItem.checked,
),
),

View File

@@ -13,7 +13,7 @@ class LiveFollowController
queryData();
}
Rx<int?> count = Rx<int?>(null);
final count = RxnInt();
@override
void checkIsEnd(int length) {

View File

@@ -50,14 +50,14 @@ class LiveRoomController extends GetxController {
int roomId = Get.arguments;
int? ruid;
DanmakuController<DanmakuExtra>? danmakuController;
PlPlayerController plPlayerController = PlPlayerController.getInstance(
final plPlayerController = PlPlayerController.getInstance(
isLive: true,
);
RxBool isLoaded = false.obs;
Rx<RoomInfoH5Data?> roomInfoH5 = Rx<RoomInfoH5Data?>(null);
final isLoaded = false.obs;
final roomInfoH5 = Rxn<RoomInfoH5Data>();
Rx<int?> liveTime = Rx<int?>(null);
final liveTime = Rxn<int>();
Timer? liveTimeTimer;
void startLiveTimer() {
@@ -100,11 +100,11 @@ class LiveRoomController extends GetxController {
LiveDmInfoData? dmInfo;
List<RichTextItem>? savedDanmaku;
int builtLength = 0;
RxList<dynamic> messages = <dynamic>[].obs;
final messages = <dynamic>[].obs;
bool get shouldRefresh => builtLength != messages.length;
late final Rx<SuperChatItem?> fsSC = Rx<SuperChatItem?>(null);
late final fsSC = Rxn<SuperChatItem>();
late final RxList<SuperChatItem> superChatMsg = <SuperChatItem>[].obs;
RxBool disableAutoScroll = false.obs;
final disableAutoScroll = false.obs;
bool autoScroll = true;
LiveMessageStream? _msgStream;
late final ScrollController scrollController;
@@ -112,7 +112,7 @@ class LiveRoomController extends GetxController {
late final PageController pageController;
int? currentQn = PlatformUtils.isMobile ? null : Pref.liveQuality;
RxString currentQnDesc = ''.obs;
final currentQnDesc = ''.obs;
final RxBool isPortrait = false.obs;
late List<({int code, String desc})> acceptQnList = [];

View File

@@ -14,9 +14,9 @@ class PgcReviewController
final PgcReviewType type;
final dynamic mediaId;
Rx<int?> count = Rx<int?>(null);
final count = RxnInt();
String? next;
Rx<PgcReviewSortType> sortType = PgcReviewSortType.def.obs;
final sortType = PgcReviewSortType.def.obs;
@override
void onInit() {

View File

@@ -14,7 +14,7 @@ class PopularSeriesController
with ReloadMixin {
late int number;
final Rx<PopularSeriesConfig?> config = Rx<PopularSeriesConfig?>(null);
final config = Rxn<PopularSeriesConfig>();
String? reminder;
List<PopularSeriesListItem>? seriesList;

View File

@@ -741,8 +741,8 @@ class VideoDetailController extends GetxController
bool isQuerying = false;
final Rx<List<LanguageItem>?> languages = Rx<List<LanguageItem>?>(null);
final Rx<String?> currLang = Rx<String?>(null);
final languages = Rxn<List<LanguageItem>>();
final currLang = Rxn<String>();
void setLanguage(String language) {
if (currLang.value == language) return;
if (!isLoginVideo) {

View File

@@ -467,14 +467,21 @@ class _PostPanelState extends State<PostPanel>
final player = plPlayerController.videoPlayerController;
if (player != null) {
final start = (item.segment.first * 1000).round();
Future<void> seekTo() => player.seek(
Duration(milliseconds: (item.segment.second * 1000).round()),
);
if (start <= 0) {
seekTo();
if (!player.state.playing) {
await player.play();
}
return;
}
final seek = max(0, start - 2000);
await player.seek(Duration(milliseconds: seek));
if (!player.state.playing) {
await player.play();
}
Future<void> seekTo() => player.seek(
Duration(milliseconds: (item.segment.second * 1000).round()),
);
if (start > seek) {
final posSub = player.stream.position.listen(
null,

View File

@@ -32,7 +32,7 @@ class VideoReplyReplyController extends ReplyController
int replyType;
bool hasRoot = false;
final Rx<ReplyInfo?> firstFloor = Rx(null);
final firstFloor = Rxn<ReplyInfo>();
final index = RxnInt();

View File

@@ -645,7 +645,10 @@ class HeaderControlState extends State<HeaderControl>
onTap: () async {
Get.back();
try {
final result = await FilePicker.pickFiles();
final result = await FilePicker.pickFiles(
type: .custom,
allowedExtensions: const ['json', 'vtt', 'srt'],
);
if (result != null) {
final file = result.files.single;
final path = file.path;

View File

@@ -2,11 +2,12 @@ import 'package:flutter/rendering.dart' show Color;
abstract final class ColourUtils {
static Color parseColor(String color) =>
Color(int.parse('FF${color.substring(1)}', radix: 16));
Color(0xFF000000 | int.parse(color.substring(1), radix: 16));
static Color parseMedalColor(String color) => Color(
int.parse('${color.substring(7)}${color.substring(1, 7)}', radix: 16),
);
static Color parseMedalColor(String color) {
final rgba = int.parse(color.substring(1), radix: 16);
return Color.fromARGB(rgba, rgba >> 24, rgba >> 16, rgba >> 8);
}
static Color index2Color(int index, Color color) => switch (index) {
0 => const Color(0xFFfdad13),