Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-12-16 14:22:47 +08:00
parent 13818533a7
commit 0baf3fcd36
283 changed files with 803 additions and 1550 deletions

View File

@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
/// from Getx
extension ContextExtensions on BuildContext {
/// The same of [MediaQuery.of(context).size]
Size get mediaQuerySize => MediaQuery.sizeOf(this);
/// The same of [MediaQuery.of(context).size.height]
/// Note: updates when you rezise your screen (like on a browser or
/// desktop window)
double get height => MediaQuery.heightOf(this);
/// The same of [MediaQuery.of(context).size.width]
/// Note: updates when you resize your screen (like on a browser or
/// desktop window)
double get width => MediaQuery.widthOf(this);
/// similar to [MediaQuery.of(context).padding]
ThemeData get theme => Theme.of(this);
/// Check if dark mode theme is enable
bool get isDarkMode => (Theme.brightnessOf(this) == Brightness.dark);
/// give access to Theme.of(context).iconTheme.color
Color? get iconColor => IconTheme.of(this).color;
/// similar to [MediaQuery.of(context).padding]
TextTheme get textTheme => TextTheme.of(this);
/// similar to [MediaQuery.of(context).padding]
EdgeInsets get mediaQueryPadding => MediaQuery.viewPaddingOf(this);
/// similar to [MediaQuery.of(context).padding]
MediaQueryData get mediaQuery => MediaQuery.of(this);
/// similar to [MediaQuery.of(context).viewPadding]
EdgeInsets get mediaQueryViewPadding => MediaQuery.viewPaddingOf(this);
/// similar to [MediaQuery.of(context).viewInsets]
EdgeInsets get mediaQueryViewInsets => MediaQuery.viewInsetsOf(this);
/// similar to [MediaQuery.of(context).orientation]
Orientation get orientation => MediaQuery.orientationOf(this);
/// check if device is on landscape mode
bool get isLandscape => orientation == Orientation.landscape;
/// check if device is on portrait mode
bool get isPortrait => orientation == Orientation.portrait;
/// similar to [MediaQuery.of(this).devicePixelRatio]
double get devicePixelRatio => MediaQuery.devicePixelRatioOf(this);
/// similar to [MediaQuery.of(this).textScaleFactor]
TextScaler get textScaler => MediaQuery.textScalerOf(this);
/// get the shortestSide from screen
double get mediaQueryShortestSide => mediaQuerySize.shortestSide;
/// True if width be larger than 800
bool get showNavbar => (width > 800);
/// True if the shortestSide is smaller than 600p
bool get isPhone => (mediaQueryShortestSide < 600);
/// True if the shortestSide is largest than 600p
bool get isSmallTablet => (mediaQueryShortestSide >= 600);
/// True if the shortestSide is largest than 720p
bool get isLargeTablet => (mediaQueryShortestSide >= 720);
/// True if the current device is Tablet
bool get isTablet => isSmallTablet || isLargeTablet;
}

View File

@@ -0,0 +1,14 @@
import 'package:floating/floating.dart';
extension RationalExt on Rational {
/// Checks whether given [Rational] instance fits into Android requirements
/// or not.
///
/// Android docs specified boundaries as inclusive.
bool get fitsInAndroidRequirements {
final aspectRatio = numerator / denominator;
const min = 1 / 2.39;
const max = 2.39;
return (min <= aspectRatio) && (aspectRatio <= max);
}
}

View File

@@ -0,0 +1,25 @@
import 'dart:io';
extension FileExt on File {
Future<void> tryDel({bool recursive = false}) async {
try {
await delete(recursive: recursive);
} catch (_) {}
}
}
extension DirectoryExt on Directory {
Future<void> tryDel({bool recursive = false}) async {
try {
await delete(recursive: recursive);
} catch (_) {}
}
Future<bool> lengthGte(int length) async {
int count = 0;
await for (var _ in list()) {
if (++count == length) return true;
}
return false;
}
}

View File

@@ -0,0 +1,6 @@
import 'package:get/get.dart';
extension GetExt on GetInterface {
S putOrFind<S>(InstanceBuilderCallback<S> dep, {String? tag}) =>
GetInstance().putOrFind(dep, tag: tag);
}

View File

@@ -0,0 +1,46 @@
extension NullableIterableExt<T> on Iterable<T>? {
bool get isNullOrEmpty => this == null || this!.isEmpty;
}
extension IterableExt<T> on Iterable<T> {
T? reduceOrNull(T Function(T value, T element) combine) {
Iterator<T> iterator = this.iterator;
if (!iterator.moveNext()) {
return null;
}
T value = iterator.current;
while (iterator.moveNext()) {
value = combine(value, iterator.current);
}
return value;
}
}
extension ListExt<T> on List<T> {
T? getOrNull(int index) {
if (index < 0 || index >= length) {
return null;
}
return this[index];
}
bool removeFirstWhere(bool Function(T) test) {
final index = indexWhere(test);
if (index != -1) {
removeAt(index);
return true;
}
return false;
}
List<R> fromCast<R>() {
return List<R>.from(this);
}
T findClosestTarget(
bool Function(T) test,
T Function(T, T) combine,
) {
return where(test).reduceOrNull(combine) ?? reduce(combine);
}
}

View File

@@ -0,0 +1,5 @@
extension MapExt<K, V> on Map<K, V> {
Map<RK, RV> fromCast<RK, RV>() {
return Map<RK, RV>.from(this);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/widgets.dart';
extension ImageExtension on num {
int? cacheSize(BuildContext context) {
if (this == 0) {
return null;
}
return (this * MediaQuery.devicePixelRatioOf(context)).round();
}
}
extension IntExt on int? {
int? operator +(int other) => this == null ? null : this! + other;
int? operator -(int other) => this == null ? null : this! - other;
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/widgets.dart';
extension ScrollControllerExt on ScrollController {
void animToTop() => animTo(0);
void animTo(
double offset, {
Duration duration = const Duration(milliseconds: 500),
}) {
if (!hasClients) return;
if ((offset - this.offset).abs() >= position.viewportDimension * 7) {
jumpTo(offset);
} else {
animateTo(
offset,
duration: duration,
curve: Curves.easeInOut,
);
}
}
void jumpToTop() {
if (!hasClients) return;
jumpTo(0);
}
}

View File

@@ -0,0 +1,5 @@
import 'dart:ui' show Size;
extension SizeExt on Size {
bool get isPortrait => width < 600 || height >= width;
}

View File

@@ -0,0 +1,7 @@
final _regExp = RegExp("^(http:)?//", caseSensitive: false);
extension StringExt on String? {
String get http2https => this?.replaceFirst(_regExp, "https://") ?? '';
bool get isNullOrEmpty => this == null || this!.isEmpty;
}

View File

@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
extension ColorSchemeExt on ColorScheme {
Color get vipColor =>
brightness.isLight ? const Color(0xFFFF6699) : const Color(0xFFD44E7D);
Color get freeColor =>
brightness.isLight ? const Color(0xFFFF7F24) : const Color(0xFFD66011);
bool get isLight => brightness.isLight;
bool get isDark => brightness.isDark;
}
extension ColorExtension on Color {
Color darken([double amount = .5]) {
assert(amount >= 0 && amount <= 1, 'Amount must be between 0 and 1');
return Color.lerp(this, Colors.black, amount)!;
}
}
extension BrightnessExt on Brightness {
Brightness get reverse => isLight ? Brightness.dark : Brightness.light;
bool get isLight => this == Brightness.light;
bool get isDark => this == Brightness.dark;
}

View File

@@ -0,0 +1,92 @@
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart'
show ThreeDotItem, ThreeDotItemType, IMSettingType;
import 'package:PiliPlus/pages/common/common_whisper_controller.dart';
import 'package:PiliPlus/pages/contact/view.dart';
import 'package:PiliPlus/pages/whisper_settings/view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
extension ThreeDotItemTypeExt on ThreeDotItemType {
Icon get icon => switch (this) {
ThreeDotItemType.THREE_DOT_ITEM_TYPE_MSG_SETTING => const Icon(
Icons.settings,
size: 20,
),
ThreeDotItemType.THREE_DOT_ITEM_TYPE_READ_ALL => const Icon(
Icons.cleaning_services,
size: 20,
),
ThreeDotItemType.THREE_DOT_ITEM_TYPE_CLEAR_LIST => const Icon(
Icons.delete_forever_outlined,
size: 20,
),
ThreeDotItemType.THREE_DOT_ITEM_TYPE_UP_HELPER => const Icon(
Icons.live_tv,
size: 20,
),
ThreeDotItemType.THREE_DOT_ITEM_TYPE_CONTACTS => const Icon(
Icons.account_box_outlined,
size: 20,
),
ThreeDotItemType.THREE_DOT_ITEM_TYPE_FANS_GROUP_HELPER => const Icon(
Icons.notifications_none,
size: 20,
),
_ => const Icon(MdiIcons.circleMedium, size: 20),
};
void action({
required BuildContext context,
required CommonWhisperController controller,
required ThreeDotItem item,
}) {
switch (this) {
case ThreeDotItemType.THREE_DOT_ITEM_TYPE_READ_ALL:
showConfirmDialog(
context: context,
title: '一键已读',
content: '是否清除全部新消息提醒?',
onConfirm: controller.onClearUnread,
);
case ThreeDotItemType.THREE_DOT_ITEM_TYPE_CLEAR_LIST:
showConfirmDialog(
context: context,
title: '清空列表',
content: '清空后所有消息将被删除,无法恢复',
onConfirm: controller.onDeleteList,
);
case ThreeDotItemType.THREE_DOT_ITEM_TYPE_MSG_SETTING:
Get.to(
const WhisperSettingsPage(
imSettingType: IMSettingType.SETTING_TYPE_NEED_ALL,
),
);
case ThreeDotItemType.THREE_DOT_ITEM_TYPE_UP_HELPER:
dynamic talkerId = RegExp(r'/(\d{3,})').firstMatch(item.url)?.group(1);
if (talkerId != null) {
talkerId = int.parse(talkerId);
Get.toNamed(
'/whisperDetail',
arguments: {
'talkerId': talkerId,
'name': item.title,
'face': switch (talkerId) {
844424930131966 =>
'https://message.biliimg.com/bfs/im/489a63efadfb202366c2f88853d2217b5ddc7a13.png',
844424930131964 =>
'https://i0.hdslb.com/bfs/im_new/58eda511672db078466e7ab8db22a95c1503684976.png',
_ => item.icon,
},
},
);
}
case ThreeDotItemType.THREE_DOT_ITEM_TYPE_CONTACTS:
Get.to(const ContactPage(isFromSelect: false));
default:
SmartDialog.showToast('TODO: $name');
}
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/widgets.dart';
extension WidgetExt on Widget {
Widget constraintWidth({
BoxConstraints constraints = const BoxConstraints(maxWidth: 625),
}) {
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: constraints,
child: this,
),
);
}
}