Compare commits

...

1 Commits

Author SHA1 Message Date
chen08209
7cb5d40969 Support custom text scaling
Optimize the display of different text scale

Optimize windows setup experience
2025-04-23 14:56:20 +08:00
44 changed files with 1341 additions and 502 deletions

View File

@@ -201,6 +201,7 @@ jobs:
env: env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TAG: ${{ github.ref_name }} TAG: ${{ github.ref_name }}
RUN_ID: ${{ github.run_id }}
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install requests pip install requests

View File

@@ -391,5 +391,6 @@
"messageTest": "Message test", "messageTest": "Message test",
"messageTestTip": "This is a message.", "messageTestTip": "This is a message.",
"crashTest": "Crash test", "crashTest": "Crash test",
"clearData": "Clear Data" "clearData": "Clear Data",
"textScale": "Text Scaling"
} }

View File

@@ -391,5 +391,7 @@
"messageTest": "メッセージテスト", "messageTest": "メッセージテスト",
"messageTestTip": "これはメッセージです。", "messageTestTip": "これはメッセージです。",
"crashTest": "クラッシュテスト", "crashTest": "クラッシュテスト",
"clearData": "データを消去" "clearData": "データを消去",
"zoom": "ズーム",
"textScale": "テキストスケーリング"
} }

View File

@@ -391,5 +391,7 @@
"messageTest": "Тестирование сообщения", "messageTest": "Тестирование сообщения",
"messageTestTip": "Это сообщение.", "messageTestTip": "Это сообщение.",
"crashTest": "Тест на сбои", "crashTest": "Тест на сбои",
"clearData": "Очистить данные" "clearData": "Очистить данные",
"zoom": "Масштаб",
"textScale": "Масштабирование текста"
} }

View File

@@ -391,5 +391,7 @@
"messageTest": "消息测试", "messageTest": "消息测试",
"messageTestTip": "这是一条消息。", "messageTestTip": "这是一条消息。",
"crashTest": "崩溃测试", "crashTest": "崩溃测试",
"clearData": "清除数据" "clearData": "清除数据",
"zoom": "缩放",
"textScale": "文本缩放"
} }

View File

@@ -17,15 +17,12 @@ const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock"; final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890; const helperPort = 47890;
const helperTag = "2024125"; const helperTag = "2024125";
const baseInfoEdgeInsets = EdgeInsets.symmetric( final baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16, vertical: 16.ap,
horizontal: 16, horizontal: 16.ap,
); );
double textScaleFactor = min( final defaultTextScaleFactor = WidgetsBinding.instance.platformDispatcher.textScaleFactor;
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
1.2,
);
const httpTimeoutDuration = Duration(milliseconds: 5000); const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100); const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100);
@@ -44,7 +41,6 @@ const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1"; const localhost = "127.0.0.1";
const clashConfigKey = "clash_config"; const clashConfigKey = "clash_config";
const configKey = "config"; const configKey = "config";
const listItemPadding = EdgeInsets.symmetric(horizontal: 16);
const double dialogCommonWidth = 300; const double dialogCommonWidth = 300;
const repository = "chen08209/FlClash"; const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090"; const defaultExternalController = "127.0.0.1:9090";
@@ -81,7 +77,7 @@ const viewModeColumnsMap = {
const defaultPrimaryColor = 0xFF795548; const defaultPrimaryColor = 0xFF795548;
double getWidgetHeight(num lines) { double getWidgetHeight(num lines) {
return max(lines * 84 * textScaleFactor + (lines - 1) * 16, 0); return max(lines * 84 + (lines - 1) * 16, 0).ap;
} }
final mainIsolate = "FlClashMainIsolate"; final mainIsolate = "FlClashMainIsolate";

View File

@@ -3,11 +3,13 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class Measure { class Measure {
final TextScaler _textScale; final TextScaler _textScaler;
final BuildContext context; final BuildContext context;
final Map<String, dynamic> _measureMap;
Measure.of(this.context) Measure.of(this.context, double textScaleFactor)
: _textScale = TextScaler.linear( : _measureMap = {},
_textScaler = TextScaler.linear(
textScaleFactor, textScaleFactor,
); );
@@ -21,7 +23,7 @@ class Measure {
style: text.style, style: text.style,
), ),
maxLines: text.maxLines, maxLines: text.maxLines,
textScaler: _textScale, textScaler: _textScaler,
textDirection: text.textDirection ?? TextDirection.ltr, textDirection: text.textDirection ?? TextDirection.ltr,
)..layout( )..layout(
maxWidth: maxWidth, maxWidth: maxWidth,
@@ -29,81 +31,75 @@ class Measure {
return textPainter.size; return textPainter.size;
} }
double? _bodyMediumHeight;
Size? _bodyLargeSize;
double? _bodySmallHeight;
double? _labelSmallHeight;
double? _labelMediumHeight;
double? _titleLargeHeight;
double? _titleMediumHeight;
double get bodyMediumHeight { double get bodyMediumHeight {
_bodyMediumHeight ??= computeTextSize( return _measureMap.getCacheValue(
Text( "bodyMediumHeight",
"X", computeTextSize(
style: context.textTheme.bodyMedium, Text(
), "X",
).height; style: context.textTheme.bodyMedium,
return _bodyMediumHeight!; ),
} ).height,
Size get bodyLargeSize {
_bodyLargeSize ??= computeTextSize(
Text(
"X",
style: context.textTheme.bodyLarge,
),
); );
return _bodyLargeSize!;
} }
double get bodySmallHeight { double get bodySmallHeight {
_bodySmallHeight ??= computeTextSize( return _measureMap.getCacheValue(
Text( "bodySmallHeight",
"X", computeTextSize(
style: context.textTheme.bodySmall, Text(
), "X",
).height; style: context.textTheme.bodySmall,
return _bodySmallHeight!; ),
).height,
);
} }
double get labelSmallHeight { double get labelSmallHeight {
_labelSmallHeight ??= computeTextSize( return _measureMap.getCacheValue(
Text( "labelSmallHeight",
"X", computeTextSize(
style: context.textTheme.labelSmall, Text(
), "X",
).height; style: context.textTheme.labelSmall,
return _labelSmallHeight!; ),
).height,
);
} }
double get labelMediumHeight { double get labelMediumHeight {
_labelMediumHeight ??= computeTextSize( return _measureMap.getCacheValue(
Text( "labelMediumHeight",
"X", computeTextSize(
style: context.textTheme.labelMedium, Text(
), "X",
).height; style: context.textTheme.labelMedium,
return _labelMediumHeight!; ),
).height,
);
} }
double get titleLargeHeight { double get titleLargeHeight {
_titleLargeHeight ??= computeTextSize( return _measureMap.getCacheValue(
Text( "titleLargeHeight",
"X", computeTextSize(
style: context.textTheme.titleLarge, Text(
), "X",
).height; style: context.textTheme.titleLarge,
return _titleLargeHeight!; ),
).height,
);
} }
double get titleMediumHeight { double get titleMediumHeight {
_titleMediumHeight ??= computeTextSize( return _measureMap.getCacheValue(
Text( "titleMediumHeight",
"X", computeTextSize(
style: context.textTheme.titleMedium, Text(
), "X",
).height; style: context.textTheme.titleMedium,
return _titleMediumHeight!; ),
).height,
);
} }
} }

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/state.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -12,6 +13,10 @@ extension NumExt on num {
} }
return formatted; return formatted;
} }
double get ap {
return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5);
}
} }
extension DoubleExt on double { extension DoubleExt on double {

View File

@@ -1,4 +1,3 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@@ -20,10 +19,7 @@ class CommonPrint {
return; return;
} }
globalState.appController.addLog( globalState.appController.addLog(
Log( Log.app(payload),
logLevel: LogLevel.app,
payload: payload,
),
); );
} }
} }

View File

@@ -4,8 +4,12 @@ import 'package:flutter/material.dart';
class CommonTheme { class CommonTheme {
final BuildContext context; final BuildContext context;
final Map<String, Color> _colorMap; final Map<String, Color> _colorMap;
final double textScaleFactor;
CommonTheme.of(this.context) : _colorMap = {}; CommonTheme.of(
this.context,
this.textScaleFactor,
) : _colorMap = {};
Color get darkenSecondaryContainer { Color get darkenSecondaryContainer {
return _colorMap.getCacheValue( return _colorMap.getCacheValue(

View File

@@ -334,12 +334,7 @@ class AppController {
try { try {
await updateProfile(profile); await updateProfile(profile);
} catch (e) { } catch (e) {
_ref.read(logsProvider.notifier).addLog( commonPrint.log(e.toString());
Log(
logLevel: LogLevel.info,
payload: e.toString(),
),
);
} }
} }
} }

View File

@@ -73,7 +73,6 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
updateRequestsThrottler(); updateRequestsThrottler();
} }
}, },
fireImmediately: true,
); );
} }
@@ -149,72 +148,77 @@ class _RequestsFragmentState extends ConsumerState<RequestsFragment>
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0)); _handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
return child!; return child!;
}, },
child: ValueListenableBuilder<ConnectionsState>( child: TextScaleNotification(
valueListenable: _requestsStateNotifier, child: ValueListenableBuilder<ConnectionsState>(
builder: (_, state, __) { valueListenable: _requestsStateNotifier,
final connections = state.list; builder: (_, state, __) {
if (connections.isEmpty) { final connections = state.list;
return NullStatus( if (connections.isEmpty) {
label: appLocalizations.nullRequestsDesc, return NullStatus(
); label: appLocalizations.nullRequestsDesc,
} );
final items = connections }
.map<Widget>( final items = connections
(connection) => ConnectionItem( .map<Widget>(
key: Key(connection.id), (connection) => ConnectionItem(
connection: connection, key: Key(connection.id),
onClickKeyword: (value) { connection: connection,
context.commonScaffoldState?.addKeyword(value); onClickKeyword: (value) {
}, context.commonScaffoldState?.addKeyword(value);
), },
) ),
.separated( )
const Divider( .separated(
height: 0, const Divider(
), height: 0,
) ),
.toList(); )
return Align( .toList();
alignment: Alignment.topCenter, return Align(
child: ScrollToEndBox( alignment: Alignment.topCenter,
controller: _scrollController, child: ScrollToEndBox(
cacheKey: _cacheKey,
dataSource: connections,
child: CommonScrollBar(
controller: _scrollController, controller: _scrollController,
child: CacheItemExtentListView( cacheKey: _cacheKey,
key: _key, dataSource: connections,
reverse: true, child: CommonScrollBar(
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController, controller: _scrollController,
itemExtentBuilder: (index) { child: CacheItemExtentListView(
final widget = items[index]; key: _key,
if (widget.runtimeType == Divider) { reverse: true,
return 0; shrinkWrap: true,
} physics: NextClampingScrollPhysics(),
final measure = globalState.measure; controller: _scrollController,
final bodyMediumHeight = measure.bodyMediumHeight; itemExtentBuilder: (index) {
final connection = connections[(index / 2).floor()]; final widget = items[index];
final height = _calcCacheHeight(connection); if (widget.runtimeType == Divider) {
return height + bodyMediumHeight + 32; return 0;
}, }
itemBuilder: (_, index) { final measure = globalState.measure;
return items[index]; final bodyMediumHeight = measure.bodyMediumHeight;
}, final connection = connections[(index / 2).floor()];
itemCount: items.length, final height = _calcCacheHeight(connection);
keyBuilder: (int index) { return height + bodyMediumHeight + 32;
final widget = items[index]; },
if (widget.runtimeType == Divider) { itemBuilder: (_, index) {
return "divider"; return items[index];
} },
final connection = connections[(index / 2).floor()]; itemCount: items.length,
return connection.id; keyBuilder: (int index) {
}, final widget = items[index];
if (widget.runtimeType == Divider) {
return "divider";
}
final connection = connections[(index / 2).floor()];
return connection.id;
},
),
), ),
), ),
), );
); },
),
onNotification: (_) {
_key.currentState?.clearCache();
}, },
), ),
); );

View File

@@ -103,8 +103,8 @@ class _DashboardFragmentState extends ConsumerState<DashboardFragment>
child: SuperGrid( child: SuperGrid(
key: key, key: key,
crossAxisCount: columns, crossAxisCount: columns,
crossAxisSpacing: 16, crossAxisSpacing: 16.ap,
mainAxisSpacing: 16, mainAxisSpacing: 16.ap,
children: [ children: [
...dashboardState.dashboardWidgets ...dashboardState.dashboardWidgets
.where( .where(

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/common.dart'; import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -57,37 +58,43 @@ class _MemoryInfoState extends State<MemoryInfo> {
onPressed: () { onPressed: () {
clashCore.requestGc(); clashCore.requestGc();
}, },
child: Column( child: Container(
children: [ padding: baseInfoEdgeInsets.copyWith(
ValueListenableBuilder( top: 0,
valueListenable: _memoryInfoStateNotifier, ),
builder: (_, trafficValue, __) { child: Column(
return Padding( mainAxisSize: MainAxisSize.max,
padding: baseInfoEdgeInsets.copyWith( mainAxisAlignment: MainAxisAlignment.end,
bottom: 0, crossAxisAlignment: CrossAxisAlignment.start,
top: 12, children: [
), SizedBox(
child: Row( height: globalState.measure.bodyMediumHeight + 2,
children: [ child: ValueListenableBuilder(
Text( valueListenable: _memoryInfoStateNotifier,
trafficValue.showValue, builder: (_, trafficValue, __) {
style: return Row(
context.textTheme.bodyMedium?.toLight.adjustSize(1), mainAxisAlignment: MainAxisAlignment.start,
), children: [
SizedBox( Text(
width: 8, trafficValue.showValue,
), style: context.textTheme.bodyMedium?.toLight
Text( .adjustSize(1),
trafficValue.showUnit, ),
style: SizedBox(
context.textTheme.bodyMedium?.toLight.adjustSize(1), width: 8,
) ),
], Text(
), trafficValue.showUnit,
); style: context.textTheme.bodyMedium?.toLight
}, .adjustSize(1),
), )
], ],
);
},
),
)
],
),
), ),
), ),
); );

View File

@@ -206,7 +206,7 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
); );
}, },
icon: Icon( icon: Icon(
size: 16, size: 16.ap,
Icons.info_outline, Icons.info_outline,
color: context.colorScheme.onSurfaceVariant, color: context.colorScheme.onSurfaceVariant,
), ),

View File

@@ -17,56 +17,66 @@ class OutboundMode extends StatelessWidget {
height: height, height: height,
child: Consumer( child: Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
final mode = final mode = ref.watch(
ref.watch(patchClashConfigProvider.select((state) => state.mode)); patchClashConfigProvider.select(
return CommonCard( (state) => state.mode,
onPressed: () {},
info: Info(
label: appLocalizations.outboundMode,
iconData: Icons.call_split_sharp,
),
child: Padding(
padding: const EdgeInsets.only(
top: 12,
bottom: 16,
),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (final item in Mode.values)
Flexible(
child: ListItem.radio(
dense: true,
horizontalTitleGap: 4,
padding: const EdgeInsets.only(
left: 12,
right: 16,
),
delegate: RadioDelegate(
value: item,
groupValue: mode,
onChanged: (value) async {
if (value == null) {
return;
}
globalState.appController.changeMode(value);
},
),
title: Text(
Intl.message(item.name),
style: Theme.of(context)
.textTheme
.bodyMedium
?.toSoftBold,
),
),
),
],
),
), ),
); );
return Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent
),
child: CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.outboundMode,
iconData: Icons.call_split_sharp,
),
child: Padding(
padding: const EdgeInsets.only(
top: 12,
bottom: 16,
),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (final item in Mode.values)
Flexible(
fit: FlexFit.tight,
child: ListItem.radio(
dense: true,
horizontalTitleGap: 4,
padding: EdgeInsets.only(
left: 12.ap,
right: 16.ap,
),
delegate: RadioDelegate(
value: item,
groupValue: mode,
onChanged: (value) async {
if (value == null) {
return;
}
globalState.appController.changeMode(value);
},
),
title: Text(
Intl.message(item.name),
style: Theme.of(context)
.textTheme
.bodyMedium
?.toSoftBold,
),
),
),
],
),
),
));
}, },
), ),
); );
@@ -76,12 +86,21 @@ class OutboundMode extends StatelessWidget {
class OutboundModeV2 extends StatelessWidget { class OutboundModeV2 extends StatelessWidget {
const OutboundModeV2({super.key}); const OutboundModeV2({super.key});
Color _getTextColor(BuildContext context, Mode mode) {
return switch (mode) {
Mode.rule => context.colorScheme.onSecondaryContainer,
Mode.global => context.colorScheme.onPrimaryContainer,
Mode.direct => context.colorScheme.onTertiaryContainer,
};
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final height = getWidgetHeight(0.72); final height = getWidgetHeight(0.72);
return SizedBox( return SizedBox(
height: height, height: height,
child: CommonCard( child: CommonCard(
padding: EdgeInsets.zero,
child: Consumer( child: Consumer(
builder: (_, ref, __) { builder: (_, ref, __) {
final mode = ref.watch( final mode = ref.watch(
@@ -102,14 +121,22 @@ class OutboundModeV2 extends StatelessWidget {
(item) => MapEntry( (item) => MapEntry(
item, item,
Container( Container(
clipBehavior: Clip.antiAlias,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration(),
height: height - 16, height: height - 16,
child: Text( child: Text(
Intl.message(item.name), Intl.message(item.name),
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleSmall .titleSmall
?.adjustSize(1), ?.adjustSize(1)
.copyWith(
color: _getTextColor(
context,
mode,
),
),
), ),
), ),
), ),

View File

@@ -34,19 +34,6 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith( _logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: globalState.appState.logs.list, logs: globalState.appState.logs.list,
); );
ref.listenManual(
logsProvider.select((state) => state.list),
(prev, next) {
if (prev != next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
}
},
fireImmediately: true,
);
ref.listenManual( ref.listenManual(
isCurrentPageProvider( isCurrentPageProvider(
PageLabel.logs, PageLabel.logs,
@@ -60,6 +47,18 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
}, },
fireImmediately: true, fireImmediately: true,
); );
ref.listenManual(
logsProvider.select((state) => state.list),
(prev, next) {
if (prev != next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
}
},
);
} }
@override @override
@@ -123,7 +122,7 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final height = globalState.measure final height = globalState.measure
.computeTextSize( .computeTextSize(
Text( Text(
log.payload ?? "", log.payload,
style: globalState.appController.context.textTheme.bodyLarge, style: globalState.appController.context.textTheme.bodyLarge,
), ),
maxWidth: _currentMaxWidth, maxWidth: _currentMaxWidth,
@@ -154,8 +153,7 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
return LayoutBuilder( return LayoutBuilder(
builder: (_, constraints) { builder: (_, constraints) {
_handleTryClearCache(constraints.maxWidth - 40); _handleTryClearCache(constraints.maxWidth - 40);
return Align( return TextScaleNotification(
alignment: Alignment.topCenter,
child: ValueListenableBuilder<LogsState>( child: ValueListenableBuilder<LogsState>(
valueListenable: _logsStateNotifier, valueListenable: _logsStateNotifier,
builder: (_, state, __) { builder: (_, state, __) {
@@ -168,7 +166,7 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final items = logs final items = logs
.map<Widget>( .map<Widget>(
(log) => LogItem( (log) => LogItem(
key: Key(log.dateTime.toString()), key: Key(log.dateTime),
log: log, log: log,
onClick: (value) { onClick: (value) {
context.commonScaffoldState?.addKeyword(value); context.commonScaffoldState?.addKeyword(value);
@@ -181,43 +179,49 @@ class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
), ),
) )
.toList(); .toList();
return ScrollToEndBox<Log>( return Align(
controller: _scrollController, alignment: Alignment.topCenter,
cacheKey: _cacheKey, child: ScrollToEndBox<Log>(
dataSource: logs,
child: CommonScrollBar(
controller: _scrollController, controller: _scrollController,
child: CacheItemExtentListView( cacheKey: _cacheKey,
key: _key, dataSource: logs,
reverse: true, child: CommonScrollBar(
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController, controller: _scrollController,
itemBuilder: (_, index) { child: CacheItemExtentListView(
return items[index]; key: _key,
}, reverse: true,
itemExtentBuilder: (index) { shrinkWrap: true,
final item = items[index]; physics: NextClampingScrollPhysics(),
if (item.runtimeType == Divider) { controller: _scrollController,
return 0; itemBuilder: (_, index) {
} return items[index];
final log = logs[(index / 2).floor()]; },
return _getItemHeight(log); itemExtentBuilder: (index) {
}, final item = items[index];
itemCount: items.length, if (item.runtimeType == Divider) {
keyBuilder: (int index) { return 0;
final item = items[index]; }
if (item.runtimeType == Divider) { final log = logs[(index / 2).floor()];
return "divider"; return _getItemHeight(log);
} },
final log = logs[(index / 2).floor()]; itemCount: items.length,
return log.payload ?? ""; keyBuilder: (int index) {
}, final item = items[index];
if (item.runtimeType == Divider) {
return "divider";
}
final log = logs[(index / 2).floor()];
return log.payload;
},
),
), ),
), ),
); );
}, },
), ),
onNotification: (_) {
_key.currentState?.clearCache();
},
); );
}, },
); );
@@ -242,14 +246,14 @@ class LogItem extends StatelessWidget {
vertical: 4, vertical: 4,
), ),
title: SelectableText( title: SelectableText(
log.payload ?? '', log.payload,
style: context.textTheme.bodyLarge, style: context.textTheme.bodyLarge,
), ),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SelectableText( SelectableText(
"${log.dateTime}", log.dateTime,
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.primary, color: context.colorScheme.primary,
), ),

View File

@@ -228,7 +228,7 @@ class _OverrideProfileState extends State<OverrideProfile> {
message: TextSpan( message: TextSpan(
text: appLocalizations.saveTip, text: appLocalizations.saveTip,
), ),
confirmText: appLocalizations.tip, confirmText: appLocalizations.save,
); );
if (res != true) { if (res != true) {
return; return;

View File

@@ -39,7 +39,19 @@ class ThemeFragment extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView(child: ThemeColorsBox()); return SingleChildScrollView(
child: Column(
children: [
_ThemeModeItem(),
_PrimaryColorItem(),
_PrueBlackItem(),
_TextScaleFactorItem(),
const SizedBox(
height: 64,
),
],
),
);
} }
} }
@@ -75,29 +87,6 @@ class ItemCard extends StatelessWidget {
} }
} }
class ThemeColorsBox extends ConsumerStatefulWidget {
const ThemeColorsBox({super.key});
@override
ConsumerState<ThemeColorsBox> createState() => _ThemeColorsBoxState();
}
class _ThemeColorsBoxState extends ConsumerState<ThemeColorsBox> {
@override
Widget build(BuildContext context) {
return Column(
children: [
_ThemeModeItem(),
_PrimaryColorItem(),
_PrueBlackItem(),
const SizedBox(
height: 64,
),
],
);
}
}
class _ThemeModeItem extends ConsumerWidget { class _ThemeModeItem extends ConsumerWidget {
const _ThemeModeItem(); const _ThemeModeItem();
@@ -482,6 +471,77 @@ class _PrueBlackItem extends ConsumerWidget {
} }
} }
class _TextScaleFactorItem extends ConsumerWidget {
const _TextScaleFactorItem();
@override
Widget build(BuildContext context, WidgetRef ref) {
final textScale = ref.watch(
themeSettingProvider.select(
(state) => state.textScale,
),
);
final String process =
"${((textScale.scale * 100) as double).round()}%";
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: ListItem.switchItem(
leading: Icon(
Icons.text_fields,
),
horizontalTitleGap: 12,
title: Text(
appLocalizations.textScale,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
delegate: SwitchDelegate(
value: textScale.enable,
onChanged: (value) {
ref.read(themeSettingProvider.notifier).updateState(
(state) => state.copyWith.textScale(
enable: value,
),
);
},
),
),
),
DisabledMask(
status: !textScale.enable,
child: ActivateBox(
active: textScale.enable,
child: SliderTheme(
data: _SliderDefaultsM3(context),
child: Slider(
min: 0.8,
divisions: 8,
padding: EdgeInsets.symmetric(
horizontal: 16,
),
max: 1.2,
label: process,
value: textScale.scale,
onChanged: (value) {
ref.read(themeSettingProvider.notifier).updateState(
(state) => state.copyWith.textScale(
scale: value,
),
);
},
),
),
),
),
],
);
}
}
class _PaletteDialog extends StatefulWidget { class _PaletteDialog extends StatefulWidget {
const _PaletteDialog(); const _PaletteDialog();
@@ -544,3 +604,112 @@ class _PaletteDialogState extends State<_PaletteDialog> {
); );
} }
} }
class _SliderDefaultsM3 extends SliderThemeData {
_SliderDefaultsM3(this.context) : super(trackHeight: 16.0);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
@override
Color? get activeTrackColor => _colors.primary;
@override
Color? get inactiveTrackColor => _colors.secondaryContainer;
@override
Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);
@override
Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38);
@override
Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12);
@override
Color? get disabledSecondaryActiveTrackColor =>
_colors.onSurface.withOpacity(0.38);
@override
Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(1.0);
@override
Color? get inactiveTickMarkColor =>
_colors.onSecondaryContainer.withOpacity(1.0);
@override
Color? get disabledActiveTickMarkColor => _colors.onInverseSurface;
@override
Color? get disabledInactiveTickMarkColor => _colors.onSurface;
@override
Color? get thumbColor => _colors.primary;
@override
Color? get disabledThumbColor => _colors.onSurface.withOpacity(0.38);
@override
Color? get overlayColor =>
WidgetStateColor.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.dragged)) {
return _colors.primary.withOpacity(0.1);
}
if (states.contains(WidgetState.hovered)) {
return _colors.primary.withOpacity(0.08);
}
if (states.contains(WidgetState.focused)) {
return _colors.primary.withOpacity(0.1);
}
return Colors.transparent;
});
@override
TextStyle? get valueIndicatorTextStyle =>
Theme.of(context).textTheme.labelLarge!.copyWith(
color: _colors.onInverseSurface,
);
@override
Color? get valueIndicatorColor => _colors.inverseSurface;
@override
SliderComponentShape? get valueIndicatorShape =>
const RoundedRectSliderValueIndicatorShape();
@override
SliderComponentShape? get thumbShape => const HandleThumbShape();
@override
SliderTrackShape? get trackShape => const GappedSliderTrackShape();
@override
SliderComponentShape? get overlayShape => const RoundSliderOverlayShape();
@override
SliderTickMarkShape? get tickMarkShape =>
const RoundSliderTickMarkShape(tickMarkRadius: 4.0 / 2);
@override
WidgetStateProperty<Size?>? get thumbSize {
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.disabled)) {
return const Size(4.0, 44.0);
}
if (states.contains(WidgetState.hovered)) {
return const Size(4.0, 44.0);
}
if (states.contains(WidgetState.focused)) {
return const Size(2.0, 44.0);
}
if (states.contains(WidgetState.pressed)) {
return const Size(2.0, 44.0);
}
return const Size(4.0, 44.0);
});
}
@override
double? get trackGap => 6.0;
}

View File

@@ -637,6 +637,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Enabling it will allow TCP concurrency", "Enabling it will allow TCP concurrency",
), ),
"testUrl": MessageLookupByLibrary.simpleMessage("Test url"), "testUrl": MessageLookupByLibrary.simpleMessage("Test url"),
"textScale": MessageLookupByLibrary.simpleMessage("Text Scaling"),
"theme": MessageLookupByLibrary.simpleMessage("Theme"), "theme": MessageLookupByLibrary.simpleMessage("Theme"),
"themeColor": MessageLookupByLibrary.simpleMessage("Theme color"), "themeColor": MessageLookupByLibrary.simpleMessage("Theme color"),
"themeDesc": MessageLookupByLibrary.simpleMessage( "themeDesc": MessageLookupByLibrary.simpleMessage(

View File

@@ -471,6 +471,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP並列処理"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP並列処理"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("TCP並列処理を許可"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("TCP並列処理を許可"),
"testUrl": MessageLookupByLibrary.simpleMessage("URLテスト"), "testUrl": MessageLookupByLibrary.simpleMessage("URLテスト"),
"textScale": MessageLookupByLibrary.simpleMessage("テキストスケーリング"),
"theme": MessageLookupByLibrary.simpleMessage("テーマ"), "theme": MessageLookupByLibrary.simpleMessage("テーマ"),
"themeColor": MessageLookupByLibrary.simpleMessage("テーマカラー"), "themeColor": MessageLookupByLibrary.simpleMessage("テーマカラー"),
"themeDesc": MessageLookupByLibrary.simpleMessage("ダークモードの設定、色の調整"), "themeDesc": MessageLookupByLibrary.simpleMessage("ダークモードの設定、色の調整"),

View File

@@ -675,6 +675,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Включение позволит использовать параллелизм TCP", "Включение позволит использовать параллелизм TCP",
), ),
"testUrl": MessageLookupByLibrary.simpleMessage("Тест URL"), "testUrl": MessageLookupByLibrary.simpleMessage("Тест URL"),
"textScale": MessageLookupByLibrary.simpleMessage("Масштабирование текста"),
"theme": MessageLookupByLibrary.simpleMessage("Тема"), "theme": MessageLookupByLibrary.simpleMessage("Тема"),
"themeColor": MessageLookupByLibrary.simpleMessage("Цвет темы"), "themeColor": MessageLookupByLibrary.simpleMessage("Цвет темы"),
"themeDesc": MessageLookupByLibrary.simpleMessage( "themeDesc": MessageLookupByLibrary.simpleMessage(

View File

@@ -413,6 +413,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"),
"testUrl": MessageLookupByLibrary.simpleMessage("测速链接"), "testUrl": MessageLookupByLibrary.simpleMessage("测速链接"),
"textScale": MessageLookupByLibrary.simpleMessage("文本缩放"),
"theme": MessageLookupByLibrary.simpleMessage("主题"), "theme": MessageLookupByLibrary.simpleMessage("主题"),
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"), "themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),

View File

@@ -3054,6 +3054,11 @@ class AppLocalizations {
String get clearData { String get clearData {
return Intl.message('Clear Data', name: 'clearData', desc: '', args: []); return Intl.message('Clear Data', name: 'clearData', desc: '', args: []);
} }
/// `Text Scaling`
String get textScale {
return Intl.message('Text Scaling', name: 'textScale', desc: '', args: []);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> { class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -73,7 +73,7 @@ class _ClashContainerState extends ConsumerState<ClashManager>
void onLog(Log log) { void onLog(Log log) {
ref.watch(logsProvider.notifier).addLog(log); ref.watch(logsProvider.notifier).addLog(log);
if (log.logLevel == LogLevel.error) { if (log.logLevel == LogLevel.error) {
globalState.showNotifier(log.payload ?? ''); globalState.showNotifier(log.payload);
} }
super.onLog(log); super.onLog(log);
} }

View File

@@ -1,10 +1,13 @@
import 'dart:math';
import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/common/measure.dart'; import 'package:fl_clash/common/measure.dart';
import 'package:fl_clash/common/theme.dart'; import 'package:fl_clash/common/theme.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ThemeManager extends StatelessWidget { class ThemeManager extends ConsumerWidget {
final Widget child; final Widget child;
const ThemeManager({ const ThemeManager({
@@ -13,9 +16,20 @@ class ThemeManager extends StatelessWidget {
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
globalState.measure = Measure.of(context); final textScale = ref.read(
globalState.theme = CommonTheme.of(context); themeSettingProvider.select((state) => state.textScale),
);
final double textScaleFactor = max(
min(
textScale.enable ? textScale.scale : defaultTextScaleFactor,
1.2,
),
0.8,
);
globalState.measure = Measure.of(context, textScaleFactor);
globalState.theme = CommonTheme.of(context, textScaleFactor);
final padding = MediaQuery.of(context).padding; final padding = MediaQuery.of(context).padding;
final height = MediaQuery.of(context).size.height; final height = MediaQuery.of(context).size.height;
return MediaQuery( return MediaQuery(

View File

@@ -84,33 +84,33 @@ extension ConnectionExt on Connection {
} }
} }
@JsonSerializable() String _logDateTime(_) {
class Log { return DateTime.now().toString();
@JsonKey(name: "LogLevel") }
LogLevel logLevel;
@JsonKey(name: "Payload")
String? payload;
DateTime _dateTime;
Log({ // String _logId(_) {
required this.logLevel, // return utils.id;
this.payload, // }
}) : _dateTime = DateTime.now();
DateTime get dateTime => _dateTime; @freezed
class Log with _$Log {
const factory Log({
@JsonKey(name: "LogLevel") @Default(LogLevel.app) LogLevel logLevel,
@JsonKey(name: "Payload") @Default("") String payload,
@JsonKey(fromJson: _logDateTime) required String dateTime,
}) = _Log;
factory Log.fromJson(Map<String, dynamic> json) { factory Log.app(
return _$LogFromJson(json); String payload,
) {
return Log(
payload: payload,
dateTime: _logDateTime(null),
// id: _logId(null),
);
} }
Map<String, dynamic> toJson() { factory Log.fromJson(Map<String, Object?> json) => _$LogFromJson(json);
return _$LogToJson(this);
}
@override
String toString() {
return 'Log{logLevel: $logLevel, payload: $payload, dateTime: $dateTime}';
}
} }
@freezed @freezed
@@ -127,11 +127,10 @@ extension LogsStateExt on LogsState {
final lowQuery = query.toLowerCase(); final lowQuery = query.toLowerCase();
return logs.where( return logs.where(
(log) { (log) {
final payload = log.payload?.toLowerCase(); final payload = log.payload.toLowerCase();
final logLevelName = log.logLevel.name; final logLevelName = log.logLevel.name;
return {logLevelName}.containsAll(keywords) && return {logLevelName}.containsAll(keywords) &&
((payload?.contains(lowQuery) ?? false) || ((payload.contains(lowQuery)) || logLevelName.contains(lowQuery));
logLevelName.contains(lowQuery));
}, },
).toList(); ).toList();
} }

View File

@@ -173,6 +173,17 @@ class ProxiesStyle with _$ProxiesStyle {
json == null ? defaultProxiesStyle : _$ProxiesStyleFromJson(json); json == null ? defaultProxiesStyle : _$ProxiesStyleFromJson(json);
} }
@freezed
class TextScale with _$TextScale {
const factory TextScale({
@Default(false) enable,
@Default(1.0) scale,
}) = _TextScale;
factory TextScale.fromJson(Map<String, Object?> json) =>
_$TextScaleFromJson(json);
}
@freezed @freezed
class ThemeProps with _$ThemeProps { class ThemeProps with _$ThemeProps {
const factory ThemeProps({ const factory ThemeProps({
@@ -181,6 +192,7 @@ class ThemeProps with _$ThemeProps {
@Default(ThemeMode.dark) ThemeMode themeMode, @Default(ThemeMode.dark) ThemeMode themeMode,
@Default(DynamicSchemeVariant.tonalSpot) DynamicSchemeVariant schemeVariant, @Default(DynamicSchemeVariant.tonalSpot) DynamicSchemeVariant schemeVariant,
@Default(false) bool pureBlack, @Default(false) bool pureBlack,
@Default(TextScale()) TextScale textScale,
}) = _ThemeProps; }) = _ThemeProps;
factory ThemeProps.fromJson(Map<String, Object?> json) => factory ThemeProps.fromJson(Map<String, Object?> json) =>

View File

@@ -1092,6 +1092,203 @@ abstract class _Connection implements Connection {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
Log _$LogFromJson(Map<String, dynamic> json) {
return _Log.fromJson(json);
}
/// @nodoc
mixin _$Log {
@JsonKey(name: "LogLevel")
LogLevel get logLevel => throw _privateConstructorUsedError;
@JsonKey(name: "Payload")
String get payload => throw _privateConstructorUsedError;
@JsonKey(fromJson: _logDateTime)
String get dateTime => throw _privateConstructorUsedError;
/// Serializes this Log to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of Log
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LogCopyWith<Log> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LogCopyWith<$Res> {
factory $LogCopyWith(Log value, $Res Function(Log) then) =
_$LogCopyWithImpl<$Res, Log>;
@useResult
$Res call(
{@JsonKey(name: "LogLevel") LogLevel logLevel,
@JsonKey(name: "Payload") String payload,
@JsonKey(fromJson: _logDateTime) String dateTime});
}
/// @nodoc
class _$LogCopyWithImpl<$Res, $Val extends Log> implements $LogCopyWith<$Res> {
_$LogCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Log
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? logLevel = null,
Object? payload = null,
Object? dateTime = null,
}) {
return _then(_value.copyWith(
logLevel: null == logLevel
? _value.logLevel
: logLevel // ignore: cast_nullable_to_non_nullable
as LogLevel,
payload: null == payload
? _value.payload
: payload // ignore: cast_nullable_to_non_nullable
as String,
dateTime: null == dateTime
? _value.dateTime
: dateTime // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$LogImplCopyWith<$Res> implements $LogCopyWith<$Res> {
factory _$$LogImplCopyWith(_$LogImpl value, $Res Function(_$LogImpl) then) =
__$$LogImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{@JsonKey(name: "LogLevel") LogLevel logLevel,
@JsonKey(name: "Payload") String payload,
@JsonKey(fromJson: _logDateTime) String dateTime});
}
/// @nodoc
class __$$LogImplCopyWithImpl<$Res> extends _$LogCopyWithImpl<$Res, _$LogImpl>
implements _$$LogImplCopyWith<$Res> {
__$$LogImplCopyWithImpl(_$LogImpl _value, $Res Function(_$LogImpl) _then)
: super(_value, _then);
/// Create a copy of Log
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? logLevel = null,
Object? payload = null,
Object? dateTime = null,
}) {
return _then(_$LogImpl(
logLevel: null == logLevel
? _value.logLevel
: logLevel // ignore: cast_nullable_to_non_nullable
as LogLevel,
payload: null == payload
? _value.payload
: payload // ignore: cast_nullable_to_non_nullable
as String,
dateTime: null == dateTime
? _value.dateTime
: dateTime // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$LogImpl implements _Log {
const _$LogImpl(
{@JsonKey(name: "LogLevel") this.logLevel = LogLevel.app,
@JsonKey(name: "Payload") this.payload = "",
@JsonKey(fromJson: _logDateTime) required this.dateTime});
factory _$LogImpl.fromJson(Map<String, dynamic> json) =>
_$$LogImplFromJson(json);
@override
@JsonKey(name: "LogLevel")
final LogLevel logLevel;
@override
@JsonKey(name: "Payload")
final String payload;
@override
@JsonKey(fromJson: _logDateTime)
final String dateTime;
@override
String toString() {
return 'Log(logLevel: $logLevel, payload: $payload, dateTime: $dateTime)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LogImpl &&
(identical(other.logLevel, logLevel) ||
other.logLevel == logLevel) &&
(identical(other.payload, payload) || other.payload == payload) &&
(identical(other.dateTime, dateTime) ||
other.dateTime == dateTime));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, logLevel, payload, dateTime);
/// Create a copy of Log
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LogImplCopyWith<_$LogImpl> get copyWith =>
__$$LogImplCopyWithImpl<_$LogImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$LogImplToJson(
this,
);
}
}
abstract class _Log implements Log {
const factory _Log(
{@JsonKey(name: "LogLevel") final LogLevel logLevel,
@JsonKey(name: "Payload") final String payload,
@JsonKey(fromJson: _logDateTime) required final String dateTime}) =
_$LogImpl;
factory _Log.fromJson(Map<String, dynamic> json) = _$LogImpl.fromJson;
@override
@JsonKey(name: "LogLevel")
LogLevel get logLevel;
@override
@JsonKey(name: "Payload")
String get payload;
@override
@JsonKey(fromJson: _logDateTime)
String get dateTime;
/// Create a copy of Log
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LogImplCopyWith<_$LogImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc /// @nodoc
mixin _$LogsState { mixin _$LogsState {
List<Log> get logs => throw _privateConstructorUsedError; List<Log> get logs => throw _privateConstructorUsedError;

View File

@@ -6,25 +6,6 @@ part of '../common.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Log _$LogFromJson(Map<String, dynamic> json) => Log(
logLevel: $enumDecode(_$LogLevelEnumMap, json['LogLevel']),
payload: json['Payload'] as String?,
);
Map<String, dynamic> _$LogToJson(Log instance) => <String, dynamic>{
'LogLevel': _$LogLevelEnumMap[instance.logLevel]!,
'Payload': instance.payload,
};
const _$LogLevelEnumMap = {
LogLevel.debug: 'debug',
LogLevel.info: 'info',
LogLevel.warning: 'warning',
LogLevel.error: 'error',
LogLevel.silent: 'silent',
LogLevel.app: 'app',
};
_$PackageImpl _$$PackageImplFromJson(Map<String, dynamic> json) => _$PackageImpl _$$PackageImplFromJson(Map<String, dynamic> json) =>
_$PackageImpl( _$PackageImpl(
packageName: json['packageName'] as String, packageName: json['packageName'] as String,
@@ -88,6 +69,28 @@ Map<String, dynamic> _$$ConnectionImplToJson(_$ConnectionImpl instance) =>
'chains': instance.chains, 'chains': instance.chains,
}; };
_$LogImpl _$$LogImplFromJson(Map<String, dynamic> json) => _$LogImpl(
logLevel: $enumDecodeNullable(_$LogLevelEnumMap, json['LogLevel']) ??
LogLevel.app,
payload: json['Payload'] as String? ?? "",
dateTime: _logDateTime(json['dateTime']),
);
Map<String, dynamic> _$$LogImplToJson(_$LogImpl instance) => <String, dynamic>{
'LogLevel': _$LogLevelEnumMap[instance.logLevel]!,
'Payload': instance.payload,
'dateTime': instance.dateTime,
};
const _$LogLevelEnumMap = {
LogLevel.debug: 'debug',
LogLevel.info: 'info',
LogLevel.warning: 'warning',
LogLevel.error: 'error',
LogLevel.silent: 'silent',
LogLevel.app: 'app',
};
_$DAVImpl _$$DAVImplFromJson(Map<String, dynamic> json) => _$DAVImpl( _$DAVImpl _$$DAVImplFromJson(Map<String, dynamic> json) => _$DAVImpl(
uri: json['uri'] as String, uri: json['uri'] as String,
user: json['user'] as String, user: json['user'] as String,

View File

@@ -1740,6 +1740,170 @@ abstract class _ProxiesStyle implements ProxiesStyle {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
TextScale _$TextScaleFromJson(Map<String, dynamic> json) {
return _TextScale.fromJson(json);
}
/// @nodoc
mixin _$TextScale {
dynamic get enable => throw _privateConstructorUsedError;
dynamic get scale => throw _privateConstructorUsedError;
/// Serializes this TextScale to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of TextScale
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$TextScaleCopyWith<TextScale> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TextScaleCopyWith<$Res> {
factory $TextScaleCopyWith(TextScale value, $Res Function(TextScale) then) =
_$TextScaleCopyWithImpl<$Res, TextScale>;
@useResult
$Res call({dynamic enable, dynamic scale});
}
/// @nodoc
class _$TextScaleCopyWithImpl<$Res, $Val extends TextScale>
implements $TextScaleCopyWith<$Res> {
_$TextScaleCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of TextScale
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? enable = freezed,
Object? scale = freezed,
}) {
return _then(_value.copyWith(
enable: freezed == enable
? _value.enable
: enable // ignore: cast_nullable_to_non_nullable
as dynamic,
scale: freezed == scale
? _value.scale
: scale // ignore: cast_nullable_to_non_nullable
as dynamic,
) as $Val);
}
}
/// @nodoc
abstract class _$$TextScaleImplCopyWith<$Res>
implements $TextScaleCopyWith<$Res> {
factory _$$TextScaleImplCopyWith(
_$TextScaleImpl value, $Res Function(_$TextScaleImpl) then) =
__$$TextScaleImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({dynamic enable, dynamic scale});
}
/// @nodoc
class __$$TextScaleImplCopyWithImpl<$Res>
extends _$TextScaleCopyWithImpl<$Res, _$TextScaleImpl>
implements _$$TextScaleImplCopyWith<$Res> {
__$$TextScaleImplCopyWithImpl(
_$TextScaleImpl _value, $Res Function(_$TextScaleImpl) _then)
: super(_value, _then);
/// Create a copy of TextScale
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? enable = freezed,
Object? scale = freezed,
}) {
return _then(_$TextScaleImpl(
enable: freezed == enable ? _value.enable! : enable,
scale: freezed == scale ? _value.scale! : scale,
));
}
}
/// @nodoc
@JsonSerializable()
class _$TextScaleImpl implements _TextScale {
const _$TextScaleImpl({this.enable = false, this.scale = 1.0});
factory _$TextScaleImpl.fromJson(Map<String, dynamic> json) =>
_$$TextScaleImplFromJson(json);
@override
@JsonKey()
final dynamic enable;
@override
@JsonKey()
final dynamic scale;
@override
String toString() {
return 'TextScale(enable: $enable, scale: $scale)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TextScaleImpl &&
const DeepCollectionEquality().equals(other.enable, enable) &&
const DeepCollectionEquality().equals(other.scale, scale));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(enable),
const DeepCollectionEquality().hash(scale));
/// Create a copy of TextScale
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$TextScaleImplCopyWith<_$TextScaleImpl> get copyWith =>
__$$TextScaleImplCopyWithImpl<_$TextScaleImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$TextScaleImplToJson(
this,
);
}
}
abstract class _TextScale implements TextScale {
const factory _TextScale({final dynamic enable, final dynamic scale}) =
_$TextScaleImpl;
factory _TextScale.fromJson(Map<String, dynamic> json) =
_$TextScaleImpl.fromJson;
@override
dynamic get enable;
@override
dynamic get scale;
/// Create a copy of TextScale
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$TextScaleImplCopyWith<_$TextScaleImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ThemeProps _$ThemePropsFromJson(Map<String, dynamic> json) { ThemeProps _$ThemePropsFromJson(Map<String, dynamic> json) {
return _ThemeProps.fromJson(json); return _ThemeProps.fromJson(json);
} }
@@ -1751,6 +1915,7 @@ mixin _$ThemeProps {
ThemeMode get themeMode => throw _privateConstructorUsedError; ThemeMode get themeMode => throw _privateConstructorUsedError;
DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError; DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError;
bool get pureBlack => throw _privateConstructorUsedError; bool get pureBlack => throw _privateConstructorUsedError;
TextScale get textScale => throw _privateConstructorUsedError;
/// Serializes this ThemeProps to a JSON map. /// Serializes this ThemeProps to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -1773,7 +1938,10 @@ abstract class $ThemePropsCopyWith<$Res> {
List<int> primaryColors, List<int> primaryColors,
ThemeMode themeMode, ThemeMode themeMode,
DynamicSchemeVariant schemeVariant, DynamicSchemeVariant schemeVariant,
bool pureBlack}); bool pureBlack,
TextScale textScale});
$TextScaleCopyWith<$Res> get textScale;
} }
/// @nodoc /// @nodoc
@@ -1796,6 +1964,7 @@ class _$ThemePropsCopyWithImpl<$Res, $Val extends ThemeProps>
Object? themeMode = null, Object? themeMode = null,
Object? schemeVariant = null, Object? schemeVariant = null,
Object? pureBlack = null, Object? pureBlack = null,
Object? textScale = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
primaryColor: freezed == primaryColor primaryColor: freezed == primaryColor
@@ -1818,8 +1987,22 @@ class _$ThemePropsCopyWithImpl<$Res, $Val extends ThemeProps>
? _value.pureBlack ? _value.pureBlack
: pureBlack // ignore: cast_nullable_to_non_nullable : pureBlack // ignore: cast_nullable_to_non_nullable
as bool, as bool,
textScale: null == textScale
? _value.textScale
: textScale // ignore: cast_nullable_to_non_nullable
as TextScale,
) as $Val); ) as $Val);
} }
/// Create a copy of ThemeProps
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$TextScaleCopyWith<$Res> get textScale {
return $TextScaleCopyWith<$Res>(_value.textScale, (value) {
return _then(_value.copyWith(textScale: value) as $Val);
});
}
} }
/// @nodoc /// @nodoc
@@ -1835,7 +2018,11 @@ abstract class _$$ThemePropsImplCopyWith<$Res>
List<int> primaryColors, List<int> primaryColors,
ThemeMode themeMode, ThemeMode themeMode,
DynamicSchemeVariant schemeVariant, DynamicSchemeVariant schemeVariant,
bool pureBlack}); bool pureBlack,
TextScale textScale});
@override
$TextScaleCopyWith<$Res> get textScale;
} }
/// @nodoc /// @nodoc
@@ -1856,6 +2043,7 @@ class __$$ThemePropsImplCopyWithImpl<$Res>
Object? themeMode = null, Object? themeMode = null,
Object? schemeVariant = null, Object? schemeVariant = null,
Object? pureBlack = null, Object? pureBlack = null,
Object? textScale = null,
}) { }) {
return _then(_$ThemePropsImpl( return _then(_$ThemePropsImpl(
primaryColor: freezed == primaryColor primaryColor: freezed == primaryColor
@@ -1878,6 +2066,10 @@ class __$$ThemePropsImplCopyWithImpl<$Res>
? _value.pureBlack ? _value.pureBlack
: pureBlack // ignore: cast_nullable_to_non_nullable : pureBlack // ignore: cast_nullable_to_non_nullable
as bool, as bool,
textScale: null == textScale
? _value.textScale
: textScale // ignore: cast_nullable_to_non_nullable
as TextScale,
)); ));
} }
} }
@@ -1890,7 +2082,8 @@ class _$ThemePropsImpl implements _ThemeProps {
final List<int> primaryColors = defaultPrimaryColors, final List<int> primaryColors = defaultPrimaryColors,
this.themeMode = ThemeMode.dark, this.themeMode = ThemeMode.dark,
this.schemeVariant = DynamicSchemeVariant.tonalSpot, this.schemeVariant = DynamicSchemeVariant.tonalSpot,
this.pureBlack = false}) this.pureBlack = false,
this.textScale = const TextScale()})
: _primaryColors = primaryColors; : _primaryColors = primaryColors;
factory _$ThemePropsImpl.fromJson(Map<String, dynamic> json) => factory _$ThemePropsImpl.fromJson(Map<String, dynamic> json) =>
@@ -1916,10 +2109,13 @@ class _$ThemePropsImpl implements _ThemeProps {
@override @override
@JsonKey() @JsonKey()
final bool pureBlack; final bool pureBlack;
@override
@JsonKey()
final TextScale textScale;
@override @override
String toString() { String toString() {
return 'ThemeProps(primaryColor: $primaryColor, primaryColors: $primaryColors, themeMode: $themeMode, schemeVariant: $schemeVariant, pureBlack: $pureBlack)'; return 'ThemeProps(primaryColor: $primaryColor, primaryColors: $primaryColors, themeMode: $themeMode, schemeVariant: $schemeVariant, pureBlack: $pureBlack, textScale: $textScale)';
} }
@override @override
@@ -1936,7 +2132,9 @@ class _$ThemePropsImpl implements _ThemeProps {
(identical(other.schemeVariant, schemeVariant) || (identical(other.schemeVariant, schemeVariant) ||
other.schemeVariant == schemeVariant) && other.schemeVariant == schemeVariant) &&
(identical(other.pureBlack, pureBlack) || (identical(other.pureBlack, pureBlack) ||
other.pureBlack == pureBlack)); other.pureBlack == pureBlack) &&
(identical(other.textScale, textScale) ||
other.textScale == textScale));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -1947,7 +2145,8 @@ class _$ThemePropsImpl implements _ThemeProps {
const DeepCollectionEquality().hash(_primaryColors), const DeepCollectionEquality().hash(_primaryColors),
themeMode, themeMode,
schemeVariant, schemeVariant,
pureBlack); pureBlack,
textScale);
/// Create a copy of ThemeProps /// Create a copy of ThemeProps
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -1971,7 +2170,8 @@ abstract class _ThemeProps implements ThemeProps {
final List<int> primaryColors, final List<int> primaryColors,
final ThemeMode themeMode, final ThemeMode themeMode,
final DynamicSchemeVariant schemeVariant, final DynamicSchemeVariant schemeVariant,
final bool pureBlack}) = _$ThemePropsImpl; final bool pureBlack,
final TextScale textScale}) = _$ThemePropsImpl;
factory _ThemeProps.fromJson(Map<String, dynamic> json) = factory _ThemeProps.fromJson(Map<String, dynamic> json) =
_$ThemePropsImpl.fromJson; _$ThemePropsImpl.fromJson;
@@ -1986,6 +2186,8 @@ abstract class _ThemeProps implements ThemeProps {
DynamicSchemeVariant get schemeVariant; DynamicSchemeVariant get schemeVariant;
@override @override
bool get pureBlack; bool get pureBlack;
@override
TextScale get textScale;
/// Create a copy of ThemeProps /// Create a copy of ThemeProps
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View File

@@ -222,6 +222,18 @@ const _$ProxyCardTypeEnumMap = {
ProxyCardType.min: 'min', ProxyCardType.min: 'min',
}; };
_$TextScaleImpl _$$TextScaleImplFromJson(Map<String, dynamic> json) =>
_$TextScaleImpl(
enable: json['enable'] ?? false,
scale: json['scale'] ?? 1.0,
);
Map<String, dynamic> _$$TextScaleImplToJson(_$TextScaleImpl instance) =>
<String, dynamic>{
'enable': instance.enable,
'scale': instance.scale,
};
_$ThemePropsImpl _$$ThemePropsImplFromJson(Map<String, dynamic> json) => _$ThemePropsImpl _$$ThemePropsImplFromJson(Map<String, dynamic> json) =>
_$ThemePropsImpl( _$ThemePropsImpl(
primaryColor: (json['primaryColor'] as num?)?.toInt(), primaryColor: (json['primaryColor'] as num?)?.toInt(),
@@ -235,6 +247,9 @@ _$ThemePropsImpl _$$ThemePropsImplFromJson(Map<String, dynamic> json) =>
_$DynamicSchemeVariantEnumMap, json['schemeVariant']) ?? _$DynamicSchemeVariantEnumMap, json['schemeVariant']) ??
DynamicSchemeVariant.tonalSpot, DynamicSchemeVariant.tonalSpot,
pureBlack: json['pureBlack'] as bool? ?? false, pureBlack: json['pureBlack'] as bool? ?? false,
textScale: json['textScale'] == null
? const TextScale()
: TextScale.fromJson(json['textScale'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$$ThemePropsImplToJson(_$ThemePropsImpl instance) => Map<String, dynamic> _$$ThemePropsImplToJson(_$ThemePropsImpl instance) =>
@@ -244,6 +259,7 @@ Map<String, dynamic> _$$ThemePropsImplToJson(_$ThemePropsImpl instance) =>
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!, 'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!,
'pureBlack': instance.pureBlack, 'pureBlack': instance.pureBlack,
'textScale': instance.textScale,
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {

View File

@@ -23,6 +23,7 @@ typedef UpdateTasks = List<FutureOr Function()>;
class GlobalState { class GlobalState {
static GlobalState? _instance; static GlobalState? _instance;
Map<Key, double> cacheScrollPosition = {}; Map<Key, double> cacheScrollPosition = {};
Map<Key, FixedMap<String, double>> cacheHeightMap = {};
bool isService = false; bool isService = false;
Timer? timer; Timer? timer;
Timer? groupsUpdateTimer; Timer? groupsUpdateTimer;
@@ -66,7 +67,8 @@ class GlobalState {
_initDynamicColor() async { _initDynamicColor() async {
try { try {
corePalette = await DynamicColorPlugin.getCorePalette(); corePalette = await DynamicColorPlugin.getCorePalette();
accentColor = await DynamicColorPlugin.getAccentColor() ?? Color(defaultPrimaryColor); accentColor = await DynamicColorPlugin.getAccentColor() ??
Color(defaultPrimaryColor);
} catch (_) {} } catch (_) {}
} }

View File

@@ -137,7 +137,7 @@ class DonutChartPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2); final center = Offset(size.width / 2, size.height / 2);
const strokeWidth = 10.0; final strokeWidth = 10.0.ap;
final radius = min(size.width / 2, size.height / 2) - strokeWidth / 2; final radius = min(size.width / 2, size.height / 2) - strokeWidth / 2;
final gapAngle = 2 * asin(strokeWidth * 1 / (2 * radius)) * 1.2; final gapAngle = 2 * asin(strokeWidth * 1 / (2 * radius)) * 1.2;

View File

@@ -62,6 +62,22 @@ class OpenDelegate extends Delegate {
}); });
} }
class NextDelegate extends Delegate {
final Widget widget;
final String title;
final double? maxWidth;
final Widget? action;
final bool blur;
const NextDelegate({
required this.title,
required this.widget,
this.maxWidth,
this.action,
this.blur = true,
});
}
class OptionsDelegate<T> extends Delegate { class OptionsDelegate<T> extends Delegate {
final List<T> options; final List<T> options;
final String title; final String title;
@@ -138,6 +154,21 @@ class ListItem<T> extends StatelessWidget {
this.tileTitleAlignment = ListTileTitleAlignment.center, this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTap = null; }) : onTap = null;
const ListItem.next({
super.key,
required this.title,
this.subtitle,
this.leading,
this.padding = const EdgeInsets.symmetric(horizontal: 16),
this.trailing,
required NextDelegate this.delegate,
this.horizontalTitleGap,
this.dense,
this.titleTextStyle,
this.subtitleTextStyle,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTap = null;
const ListItem.options({ const ListItem.options({
super.key, super.key,
required this.title, required this.title,
@@ -285,6 +316,34 @@ class ListItem<T> extends StatelessWidget {
}, },
); );
} }
if (delegate is NextDelegate) {
final nextDelegate = delegate as NextDelegate;
final child = SafeArea(
child: nextDelegate.widget,
);
return _buildListTile(
onTap: () {
showExtend(
context,
props: ExtendProps(
blur: nextDelegate.blur,
maxWidth: nextDelegate.maxWidth,
),
builder: (_, type) {
return AdaptiveSheetScaffold(
actions: [
if (nextDelegate.action != null) nextDelegate.action!,
],
type: type,
body: child,
title: nextDelegate.title,
);
},
);
},
);
}
if (delegate is OptionsDelegate) { if (delegate is OptionsDelegate) {
final optionsDelegate = delegate as OptionsDelegate<T>; final optionsDelegate = delegate as OptionsDelegate<T>;
return _buildListTile( return _buildListTile(
@@ -353,14 +412,11 @@ class ListItem<T> extends StatelessWidget {
radioDelegate.onChanged!(radioDelegate.value); radioDelegate.onChanged!(radioDelegate.value);
} }
}, },
leading: SizedBox( leading: Radio<T>(
width: 32, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
height: 32, value: radioDelegate.value,
child: Radio<T>( groupValue: radioDelegate.groupValue,
value: radioDelegate.value, onChanged: radioDelegate.onChanged,
groupValue: radioDelegate.groupValue,
onChanged: radioDelegate.onChanged,
),
), ),
trailing: trailing, trailing: trailing,
); );

View File

@@ -0,0 +1,33 @@
import 'package:fl_clash/models/config.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class TextScaleNotification extends StatelessWidget {
final Widget child;
final Function(TextScale textScale) onNotification;
const TextScaleNotification({
super.key,
required this.child,
required this.onNotification,
});
@override
Widget build(BuildContext context) {
return Consumer(
builder: (_, ref, __) {
ref.listen(
themeSettingProvider.select((state) => state.textScale),
(prev, next) {
if (prev != next) {
onNotification(next);
}
},
);
return child;
},
child: child,
);
}
}

View File

@@ -125,25 +125,25 @@ class CommonScaffoldState extends State<CommonScaffold> {
} }
} }
ThemeData _appBarTheme(BuildContext context) { Widget _buildSearchingAppBarTheme(Widget child) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme; final ColorScheme colorScheme = theme.colorScheme;
return theme.copyWith( return Theme(
appBarTheme: AppBarTheme( data: theme.copyWith(
systemOverlayStyle: colorScheme.brightness == Brightness.dark appBarTheme: theme.appBarTheme.copyWith(
? SystemUiOverlayStyle.light backgroundColor: colorScheme.brightness == Brightness.dark
: SystemUiOverlayStyle.dark, ? Colors.grey[900]
backgroundColor: colorScheme.brightness == Brightness.dark : Colors.white,
? Colors.grey[900] iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey),
: Colors.white, titleTextStyle: theme.textTheme.titleLarge,
iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), toolbarTextStyle: theme.textTheme.bodyMedium,
titleTextStyle: theme.textTheme.titleLarge, ),
toolbarTextStyle: theme.textTheme.bodyMedium, inputDecorationTheme: InputDecorationTheme(
), hintStyle: theme.inputDecorationTheme.hintStyle,
inputDecorationTheme: InputDecorationTheme( border: InputBorder.none,
hintStyle: theme.inputDecorationTheme.hintStyle, ),
border: InputBorder.none,
), ),
child: child,
); );
} }
@@ -318,72 +318,66 @@ class CommonScaffoldState extends State<CommonScaffold> {
child: appBar, child: appBar,
); );
} }
return _isSearch return _isSearch ? _buildSearchingAppBarTheme(appBar) : appBar;
? Theme(
data: _appBarTheme(context),
child: CommonPopScope(
onPop: () {
if (_isSearch) {
_handleExitSearching();
return false;
}
return true;
},
child: appBar,
),
)
: appBar;
} }
PreferredSizeWidget _buildAppBar() { PreferredSizeWidget _buildAppBar() {
return PreferredSize( return PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight), preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack( child: Theme(
alignment: Alignment.bottomCenter, data: Theme.of(context).copyWith(
children: [ appBarTheme: AppBarTheme(
ValueListenableBuilder<AppBarState>( systemOverlayStyle: SystemUiOverlayStyle(
valueListenable: _appBarState, statusBarColor: Colors.transparent,
builder: (_, state, __) { statusBarIconBrightness:
return _buildAppBarWrap( Theme.of(context).brightness == Brightness.dark
AppBar( ? Brightness.light
centerTitle: widget.centerTitle ?? false, : Brightness.dark,
systemOverlayStyle: SystemUiOverlayStyle( systemNavigationBarIconBrightness:
statusBarColor: Colors.transparent, Theme.of(context).brightness == Brightness.dark
statusBarIconBrightness: ? Brightness.light
Theme.of(context).brightness == Brightness.dark : Brightness.dark,
? Brightness.light systemNavigationBarColor: widget.bottomNavigationBar != null
: Brightness.dark, ? context.colorScheme.surfaceContainer
systemNavigationBarIconBrightness: : context.colorScheme.surface,
Theme.of(context).brightness == Brightness.dark systemNavigationBarDividerColor: Colors.transparent,
? Brightness.light ),
: Brightness.dark, ),
systemNavigationBarColor: widget.bottomNavigationBar != null ),
? context.colorScheme.surfaceContainer child: widget.appBar ??
: context.colorScheme.surface, Stack(
systemNavigationBarDividerColor: Colors.transparent, alignment: Alignment.bottomCenter,
), children: [
automaticallyImplyLeading: widget.automaticallyImplyLeading, ValueListenableBuilder<AppBarState>(
leading: _buildLeading(), valueListenable: _appBarState,
title: _buildTitle(state.searchState), builder: (_, state, __) {
actions: _buildActions( return _buildAppBarWrap(
state.searchState != null, AppBar(
state.actions.isNotEmpty centerTitle: widget.centerTitle ?? false,
? state.actions automaticallyImplyLeading:
: widget.actions ?? [], widget.automaticallyImplyLeading,
), leading: _buildLeading(),
title: _buildTitle(state.searchState),
actions: _buildActions(
state.searchState != null,
state.actions.isNotEmpty
? state.actions
: widget.actions ?? [],
),
),
);
},
), ),
); ValueListenableBuilder(
}, valueListenable: _loading,
), builder: (_, value, __) {
ValueListenableBuilder( return value == true
valueListenable: _loading, ? const LinearProgressIndicator()
builder: (_, value, __) { : Container();
return value == true },
? const LinearProgressIndicator() ),
: Container(); ],
}, ),
),
],
), ),
); );
} }
@@ -391,49 +385,51 @@ class CommonScaffoldState extends State<CommonScaffold> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(widget.appBar != null || widget.title != null); assert(widget.appBar != null || widget.title != null);
final body = Column( final body = SafeArea(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
ValueListenableBuilder( children: [
valueListenable: _keywordsNotifier, ValueListenableBuilder(
builder: (_, keywords, __) { valueListenable: _keywordsNotifier,
WidgetsBinding.instance.addPostFrameCallback((_) { builder: (_, keywords, __) {
if (_onKeywordsUpdate != null) { WidgetsBinding.instance.addPostFrameCallback((_) {
_onKeywordsUpdate!(keywords); if (_onKeywordsUpdate != null) {
_onKeywordsUpdate!(keywords);
}
});
if (keywords.isEmpty) {
return SizedBox();
} }
}); return Padding(
if (keywords.isEmpty) { padding: const EdgeInsets.symmetric(
return SizedBox(); horizontal: 16,
} vertical: 16,
return Padding( ),
padding: const EdgeInsets.symmetric( child: Wrap(
horizontal: 16, runSpacing: 8,
vertical: 16, spacing: 8,
), children: [
child: Wrap( for (final keyword in keywords)
runSpacing: 8, CommonChip(
spacing: 8, label: keyword,
children: [ type: ChipType.delete,
for (final keyword in keywords) onPressed: () {
CommonChip( _deleteKeyword(keyword);
label: keyword, },
type: ChipType.delete, ),
onPressed: () { ],
_deleteKeyword(keyword); ),
}, );
), },
], ),
), Expanded(
); child: widget.body,
}, ),
), ],
Expanded( ),
child: widget.body,
),
],
); );
final scaffold = Scaffold( final scaffold = Scaffold(
appBar: widget.appBar ?? _buildAppBar(), appBar: _buildAppBar(),
body: body, body: body,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
floatingActionButton: ValueListenableBuilder<Widget?>( floatingActionButton: ValueListenableBuilder<Widget?>(

View File

@@ -1058,18 +1058,18 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
} }
void _paintThumb(PaintingContext context, Offset offset, Rect thumbRect) { void _paintThumb(PaintingContext context, Offset offset, Rect thumbRect) {
const List<BoxShadow> thumbShadow = <BoxShadow>[ // const List<BoxShadow> thumbShadow = <BoxShadow>[
BoxShadow(color: Color(0x1F000000), offset: Offset(0, 3), blurRadius: 8), // BoxShadow(color: Color(0x1F000000), offset: Offset(0, 3), blurRadius: 8),
BoxShadow(color: Color(0x0A000000), offset: Offset(0, 3), blurRadius: 1), // BoxShadow(color: Color(0x0A000000), offset: Offset(0, 3), blurRadius: 1),
]; // ];
final RRect thumbRRect = final RRect thumbRRect =
RRect.fromRectAndRadius(thumbRect.shift(offset), _kThumbRadius); RRect.fromRectAndRadius(thumbRect.shift(offset), _kThumbRadius);
for (final BoxShadow shadow in thumbShadow) { // for (final BoxShadow shadow in thumbShadow) {
context.canvas // context.canvas
.drawRRect(thumbRRect.shift(shadow.offset), shadow.toPaint()); // .drawRRect(thumbRRect.shift(shadow.offset), shadow.toPaint());
} // }
context.canvas.drawRRect( context.canvas.drawRRect(
thumbRRect.inflate(0.5), Paint()..color = const Color(0x0A000000)); thumbRRect.inflate(0.5), Paint()..color = const Color(0x0A000000));

View File

@@ -84,6 +84,7 @@ class EmojiText extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RichText( return RichText(
textScaler: MediaQuery.of(context).textScaler,
maxLines: maxLines, maxLines: maxLines,
overflow: overflow ?? TextOverflow.clip, overflow: overflow ?? TextOverflow.clip,
text: TextSpan( text: TextSpan(

View File

@@ -32,3 +32,4 @@ export 'effect.dart';
export 'palette.dart'; export 'palette.dart';
export 'tab.dart'; export 'tab.dart';
export 'container.dart'; export 'container.dart';
export 'notification.dart';

View File

@@ -1,7 +1,7 @@
name: fl_clash name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none' publish_to: 'none'
version: 0.8.83+202504221 version: 0.8.83+202504231
environment: environment:
sdk: '>=3.1.0 <4.0.0' sdk: '>=3.1.0 <4.0.0'
@@ -93,5 +93,5 @@ ffigen:
flutter_intl: flutter_intl:
enabled: true enabled: true
class_name: AppLocalizations class_name: AppLocalizations
arb_dir: lib/l10n/arb arb_dir: arb
output_dir: lib/l10n output_dir: lib/l10n

View File

@@ -4,6 +4,7 @@ import requests
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TAG = os.getenv("TAG") TAG = os.getenv("TAG")
RUN_ID = os.getenv("RUN_ID")
IS_STABLE = "-" not in TAG IS_STABLE = "-" not in TAG
@@ -45,7 +46,8 @@ if TAG:
if IS_STABLE: if IS_STABLE:
text += f"\nhttps://github.com/chen08209/FlClash/releases/tag/{TAG}\n" text += f"\nhttps://github.com/chen08209/FlClash/releases/tag/{TAG}\n"
else:
text += f"\nhttps://github.com/chen08209/FlClash/actions/runs/{RUN_ID}\n"
if os.path.exists(release): if os.path.exists(release):
text += "\n" text += "\n"

View File

@@ -0,0 +1,83 @@
[Setup]
AppId={{APP_ID}}
AppVersion={{APP_VERSION}}
AppName={{DISPLAY_NAME}}
AppPublisher={{PUBLISHER_NAME}}
AppPublisherURL={{PUBLISHER_URL}}
AppSupportURL={{PUBLISHER_URL}}
AppUpdatesURL={{PUBLISHER_URL}}
DefaultDirName={{INSTALL_DIR_NAME}}
DisableProgramGroupPage=yes
OutputDir=.
OutputBaseFilename={{OUTPUT_BASE_FILENAME}}
Compression=lzma
SolidCompression=yes
SetupIconFile={{SETUP_ICON_FILE}}
WizardStyle=modern
PrivilegesRequired={{PRIVILEGES_REQUIRED}}
ArchitecturesAllowed={{ARCH}}
ArchitecturesInstallIn64BitMode={{ARCH}}
[Code]
procedure KillProcesses;
var
Processes: TArrayOfString;
i: Integer;
ResultCode: Integer;
begin
Processes := ['FlClash.exe', 'FlClashCore.exe', 'FlClashHelperService.exe'];
for i := 0 to GetArrayLength(Processes)-1 do
begin
Exec('taskkill', '/f /im ' + Processes[i], '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
end;
function InitializeSetup(): Boolean;
begin
KillProcesses;
Result := True;
end;
[Languages]
{% for locale in LOCALES %}
{% if locale.lang == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %}
{% if locale.lang == 'hy' %}Name: "armenian"; MessagesFile: "compiler:Languages\\Armenian.isl"{% endif %}
{% if locale.lang == 'bg' %}Name: "bulgarian"; MessagesFile: "compiler:Languages\\Bulgarian.isl"{% endif %}
{% if locale.lang == 'ca' %}Name: "catalan"; MessagesFile: "compiler:Languages\\Catalan.isl"{% endif %}
{% if locale.lang == 'zh' %}
Name: "chineseSimplified"; MessagesFile: {% if locale.file %}{{ locale.file }}{% else %}"compiler:Languages\\ChineseSimplified.isl"{% endif %}
{% endif %}
{% if locale.lang == 'co' %}Name: "corsican"; MessagesFile: "compiler:Languages\\Corsican.isl"{% endif %}
{% if locale.lang == 'cs' %}Name: "czech"; MessagesFile: "compiler:Languages\\Czech.isl"{% endif %}
{% if locale.lang == 'da' %}Name: "danish"; MessagesFile: "compiler:Languages\\Danish.isl"{% endif %}
{% if locale.lang == 'nl' %}Name: "dutch"; MessagesFile: "compiler:Languages\\Dutch.isl"{% endif %}
{% if locale.lang == 'fi' %}Name: "finnish"; MessagesFile: "compiler:Languages\\Finnish.isl"{% endif %}
{% if locale.lang == 'fr' %}Name: "french"; MessagesFile: "compiler:Languages\\French.isl"{% endif %}
{% if locale.lang == 'de' %}Name: "german"; MessagesFile: "compiler:Languages\\German.isl"{% endif %}
{% if locale.lang == 'he' %}Name: "hebrew"; MessagesFile: "compiler:Languages\\Hebrew.isl"{% endif %}
{% if locale.lang == 'is' %}Name: "icelandic"; MessagesFile: "compiler:Languages\\Icelandic.isl"{% endif %}
{% if locale.lang == 'it' %}Name: "italian"; MessagesFile: "compiler:Languages\\Italian.isl"{% endif %}
{% if locale.lang == 'ja' %}Name: "japanese"; MessagesFile: "compiler:Languages\\Japanese.isl"{% endif %}
{% if locale.lang == 'no' %}Name: "norwegian"; MessagesFile: "compiler:Languages\\Norwegian.isl"{% endif %}
{% if locale.lang == 'pl' %}Name: "polish"; MessagesFile: "compiler:Languages\\Polish.isl"{% endif %}
{% if locale.lang == 'pt' %}Name: "portuguese"; MessagesFile: "compiler:Languages\\Portuguese.isl"{% endif %}
{% if locale.lang == 'ru' %}Name: "russian"; MessagesFile: "compiler:Languages\\Russian.isl"{% endif %}
{% if locale.lang == 'sk' %}Name: "slovak"; MessagesFile: "compiler:Languages\\Slovak.isl"{% endif %}
{% if locale.lang == 'sl' %}Name: "slovenian"; MessagesFile: "compiler:Languages\\Slovenian.isl"{% endif %}
{% if locale.lang == 'es' %}Name: "spanish"; MessagesFile: "compiler:Languages\\Spanish.isl"{% endif %}
{% if locale.lang == 'tr' %}Name: "turkish"; MessagesFile: "compiler:Languages\\Turkish.isl"{% endif %}
{% if locale.lang == 'uk' %}Name: "ukrainian"; MessagesFile: "compiler:Languages\\Ukrainian.isl"{% endif %}
{% endfor %}
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkedonce{% endif %}
[Files]
Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"
Name: "{autodesktop}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; Tasks: desktopicon
[Run]
Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: {% if PRIVILEGES_REQUIRED == 'admin' %}runascurrentuser{% endif %} nowait postinstall skipifsilent

View File

@@ -1,3 +1,4 @@
script_template: inno_setup.iss
app_id: 728B3532-C74B-4870-9068-BE70FE12A3E6 app_id: 728B3532-C74B-4870-9068-BE70FE12A3E6
app_name: FlClash app_name: FlClash
publisher: chen08209 publisher: chen08209
@@ -9,4 +10,5 @@ setup_icon_file: ..\windows\runner\resources\app_icon.ico
locales: locales:
- lang: zh - lang: zh
file: ..\windows\packaging\exe\ChineseSimplified.isl file: ..\windows\packaging\exe\ChineseSimplified.isl
- lang: en - lang: en
privileges_required: admin