Compare commits
1 Commits
v0.8.88-pr
...
v0.8.88-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0183414fe |
@@ -100,11 +100,11 @@ object Service {
|
||||
|
||||
private suspend fun awaitIResultInterface(
|
||||
block: (IResultInterface) -> Unit
|
||||
): Unit = suspendCancellableCoroutine { continuation ->
|
||||
): Long = suspendCancellableCoroutine { continuation ->
|
||||
val callback = object : IResultInterface.Stub() {
|
||||
override fun onResult() {
|
||||
override fun onResult(time: Long) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(Unit)
|
||||
continuation.resume(time)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,19 +119,25 @@ object Service {
|
||||
}
|
||||
|
||||
|
||||
suspend fun startService(options: VpnOptions) {
|
||||
delegate.useService {
|
||||
suspend fun startService(options: VpnOptions, runTime: Long): Long {
|
||||
return delegate.useService {
|
||||
awaitIResultInterface { callback ->
|
||||
it.startService(options, callback)
|
||||
it.startService(options, runTime, callback)
|
||||
}
|
||||
}
|
||||
}.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
suspend fun stopService() {
|
||||
delegate.useService {
|
||||
suspend fun stopService(): Long {
|
||||
return delegate.useService {
|
||||
awaitIResultInterface { callback ->
|
||||
it.stopService(callback)
|
||||
}
|
||||
}
|
||||
}.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
suspend fun getRunTime(): Long {
|
||||
return delegate.useService {
|
||||
it.runTime
|
||||
}.getOrNull() ?: 0L
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,18 @@ object State {
|
||||
action?.invoke()
|
||||
}
|
||||
|
||||
suspend fun handleSyncState() {
|
||||
runLock.withLock {
|
||||
Service.bind()
|
||||
runTime = Service.getRunTime()
|
||||
val runState = when (runTime == 0L) {
|
||||
true -> RunState.STOP
|
||||
false -> RunState.START
|
||||
}
|
||||
runStateFlow.tryEmit(runState)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleStartServiceAction() {
|
||||
tilePlugin?.handleStart()
|
||||
if (flutterEngine != null) {
|
||||
@@ -91,6 +103,9 @@ object State {
|
||||
|
||||
suspend fun startServiceWithEngine() {
|
||||
runLock.withLock {
|
||||
if (serviceFlutterEngine != null || runStateFlow.value == RunState.PENDING || runStateFlow.value == RunState.START) {
|
||||
return
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
serviceFlutterEngine = FlutterEngine(GlobalState.application)
|
||||
serviceFlutterEngine?.plugins?.add(ServicePlugin())
|
||||
@@ -119,8 +134,7 @@ object State {
|
||||
return@launch
|
||||
}
|
||||
appPlugin?.prepare(options.enable) {
|
||||
Service.startService(options)
|
||||
runTime = System.currentTimeMillis()
|
||||
runTime = Service.startService(options, runTime)
|
||||
runStateFlow.tryEmit(RunState.START)
|
||||
}
|
||||
}
|
||||
@@ -135,9 +149,8 @@ object State {
|
||||
return@launch
|
||||
}
|
||||
runStateFlow.tryEmit(RunState.PENDING)
|
||||
Service.stopService()
|
||||
runTime = Service.stopService()
|
||||
runStateFlow.tryEmit(RunState.STOP)
|
||||
runTime = 0
|
||||
}
|
||||
destroyServiceEngine()
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class TileService : TileService() {
|
||||
scope?.cancel()
|
||||
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
scope?.launch {
|
||||
State.handleSyncState()
|
||||
State.runStateFlow.collect {
|
||||
updateTile(it)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
"init" -> {
|
||||
handleInit(result)
|
||||
handleInit(call, result)
|
||||
}
|
||||
|
||||
"shutdown" -> {
|
||||
@@ -131,11 +131,16 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
}
|
||||
|
||||
fun handleInit(result: MethodChannel.Result) {
|
||||
fun handleInit(call: MethodCall, result: MethodChannel.Result) {
|
||||
Service.bind()
|
||||
launch {
|
||||
Service.setEventListener {
|
||||
handleSendEvent(it)
|
||||
val needSetEventListener = call.arguments<Boolean>() ?: false
|
||||
when (needSetEventListener) {
|
||||
true -> Service.setEventListener {
|
||||
handleSendEvent(it)
|
||||
}
|
||||
|
||||
false -> Service.setEventListener(null)
|
||||
}.onSuccess {
|
||||
result.success("")
|
||||
}.onFailure {
|
||||
@@ -147,6 +152,9 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
|
||||
private fun handleGetRunTime(result: MethodChannel.Result) {
|
||||
return result.success(State.runTime)
|
||||
launch {
|
||||
State.handleSyncState()
|
||||
result.success(State.runTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
android:process=":remote">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="service" />
|
||||
android:value="proxy" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
|
||||
@@ -10,8 +10,9 @@ import com.follow.clash.service.models.NotificationParams;
|
||||
interface IRemoteInterface {
|
||||
void invokeAction(in String data, in ICallbackInterface callback);
|
||||
void updateNotificationParams(in NotificationParams params);
|
||||
void startService(in VpnOptions options, in IResultInterface result);
|
||||
void startService(in VpnOptions options, in long runTime, in IResultInterface result);
|
||||
void stopService(in IResultInterface result);
|
||||
void setEventListener(in IEventInterface event);
|
||||
void setCrashlytics(in boolean enable);
|
||||
long getRunTime();
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
package com.follow.clash.service;
|
||||
|
||||
interface IResultInterface {
|
||||
oneway void onResult();
|
||||
oneway void onResult(in long runTime);
|
||||
}
|
||||
@@ -8,23 +8,20 @@ import com.follow.clash.common.ServiceDelegate
|
||||
import com.follow.clash.common.chunkedForAidl
|
||||
import com.follow.clash.common.intent
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.service.State.delegate
|
||||
import com.follow.clash.service.State.intent
|
||||
import com.follow.clash.service.State.runLock
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
import com.follow.clash.service.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.UUID
|
||||
|
||||
class RemoteService : Service(),
|
||||
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
|
||||
private var delegate: ServiceDelegate<IBaseService>? = null
|
||||
private var intent: Intent? = null
|
||||
|
||||
val runLock = Mutex()
|
||||
|
||||
private fun handleStopService(result: IResultInterface) {
|
||||
launch {
|
||||
runLock.withLock {
|
||||
@@ -32,7 +29,8 @@ class RemoteService : Service(),
|
||||
service.stop()
|
||||
delegate?.unbind()
|
||||
}
|
||||
result.onResult()
|
||||
State.runTime = 0
|
||||
result.onResult(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +41,7 @@ class RemoteService : Service(),
|
||||
delegate = null
|
||||
}
|
||||
|
||||
private fun handleStartService(result: IResultInterface) {
|
||||
private fun handleStartService(runTime: Long, result: IResultInterface) {
|
||||
launch {
|
||||
runLock.withLock {
|
||||
val nextIntent = when (State.options?.enable == true) {
|
||||
@@ -65,7 +63,11 @@ class RemoteService : Service(),
|
||||
delegate?.useService { service ->
|
||||
service.start()
|
||||
}
|
||||
result.onResult()
|
||||
State.runTime = when (runTime != 0L) {
|
||||
true -> runTime
|
||||
false -> System.currentTimeMillis()
|
||||
}
|
||||
result.onResult(State.runTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,10 +91,11 @@ class RemoteService : Service(),
|
||||
|
||||
override fun startService(
|
||||
options: VpnOptions,
|
||||
runtime: Long,
|
||||
result: IResultInterface,
|
||||
) {
|
||||
State.options = options
|
||||
handleStartService(result)
|
||||
handleStartService(runtime, result)
|
||||
}
|
||||
|
||||
override fun stopService(result: IResultInterface) {
|
||||
@@ -100,7 +103,7 @@ class RemoteService : Service(),
|
||||
}
|
||||
|
||||
override fun setEventListener(eventListener: IEventInterface?) {
|
||||
GlobalState.log("isRemoveEventListener is ${eventListener == null}")
|
||||
GlobalState.log("RemoveEventListener ${eventListener == null}")
|
||||
when (eventListener != null) {
|
||||
true -> Core.callSetEventListener {
|
||||
runCatching {
|
||||
@@ -120,9 +123,18 @@ class RemoteService : Service(),
|
||||
override fun setCrashlytics(enable: Boolean) {
|
||||
GlobalState.setCrashlytics(enable)
|
||||
}
|
||||
|
||||
override fun getRunTime(): Long {
|
||||
return State.runTime
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
GlobalState.log("Remote service destroy")
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,22 @@
|
||||
package com.follow.clash.service
|
||||
|
||||
import android.content.Intent
|
||||
import com.follow.clash.common.ServiceDelegate
|
||||
import com.follow.clash.service.models.NotificationParams
|
||||
import com.follow.clash.service.models.VpnOptions
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
||||
object State {
|
||||
var options: VpnOptions? = null
|
||||
var notificationParamsFlow: MutableStateFlow<NotificationParams?> = MutableStateFlow(
|
||||
NotificationParams()
|
||||
)
|
||||
|
||||
val runLock = Mutex()
|
||||
var runTime: Long = 0L
|
||||
|
||||
var delegate: ServiceDelegate<IBaseService>? = null
|
||||
|
||||
var intent: Intent? = null
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
module core
|
||||
|
||||
go 1.20
|
||||
go 1.25
|
||||
|
||||
replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||
|
||||
|
||||
@@ -36,16 +36,25 @@ class Throttler {
|
||||
Function func, {
|
||||
List<dynamic>? args,
|
||||
Duration duration = const Duration(milliseconds: 600),
|
||||
bool fire = false,
|
||||
}) {
|
||||
final timer = _operations[tag];
|
||||
if (timer != null) {
|
||||
return true;
|
||||
}
|
||||
_operations[tag] = Timer(duration, () {
|
||||
_operations[tag]?.cancel();
|
||||
_operations.remove(tag);
|
||||
if (fire) {
|
||||
Function.apply(func, args);
|
||||
});
|
||||
_operations[tag] = Timer(duration, () {
|
||||
_operations[tag]?.cancel();
|
||||
_operations.remove(tag);
|
||||
});
|
||||
} else {
|
||||
_operations[tag] = Timer(duration, () {
|
||||
Function.apply(func, args);
|
||||
_operations[tag]?.cancel();
|
||||
_operations.remove(tag);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -134,11 +134,15 @@ class CommonPageTransition extends StatefulWidget {
|
||||
bool allowSnapshotting,
|
||||
Widget? child,
|
||||
) {
|
||||
final Animation<Offset> delegatedPositionAnimation = CurvedAnimation(
|
||||
final CurvedAnimation animation = CurvedAnimation(
|
||||
parent: secondaryAnimation,
|
||||
curve: Curves.linearToEaseOut,
|
||||
reverseCurve: Curves.easeInToLinear,
|
||||
).drive(_kMiddleLeftTween);
|
||||
);
|
||||
final Animation<Offset> delegatedPositionAnimation = animation.drive(
|
||||
_kMiddleLeftTween,
|
||||
);
|
||||
animation.dispose();
|
||||
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -12,14 +13,14 @@ class CommonPrint {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
void log(String? text) {
|
||||
void log(String? text, {LogLevel logLevel = LogLevel.info}) {
|
||||
final payload = '[APP] $text';
|
||||
debugPrint(payload);
|
||||
if (!globalState.isInit) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.addLog(
|
||||
Log.app(payload),
|
||||
Log.app(payload).copyWith(logLevel: logLevel),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,13 +73,13 @@ class Request {
|
||||
}
|
||||
|
||||
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
|
||||
'https://ipwho.is/': IpInfo.fromIpWhoIsJson,
|
||||
'https://api.myip.com/': IpInfo.fromMyIpJson,
|
||||
'https://ipapi.co/json/': IpInfo.fromIpApiCoJson,
|
||||
'https://ident.me/json/': IpInfo.fromIdentMeJson,
|
||||
'http://ip-api.com/json/': IpInfo.fromIpAPIJson,
|
||||
'https://api.ip.sb/geoip/': IpInfo.fromIpSbJson,
|
||||
'https://ipinfo.io/json/': IpInfo.fromIpInfoIoJson,
|
||||
'https://ipwho.is': IpInfo.fromIpWhoIsJson,
|
||||
'https://api.myip.com': IpInfo.fromMyIpJson,
|
||||
'https://ipapi.co/json': IpInfo.fromIpApiCoJson,
|
||||
'https://ident.me/json': IpInfo.fromIdentMeJson,
|
||||
'http://ip-api.com/json': IpInfo.fromIpAPIJson,
|
||||
'https://api.ip.sb/geoip': IpInfo.fromIpSbJson,
|
||||
'https://ipinfo.io/json': IpInfo.fromIpInfoIoJson,
|
||||
};
|
||||
|
||||
Future<Result<IpInfo?>> checkIp({CancelToken? cancelToken}) async {
|
||||
@@ -92,11 +92,13 @@ class Request {
|
||||
}
|
||||
}
|
||||
|
||||
final future = dio.get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(responseType: ResponseType.json),
|
||||
);
|
||||
final future = dio
|
||||
.get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(responseType: ResponseType.json),
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
future
|
||||
.then((res) {
|
||||
if (res.statusCode == HttpStatus.ok && res.data != null) {
|
||||
|
||||
@@ -11,16 +11,15 @@ extension StringExtension on String {
|
||||
}
|
||||
|
||||
dynamic get splitByMultipleSeparators {
|
||||
final parts =
|
||||
split(RegExp(r'[, ;]+')).where((part) => part.isNotEmpty).toList();
|
||||
final parts = split(
|
||||
RegExp(r'[, ;]+'),
|
||||
).where((part) => part.isNotEmpty).toList();
|
||||
|
||||
return parts.length > 1 ? parts : this;
|
||||
}
|
||||
|
||||
int compareToLower(String other) {
|
||||
return toLowerCase().compareTo(
|
||||
other.toLowerCase(),
|
||||
);
|
||||
return toLowerCase().compareTo(other.toLowerCase());
|
||||
}
|
||||
|
||||
List<int> get encodeUtf16LeWithBom {
|
||||
@@ -66,9 +65,9 @@ extension StringExtension on String {
|
||||
return md5.convert(bytes).toString();
|
||||
}
|
||||
|
||||
// bool containsToLower(String target) {
|
||||
// return toLowerCase().contains(target);
|
||||
// }
|
||||
// bool containsToLower(String target) {
|
||||
// return toLowerCase().contains(target);
|
||||
// }
|
||||
}
|
||||
|
||||
extension StringExtensionSafe on String? {
|
||||
|
||||
@@ -181,7 +181,10 @@ class Windows {
|
||||
calloc.free(argumentsPtr);
|
||||
calloc.free(operationPtr);
|
||||
|
||||
commonPrint.log('windows runas: $command $arguments resultCode:$result');
|
||||
commonPrint.log(
|
||||
'windows runas: $command $arguments resultCode:$result',
|
||||
logLevel: LogLevel.warning,
|
||||
);
|
||||
|
||||
if (result < 42) {
|
||||
return false;
|
||||
|
||||
@@ -353,7 +353,7 @@ class AppController {
|
||||
try {
|
||||
await updateProfile(profile);
|
||||
} catch (e) {
|
||||
commonPrint.log(e.toString());
|
||||
commonPrint.log(e.toString(), logLevel: LogLevel.warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -529,6 +529,7 @@ class AppController {
|
||||
FlutterError.onError = (details) {
|
||||
commonPrint.log(
|
||||
'exception: ${details.exception} stack: ${details.stack}',
|
||||
logLevel: LogLevel.warning,
|
||||
);
|
||||
};
|
||||
updateTray(true);
|
||||
@@ -551,7 +552,12 @@ class AppController {
|
||||
|
||||
Future<void> _connectCore() async {
|
||||
_ref.read(coreStatusProvider.notifier).value = CoreStatus.connecting;
|
||||
final message = await coreController.preload();
|
||||
final result = await Future.wait([
|
||||
coreController.preload(),
|
||||
if (!globalState.isService) Future.delayed(Duration(milliseconds: 300)),
|
||||
]);
|
||||
final String message = result[0];
|
||||
await Future.delayed(commonDuration);
|
||||
if (message.isNotEmpty) {
|
||||
_ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
|
||||
if (context.mounted) {
|
||||
@@ -956,7 +962,7 @@ class AppController {
|
||||
final res = await futureFunction();
|
||||
return res;
|
||||
} catch (e) {
|
||||
commonPrint.log('$futureFunction ===> $e');
|
||||
commonPrint.log('$futureFunction ===> $e', logLevel: LogLevel.warning);
|
||||
if (realSilence) {
|
||||
globalState.showNotifier(e.toString());
|
||||
} else {
|
||||
|
||||
@@ -52,7 +52,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
}
|
||||
},
|
||||
(error, stack) async {
|
||||
commonPrint.log('Service error: $error');
|
||||
commonPrint.log('Service error: $error', logLevel: LogLevel.warning);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -97,7 +97,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
_process?.stderr.listen((e) {
|
||||
final error = utf8.decode(e);
|
||||
if (error.isNotEmpty) {
|
||||
commonPrint.log(error);
|
||||
commonPrint.log(error, logLevel: LogLevel.warning);
|
||||
}
|
||||
});
|
||||
await _socketCompleter.future;
|
||||
|
||||
@@ -22,6 +22,7 @@ Future<void> main() async {
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _service(List<String> flags) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
globalState.isService = true;
|
||||
await globalState.init();
|
||||
await coreController.preload();
|
||||
tile?.addListener(
|
||||
|
||||
@@ -101,7 +101,7 @@ class MessageManagerState extends State<MessageManager> {
|
||||
_cancelMessage(messages.last.id);
|
||||
},
|
||||
child: Card(
|
||||
shape: const RoundedRectangleBorder(
|
||||
shape: const RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(14),
|
||||
),
|
||||
@@ -127,6 +127,7 @@ class MessageManagerState extends State<MessageManager> {
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
IconButton(
|
||||
padding: EdgeInsets.all(2),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () {
|
||||
_cancelMessage(messages.last.id);
|
||||
|
||||
@@ -87,13 +87,23 @@ class ThemeManager extends ConsumerWidget {
|
||||
top: padding.top > height * 0.3 ? 20.0 : padding.top,
|
||||
),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
globalState.appController.updateViewSize(
|
||||
Size(container.maxWidth, container.maxHeight),
|
||||
);
|
||||
return _buildSystemUi(child);
|
||||
},
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
floatingActionButtonTheme: Theme.of(context).floatingActionButtonTheme
|
||||
.copyWith(
|
||||
shape: const RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16.0)),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
globalState.appController.updateViewSize(
|
||||
Size(container.maxWidth, container.maxHeight),
|
||||
);
|
||||
return _buildSystemUi(child);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,11 +24,16 @@ class _VpnContainerState extends ConsumerState<VpnManager> {
|
||||
}
|
||||
|
||||
void showTip() {
|
||||
debouncer.call(FunctionTag.vpnTip, () {
|
||||
if (ref.read(isStartProvider)) {
|
||||
globalState.showNotifier(appLocalizations.vpnTip);
|
||||
}
|
||||
});
|
||||
throttler.call(
|
||||
FunctionTag.vpnTip,
|
||||
() {
|
||||
if (ref.read(isStartProvider)) {
|
||||
globalState.showNotifier(appLocalizations.vpnTip);
|
||||
}
|
||||
},
|
||||
duration: const Duration(seconds: 6),
|
||||
fire: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -271,9 +271,11 @@ class AppIcon extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
decoration: ShapeDecoration(
|
||||
color: context.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Transform.translate(
|
||||
|
||||
@@ -169,8 +169,8 @@ class ScannerOverlay extends CustomPainter {
|
||||
final backgroundPath = Path()..addRect(Rect.largest);
|
||||
|
||||
final cutoutPath = Path()
|
||||
..addRRect(
|
||||
RRect.fromRectAndCorners(
|
||||
..addRSuperellipse(
|
||||
RSuperellipse.fromRectAndCorners(
|
||||
scanWindow,
|
||||
topLeft: Radius.circular(borderRadius),
|
||||
topRight: Radius.circular(borderRadius),
|
||||
@@ -195,7 +195,7 @@ class ScannerOverlay extends CustomPainter {
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4.0;
|
||||
|
||||
final borderRect = RRect.fromRectAndCorners(
|
||||
final border = RSuperellipse.fromRectAndCorners(
|
||||
scanWindow,
|
||||
topLeft: Radius.circular(borderRadius),
|
||||
topRight: Radius.circular(borderRadius),
|
||||
@@ -204,7 +204,7 @@ class ScannerOverlay extends CustomPainter {
|
||||
);
|
||||
|
||||
canvas.drawPath(backgroundWithCutout, backgroundPaint);
|
||||
canvas.drawRRect(borderRect, borderPaint);
|
||||
canvas.drawRSuperellipse(border, borderPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -86,7 +86,11 @@ class Service {
|
||||
}
|
||||
|
||||
Future<String> init() async {
|
||||
return await methodChannel.invokeMethod<String>('init') ?? '';
|
||||
return await methodChannel.invokeMethod<String>(
|
||||
'init',
|
||||
!globalState.isService,
|
||||
) ??
|
||||
'';
|
||||
}
|
||||
|
||||
Future<bool> shutdown() async {
|
||||
|
||||
@@ -48,6 +48,7 @@ class GlobalState {
|
||||
AppController? _appController;
|
||||
bool isInit = false;
|
||||
bool isUserDisconnected = false;
|
||||
bool isService = false;
|
||||
|
||||
bool get isStart => startTime != null && startTime!.isBeforeNow;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class StartButton extends ConsumerStatefulWidget {
|
||||
|
||||
class _StartButtonState extends ConsumerState<StartButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
AnimationController? _controller;
|
||||
late Animation<double> _animation;
|
||||
bool isStart = false;
|
||||
|
||||
@@ -28,7 +28,7 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
parent: _controller!,
|
||||
curve: Curves.easeOutBack,
|
||||
);
|
||||
ref.listenManual(runTimeProvider.select((state) => state != null), (
|
||||
@@ -44,7 +44,8 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_controller?.dispose();
|
||||
_controller = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -59,9 +60,9 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
void updateController() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isStart && mounted) {
|
||||
_controller.forward();
|
||||
_controller?.forward();
|
||||
} else {
|
||||
_controller.reverse();
|
||||
_controller?.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -74,12 +75,13 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
}
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
sizeConstraints: BoxConstraints(minWidth: 56, maxWidth: 200),
|
||||
),
|
||||
floatingActionButtonTheme: Theme.of(context).floatingActionButtonTheme
|
||||
.copyWith(
|
||||
sizeConstraints: BoxConstraints(minWidth: 56, maxWidth: 200),
|
||||
),
|
||||
),
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
animation: _controller!.view,
|
||||
builder: (_, child) {
|
||||
final textWidth =
|
||||
globalState.measure
|
||||
|
||||
@@ -128,10 +128,11 @@ class TrafficUsage extends StatelessWidget {
|
||||
Container(
|
||||
width: 20,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
decoration: ShapeDecoration(
|
||||
color: primaryColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
3,
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -151,10 +152,11 @@ class TrafficUsage extends StatelessWidget {
|
||||
Container(
|
||||
width: 20,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
decoration: ShapeDecoration(
|
||||
color: secondaryColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
3,
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/core/controller.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/common.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
@@ -19,12 +18,14 @@ class DeveloperView extends ConsumerWidget {
|
||||
items: [
|
||||
ListItem(
|
||||
title: Text(appLocalizations.messageTest),
|
||||
minVerticalPadding: 14,
|
||||
onTap: () {
|
||||
context.showNotifier(appLocalizations.messageTestTip);
|
||||
},
|
||||
),
|
||||
ListItem(
|
||||
title: Text(appLocalizations.logsTest),
|
||||
minVerticalPadding: 14,
|
||||
onTap: () {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
globalState.appController.addLog(
|
||||
@@ -37,6 +38,7 @@ class DeveloperView extends ConsumerWidget {
|
||||
),
|
||||
ListItem(
|
||||
title: Text(appLocalizations.crashTest),
|
||||
minVerticalPadding: 14,
|
||||
onTap: () {
|
||||
if (kDebugMode) {
|
||||
coreController.crash();
|
||||
@@ -45,18 +47,20 @@ class DeveloperView extends ConsumerWidget {
|
||||
),
|
||||
ListItem(
|
||||
title: Text(appLocalizations.clearData),
|
||||
minVerticalPadding: 14,
|
||||
onTap: () async {
|
||||
await globalState.appController.handleClear();
|
||||
},
|
||||
),
|
||||
ListItem(
|
||||
title: Text('Loading'),
|
||||
onTap: () {
|
||||
ref.read(loadingProvider.notifier).value = !ref.read(
|
||||
loadingProvider,
|
||||
);
|
||||
},
|
||||
),
|
||||
// ListItem(
|
||||
// title: Text('Loading'),
|
||||
// minVerticalPadding: 14,
|
||||
// onTap: () {
|
||||
// ref.read(loadingProvider.notifier).value = !ref.read(
|
||||
// loadingProvider,
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ class DelayTestButton extends StatefulWidget {
|
||||
class _DelayTestButtonState extends State<DelayTestButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scale;
|
||||
late Animation<double> _animation;
|
||||
|
||||
Future<void> _healthcheck() async {
|
||||
if (_controller.isAnimating) {
|
||||
@@ -378,10 +378,10 @@ class _DelayTestButtonState extends State<DelayTestButton>
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
duration: const Duration(milliseconds: 400),
|
||||
);
|
||||
_scale = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: const Interval(0, 1)),
|
||||
_animation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOutBack),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,7 +399,10 @@ class _DelayTestButtonState extends State<DelayTestButton>
|
||||
return SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: Transform.scale(scale: _scale.value, child: child),
|
||||
child: FadeTransition(
|
||||
opacity: _animation,
|
||||
child: ScaleTransition(scale: _animation, child: child),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: FloatingActionButton(
|
||||
|
||||
@@ -177,7 +177,9 @@ class CommonCard extends StatelessWidget {
|
||||
style: ButtonStyle(
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(radius)),
|
||||
RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
),
|
||||
),
|
||||
iconColor: WidgetStatePropertyAll(context.colorScheme.primary),
|
||||
iconSize: WidgetStateProperty.all(20),
|
||||
|
||||
@@ -37,7 +37,7 @@ class ColorSchemeBox extends StatelessWidget {
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ClipRRect(
|
||||
child: ClipRSuperellipse(
|
||||
borderRadius: BorderRadius.circular(36),
|
||||
child: SizedBox(
|
||||
width: 72,
|
||||
@@ -47,22 +47,16 @@ class ColorSchemeBox extends StatelessWidget {
|
||||
children: [
|
||||
GridItem(
|
||||
mainAxisCellCount: 2,
|
||||
child: Container(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
child: Container(color: colorScheme.primary),
|
||||
),
|
||||
GridItem(
|
||||
mainAxisCellCount: 1,
|
||||
child: Container(
|
||||
color: colorScheme.secondary,
|
||||
),
|
||||
child: Container(color: colorScheme.secondary),
|
||||
),
|
||||
GridItem(
|
||||
mainAxisCellCount: 1,
|
||||
child: Container(
|
||||
color: colorScheme.tertiary,
|
||||
),
|
||||
)
|
||||
child: Container(color: colorScheme.tertiary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -73,11 +67,8 @@ class ColorSchemeBox extends StatelessWidget {
|
||||
const Positioned(
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
child: Icon(
|
||||
Icons.colorize,
|
||||
size: 20,
|
||||
),
|
||||
)
|
||||
child: Icon(Icons.colorize, size: 20),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -112,9 +103,7 @@ class PrimaryColorBox extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
return Theme(
|
||||
data: themeData.copyWith(
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
data: themeData.copyWith(colorScheme: colorScheme),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,30 +18,21 @@ class RadioDelegate<T> extends Delegate {
|
||||
final T value;
|
||||
final void Function()? onTab;
|
||||
|
||||
const RadioDelegate({
|
||||
required this.value,
|
||||
this.onTab,
|
||||
});
|
||||
const RadioDelegate({required this.value, this.onTab});
|
||||
}
|
||||
|
||||
class SwitchDelegate<T> extends Delegate {
|
||||
final bool value;
|
||||
final ValueChanged<bool>? onChanged;
|
||||
|
||||
const SwitchDelegate({
|
||||
required this.value,
|
||||
this.onChanged,
|
||||
});
|
||||
const SwitchDelegate({required this.value, this.onChanged});
|
||||
}
|
||||
|
||||
class CheckboxDelegate<T> extends Delegate {
|
||||
final bool value;
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
|
||||
const CheckboxDelegate({
|
||||
this.value = false,
|
||||
this.onChanged,
|
||||
});
|
||||
const CheckboxDelegate({this.value = false, this.onChanged});
|
||||
}
|
||||
|
||||
class OpenDelegate extends Delegate {
|
||||
@@ -129,6 +120,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
final double? horizontalTitleGap;
|
||||
final TextStyle? titleTextStyle;
|
||||
final TextStyle? subtitleTextStyle;
|
||||
final double minVerticalPadding;
|
||||
final void Function()? onTap;
|
||||
|
||||
const ListItem({
|
||||
@@ -143,6 +135,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.onTap,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : delegate = const Delegate();
|
||||
|
||||
@@ -158,6 +151,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
|
||||
@@ -173,6 +167,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
|
||||
@@ -188,6 +183,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
|
||||
@@ -203,6 +199,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : onTap = null;
|
||||
|
||||
@@ -217,9 +214,10 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : trailing = null,
|
||||
onTap = null;
|
||||
}) : trailing = null,
|
||||
onTap = null;
|
||||
|
||||
const ListItem.switchItem({
|
||||
super.key,
|
||||
@@ -232,9 +230,10 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : trailing = null,
|
||||
onTap = null;
|
||||
}) : trailing = null,
|
||||
onTap = null;
|
||||
|
||||
const ListItem.radio({
|
||||
super.key,
|
||||
@@ -247,9 +246,10 @@ class ListItem<T> extends StatelessWidget {
|
||||
this.dense,
|
||||
this.titleTextStyle,
|
||||
this.subtitleTextStyle,
|
||||
this.minVerticalPadding = 12,
|
||||
this.tileTitleAlignment = ListTileTitleAlignment.center,
|
||||
}) : leading = null,
|
||||
onTap = null;
|
||||
}) : leading = null,
|
||||
onTap = null;
|
||||
|
||||
Widget _buildListTile({
|
||||
void Function()? onTap,
|
||||
@@ -264,7 +264,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
leading: leading ?? this.leading,
|
||||
horizontalTitleGap: horizontalTitleGap,
|
||||
title: title,
|
||||
minVerticalPadding: 12,
|
||||
minVerticalPadding: minVerticalPadding,
|
||||
subtitle: subtitle,
|
||||
titleAlignment: tileTitleAlignment,
|
||||
onTap: onTap,
|
||||
@@ -282,7 +282,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
closedBuilder: (_, action) {
|
||||
openAction() {
|
||||
final isMobile = globalState.appState.viewMode == ViewMode.mobile;
|
||||
if (!isMobile || system.isDesktop) {
|
||||
if (!isMobile) {
|
||||
showExtend(
|
||||
context,
|
||||
props: ExtendProps(
|
||||
@@ -306,9 +306,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
action();
|
||||
}
|
||||
|
||||
return _buildListTile(
|
||||
onTap: openAction,
|
||||
);
|
||||
return _buildListTile(onTap: openAction);
|
||||
},
|
||||
openBuilder: (_, action) {
|
||||
return openDelegate.wrap
|
||||
@@ -422,9 +420,7 @@ class ListItem<T> extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return _buildListTile(
|
||||
onTap: onTap,
|
||||
);
|
||||
return _buildListTile(onTap: onTap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,13 +444,9 @@ class ListHeader extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: padding ??
|
||||
const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 8,
|
||||
top: 24,
|
||||
bottom: 8,
|
||||
),
|
||||
padding:
|
||||
padding ??
|
||||
const EdgeInsets.only(left: 16, right: 8, top: 24, bottom: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -466,19 +458,18 @@ class ListHeader extends StatelessWidget {
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant
|
||||
.opacity80,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.opacity80,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (subTitle != null)
|
||||
Text(
|
||||
subTitle!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -486,12 +477,7 @@ class ListHeader extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
...genActions(
|
||||
actions,
|
||||
space: space,
|
||||
),
|
||||
],
|
||||
children: [...genActions(actions, space: space)],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -506,18 +492,11 @@ List<Widget> generateSection({
|
||||
bool separated = true,
|
||||
}) {
|
||||
final genItems = separated
|
||||
? items.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
? items.separated(const Divider(height: 0))
|
||||
: items;
|
||||
return [
|
||||
if (items.isNotEmpty && title != null)
|
||||
ListHeader(
|
||||
title: title,
|
||||
actions: actions,
|
||||
),
|
||||
ListHeader(title: title, actions: actions),
|
||||
...genItems,
|
||||
];
|
||||
}
|
||||
@@ -528,22 +507,26 @@ Widget generateSectionV2({
|
||||
List<Widget>? actions,
|
||||
bool separated = true,
|
||||
}) {
|
||||
final genItems = items
|
||||
.map<Widget>((item) {
|
||||
return ClipRSuperellipse(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CommonCard(
|
||||
type: CommonCardType.filled,
|
||||
radius: 0,
|
||||
child: item,
|
||||
),
|
||||
);
|
||||
})
|
||||
.separated(const Divider(height: 2, color: Colors.transparent));
|
||||
return Column(
|
||||
children: [
|
||||
if (items.isNotEmpty && title != null)
|
||||
ListHeader(
|
||||
title: title,
|
||||
actions: actions,
|
||||
),
|
||||
CommonCard(
|
||||
radius: 18,
|
||||
type: CommonCardType.filled,
|
||||
child: Column(
|
||||
children: [
|
||||
...items,
|
||||
],
|
||||
),
|
||||
)
|
||||
ListHeader(title: title, actions: actions),
|
||||
ClipRSuperellipse(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Column(children: [...genItems]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -555,18 +538,10 @@ List<Widget> generateInfoSection({
|
||||
bool separated = true,
|
||||
}) {
|
||||
final genItems = separated
|
||||
? items.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
? items.separated(const Divider(height: 0))
|
||||
: items;
|
||||
return [
|
||||
if (items.isNotEmpty)
|
||||
InfoHeader(
|
||||
info: info,
|
||||
actions: actions,
|
||||
),
|
||||
if (items.isNotEmpty) InfoHeader(info: info, actions: actions),
|
||||
...genItems,
|
||||
];
|
||||
}
|
||||
@@ -575,8 +550,6 @@ Widget generateListView(List<Widget> items) {
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, index) => items[index],
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 16,
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,19 +2,15 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
typedef CloseContainerActionCallback<S> = void Function({S? returnValue});
|
||||
typedef OpenContainerBuilder<S> = Widget Function(
|
||||
BuildContext context,
|
||||
CloseContainerActionCallback<S> action,
|
||||
);
|
||||
typedef CloseContainerBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
VoidCallback action,
|
||||
);
|
||||
typedef OpenContainerBuilder<S> =
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
CloseContainerActionCallback<S> action,
|
||||
);
|
||||
typedef CloseContainerBuilder =
|
||||
Widget Function(BuildContext context, VoidCallback action);
|
||||
|
||||
enum ContainerTransitionType {
|
||||
fade,
|
||||
fadeThrough,
|
||||
}
|
||||
enum ContainerTransitionType { fade, fadeThrough }
|
||||
|
||||
typedef ClosedCallback<S> = void Function(S data);
|
||||
|
||||
@@ -56,20 +52,23 @@ class _OpenContainerState<T> extends State<OpenContainer<T?>> {
|
||||
Future<void> openContainer() async {
|
||||
final Color middleColor =
|
||||
widget.middleColor ?? Theme.of(context).canvasColor;
|
||||
final T? data = await Navigator.of(
|
||||
context,
|
||||
rootNavigator: widget.useRootNavigator,
|
||||
).push(_OpenContainerRoute<T>(
|
||||
middleColor: middleColor,
|
||||
closedBuilder: widget.closedBuilder,
|
||||
openBuilder: widget.openBuilder,
|
||||
hideableKey: _hideableKey,
|
||||
closedBuilderKey: _closedBuilderKey,
|
||||
transitionDuration: widget.transitionDuration,
|
||||
transitionType: widget.transitionType,
|
||||
useRootNavigator: widget.useRootNavigator,
|
||||
routeSettings: widget.routeSettings,
|
||||
));
|
||||
final T? data =
|
||||
await Navigator.of(
|
||||
context,
|
||||
rootNavigator: widget.useRootNavigator,
|
||||
).push(
|
||||
_OpenContainerRoute<T>(
|
||||
middleColor: middleColor,
|
||||
closedBuilder: widget.closedBuilder,
|
||||
openBuilder: widget.openBuilder,
|
||||
hideableKey: _hideableKey,
|
||||
closedBuilderKey: _closedBuilderKey,
|
||||
transitionDuration: widget.transitionDuration,
|
||||
transitionType: widget.transitionType,
|
||||
useRootNavigator: widget.useRootNavigator,
|
||||
routeSettings: widget.routeSettings,
|
||||
),
|
||||
);
|
||||
if (widget.onClosed != null) {
|
||||
widget.onClosed!(data);
|
||||
}
|
||||
@@ -97,10 +96,7 @@ class _OpenContainerState<T> extends State<OpenContainer<T?>> {
|
||||
}
|
||||
|
||||
class _Hideable extends StatefulWidget {
|
||||
const _Hideable({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
const _Hideable({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@@ -161,9 +157,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
required this.transitionType,
|
||||
required this.useRootNavigator,
|
||||
required RouteSettings? routeSettings,
|
||||
}) : _closedOpacityTween = _getClosedOpacityTween(transitionType),
|
||||
_openOpacityTween = _getOpenOpacityTween(transitionType),
|
||||
super(settings: routeSettings);
|
||||
}) : _closedOpacityTween = _getClosedOpacityTween(transitionType),
|
||||
_openOpacityTween = _getOpenOpacityTween(transitionType),
|
||||
super(settings: routeSettings);
|
||||
|
||||
static _FlippableTweenSequence<Color?> _getColorTween({
|
||||
required ContainerTransitionType transitionType,
|
||||
@@ -173,99 +169,89 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
}) {
|
||||
switch (transitionType) {
|
||||
case ContainerTransitionType.fade:
|
||||
return _FlippableTweenSequence<Color?>(
|
||||
<TweenSequenceItem<Color?>>[
|
||||
TweenSequenceItem<Color>(
|
||||
tween: ConstantTween<Color>(closedColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: closedColor, end: openColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color>(
|
||||
tween: ConstantTween<Color>(openColor),
|
||||
weight: 3 / 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<Color?>(<TweenSequenceItem<Color?>>[
|
||||
TweenSequenceItem<Color>(
|
||||
tween: ConstantTween<Color>(closedColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: closedColor, end: openColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color>(
|
||||
tween: ConstantTween<Color>(openColor),
|
||||
weight: 3 / 5,
|
||||
),
|
||||
]);
|
||||
case ContainerTransitionType.fadeThrough:
|
||||
return _FlippableTweenSequence<Color?>(
|
||||
<TweenSequenceItem<Color?>>[
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: closedColor, end: middleColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: middleColor, end: openColor),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<Color?>(<TweenSequenceItem<Color?>>[
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: closedColor, end: middleColor),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<Color?>(
|
||||
tween: ColorTween(begin: middleColor, end: openColor),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
static _FlippableTweenSequence<double> _getClosedOpacityTween(
|
||||
ContainerTransitionType transitionType) {
|
||||
ContainerTransitionType transitionType,
|
||||
) {
|
||||
switch (transitionType) {
|
||||
case ContainerTransitionType.fade:
|
||||
return _FlippableTweenSequence<double>(
|
||||
<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(1.0),
|
||||
weight: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(1.0),
|
||||
weight: 1,
|
||||
),
|
||||
]);
|
||||
case ContainerTransitionType.fadeThrough:
|
||||
return _FlippableTweenSequence<double>(
|
||||
<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 1.0, end: 0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 1.0, end: 0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
static _FlippableTweenSequence<double> _getOpenOpacityTween(
|
||||
ContainerTransitionType transitionType) {
|
||||
ContainerTransitionType transitionType,
|
||||
) {
|
||||
switch (transitionType) {
|
||||
case ContainerTransitionType.fade:
|
||||
return _FlippableTweenSequence<double>(
|
||||
<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(1.0),
|
||||
weight: 3 / 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(1.0),
|
||||
weight: 3 / 5,
|
||||
),
|
||||
]);
|
||||
case ContainerTransitionType.fadeThrough:
|
||||
return _FlippableTweenSequence<double>(
|
||||
<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _FlippableTweenSequence<double>(<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(0.0),
|
||||
weight: 1 / 5,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
weight: 4 / 5,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,8 +311,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
@override
|
||||
void dispose() {
|
||||
if (hideableKey.currentState?.isVisible == false) {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((Duration d) => _toggleHideable(hide: false));
|
||||
SchedulerBinding.instance.addPostFrameCallback(
|
||||
(Duration d) => _toggleHideable(hide: false),
|
||||
);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
@@ -343,10 +330,12 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
required BuildContext navigatorContext,
|
||||
bool delayForSourceRoute = false,
|
||||
}) {
|
||||
final RenderBox navigator = Navigator.of(
|
||||
navigatorContext,
|
||||
rootNavigator: useRootNavigator,
|
||||
).context.findRenderObject()! as RenderBox;
|
||||
final RenderBox navigator =
|
||||
Navigator.of(
|
||||
navigatorContext,
|
||||
rootNavigator: useRootNavigator,
|
||||
).context.findRenderObject()!
|
||||
as RenderBox;
|
||||
final Size navSize = _getSize(navigator);
|
||||
_rectTween.end = Offset.zero & navSize;
|
||||
|
||||
@@ -359,8 +348,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
}
|
||||
|
||||
if (delayForSourceRoute) {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback(takeMeasurementsInSourceRoute);
|
||||
SchedulerBinding.instance.addPostFrameCallback(
|
||||
takeMeasurementsInSourceRoute,
|
||||
);
|
||||
} else {
|
||||
takeMeasurementsInSourceRoute();
|
||||
}
|
||||
@@ -451,8 +441,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
final Animation<double> curvedAnimation = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
reverseCurve:
|
||||
_transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped,
|
||||
reverseCurve: _transitionWasInterrupted
|
||||
? null
|
||||
: Curves.fastOutSlowIn.flipped,
|
||||
);
|
||||
TweenSequence<Color?>? colorTween;
|
||||
TweenSequence<double>? closedOpacityTween, openOpacityTween;
|
||||
@@ -508,8 +499,9 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
child: (hideableKey.currentState?.isInTree ?? false)
|
||||
? null
|
||||
: FadeTransition(
|
||||
opacity:
|
||||
closedOpacityTween!.animate(animation),
|
||||
opacity: closedOpacityTween!.animate(
|
||||
animation,
|
||||
),
|
||||
child: Builder(
|
||||
key: closedBuilderKey,
|
||||
builder: (BuildContext context) {
|
||||
@@ -521,22 +513,18 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Open child fading in.
|
||||
FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
OverflowBox(
|
||||
maxWidth: _rectTween.end!.width,
|
||||
maxHeight: _rectTween.end!.height,
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: _rectTween.end!.width,
|
||||
height: _rectTween.end!.height,
|
||||
child: FadeTransition(
|
||||
opacity: openOpacityTween!.animate(animation),
|
||||
child: Builder(
|
||||
key: _openBuilderKey,
|
||||
builder: (BuildContext context) {
|
||||
return openBuilder(context, closeContainer);
|
||||
},
|
||||
),
|
||||
child: FadeTransition(
|
||||
opacity: openOpacityTween!.animate(animation),
|
||||
child: Builder(
|
||||
key: _openBuilderKey,
|
||||
builder: (BuildContext context) {
|
||||
return openBuilder(context, closeContainer);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -578,10 +566,12 @@ class _FlippableTweenSequence<T> extends TweenSequence<T> {
|
||||
if (_flipped == null) {
|
||||
final List<TweenSequenceItem<T>> newItems = <TweenSequenceItem<T>>[];
|
||||
for (int i = 0; i < _items.length; i++) {
|
||||
newItems.add(TweenSequenceItem<T>(
|
||||
tween: _items[i].tween,
|
||||
weight: _items[_items.length - 1 - i].weight,
|
||||
));
|
||||
newItems.add(
|
||||
TweenSequenceItem<T>(
|
||||
tween: _items[i].tween,
|
||||
weight: _items[_items.length - 1 - i].weight,
|
||||
),
|
||||
);
|
||||
}
|
||||
_flipped = _FlippableTweenSequence<T>(newItems);
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ class _ShadePainter extends CustomPainter {
|
||||
effectiveSquareRadius * 2,
|
||||
effectiveSquareRadius * 2,
|
||||
);
|
||||
final RRect rRect = RRect.fromRectAndRadius(
|
||||
final RSuperellipse rSuperellipse = RSuperellipse.fromRectAndRadius(
|
||||
rectBox,
|
||||
Radius.circular(trackBorderRadius),
|
||||
);
|
||||
@@ -254,8 +254,8 @@ class _ShadePainter extends CustomPainter {
|
||||
HSVColor.fromAHSV(1, colorHue, 1, 1).toColor(),
|
||||
],
|
||||
).createShader(rectBox);
|
||||
canvas.drawRRect(
|
||||
rRect,
|
||||
canvas.drawRSuperellipse(
|
||||
rSuperellipse,
|
||||
Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..shader = horizontal,
|
||||
@@ -266,8 +266,8 @@ class _ShadePainter extends CustomPainter {
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[Colors.transparent, Colors.black],
|
||||
).createShader(rectBox);
|
||||
canvas.drawRRect(
|
||||
rRect,
|
||||
canvas.drawRSuperellipse(
|
||||
rSuperellipse,
|
||||
Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..shader = vertical,
|
||||
|
||||
@@ -38,10 +38,9 @@ class CommonPopupRoute<T> extends PopupRoute<T> {
|
||||
Widget child,
|
||||
) {
|
||||
final align = Alignment.topRight;
|
||||
final animationValue = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeIn,
|
||||
).value;
|
||||
final curveAnimation = animation
|
||||
.drive(Tween(begin: 0.0, end: 1.0))
|
||||
.drive(CurveTween(curve: Curves.easeOutBack));
|
||||
return SafeArea(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: offsetNotifier,
|
||||
@@ -58,15 +57,17 @@ class CommonPopupRoute<T> extends PopupRoute<T> {
|
||||
},
|
||||
child: AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (_, Widget? child) {
|
||||
return Opacity(
|
||||
opacity: 0.1 + 0.9 * animationValue,
|
||||
child: Transform.scale(
|
||||
builder: (_, child) {
|
||||
return FadeTransition(
|
||||
opacity: curveAnimation,
|
||||
child: ScaleTransition(
|
||||
alignment: align,
|
||||
scale: 0.7 + 0.3 * animationValue,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, -10) * (1 - animationValue),
|
||||
child: child!,
|
||||
scale: curveAnimation,
|
||||
child: SlideTransition(
|
||||
position: curveAnimation.drive(
|
||||
Tween(begin: const Offset(0, -0.02), end: Offset.zero),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -78,7 +79,7 @@ class CommonPopupRoute<T> extends PopupRoute<T> {
|
||||
}
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 150);
|
||||
Duration get transitionDuration => const Duration(milliseconds: 250);
|
||||
}
|
||||
|
||||
class PopupController extends ValueNotifier<bool> {
|
||||
@@ -270,8 +271,8 @@ class CommonPopupMenu extends StatelessWidget {
|
||||
elevation: 12,
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@@ -18,7 +18,6 @@ class CommonScaffold extends StatefulWidget {
|
||||
final Widget body;
|
||||
final Color? backgroundColor;
|
||||
final String? title;
|
||||
final Widget? leading;
|
||||
final List<Widget>? actions;
|
||||
final bool? centerTitle;
|
||||
final Widget? floatingActionButton;
|
||||
@@ -31,7 +30,6 @@ class CommonScaffold extends StatefulWidget {
|
||||
this.appBar,
|
||||
required this.body,
|
||||
this.backgroundColor,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.actions,
|
||||
this.centerTitle,
|
||||
@@ -163,7 +161,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
_keywordsNotifier.value = keywords;
|
||||
}
|
||||
|
||||
Widget? _buildLeading() {
|
||||
Widget? _buildLeading(VoidCallback? backAction) {
|
||||
if (_isEdit) {
|
||||
return IconButton(
|
||||
onPressed: _appBarState.value.editState?.onExit,
|
||||
@@ -176,7 +174,16 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
icon: Icon(Icons.arrow_back),
|
||||
);
|
||||
}
|
||||
return widget.leading;
|
||||
return backAction != null
|
||||
? BackButton(
|
||||
onPressed: () {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
backAction();
|
||||
},
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
Widget _buildTitle(AppBarSearchState? startState) {
|
||||
@@ -251,7 +258,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
PreferredSizeWidget _buildAppBar(VoidCallback? backAction) {
|
||||
return PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: Stack(
|
||||
@@ -263,8 +270,11 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
builder: (_, state, _) {
|
||||
return _buildAppBarWrap(
|
||||
AppBar(
|
||||
automaticallyImplyLeading: backAction != null
|
||||
? false
|
||||
: true,
|
||||
centerTitle: widget.centerTitle ?? false,
|
||||
leading: _buildLeading(),
|
||||
leading: _buildLeading(backAction),
|
||||
title: _buildTitle(state.searchState),
|
||||
actions: _buildActions(
|
||||
state.searchState != null,
|
||||
@@ -285,6 +295,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(widget.appBar != null || widget.title != null);
|
||||
final backActionProvider = CommonScaffoldBackActionProvider.of(context);
|
||||
final body = SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -327,7 +338,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: _buildAppBar(),
|
||||
appBar: _buildAppBar(backActionProvider?.backAction),
|
||||
body: body,
|
||||
resizeToAvoidBottomInset: true,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
@@ -349,3 +360,23 @@ List<Widget> genActions(List<Widget> actions, {double? space}) {
|
||||
SizedBox(width: 8),
|
||||
];
|
||||
}
|
||||
|
||||
class CommonScaffoldBackActionProvider extends InheritedWidget {
|
||||
final VoidCallback? backAction;
|
||||
|
||||
const CommonScaffoldBackActionProvider({
|
||||
super.key,
|
||||
required this.backAction,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static CommonScaffoldBackActionProvider? of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<CommonScaffoldBackActionProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(CommonScaffoldBackActionProvider oldWidget) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,7 @@ class ExtendProps {
|
||||
});
|
||||
}
|
||||
|
||||
enum SheetType {
|
||||
page,
|
||||
bottomSheet,
|
||||
sideSheet,
|
||||
}
|
||||
enum SheetType { page, bottomSheet, sideSheet }
|
||||
|
||||
typedef SheetBuilder = Widget Function(BuildContext context, SheetType type);
|
||||
|
||||
@@ -55,28 +51,24 @@ Future<T?> showSheet<T>({
|
||||
final isMobile = globalState.appState.viewMode == ViewMode.mobile;
|
||||
return switch (isMobile) {
|
||||
true => showModalBottomSheet<T>(
|
||||
context: context,
|
||||
isScrollControlled: props.isScrollControlled,
|
||||
builder: (_) {
|
||||
return SafeArea(
|
||||
child: builder(context, SheetType.bottomSheet),
|
||||
);
|
||||
},
|
||||
showDragHandle: false,
|
||||
useSafeArea: props.useSafeArea,
|
||||
),
|
||||
context: context,
|
||||
isScrollControlled: props.isScrollControlled,
|
||||
builder: (_) {
|
||||
return SafeArea(child: builder(context, SheetType.bottomSheet));
|
||||
},
|
||||
showDragHandle: false,
|
||||
useSafeArea: props.useSafeArea,
|
||||
),
|
||||
false => showModalSideSheet<T>(
|
||||
useSafeArea: props.useSafeArea,
|
||||
isScrollControlled: props.isScrollControlled,
|
||||
context: context,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: props.maxWidth ?? 360,
|
||||
),
|
||||
filter: props.blur ? commonFilter : null,
|
||||
builder: (_) {
|
||||
return builder(context, SheetType.sideSheet);
|
||||
},
|
||||
),
|
||||
useSafeArea: props.useSafeArea,
|
||||
isScrollControlled: props.isScrollControlled,
|
||||
context: context,
|
||||
constraints: BoxConstraints(maxWidth: props.maxWidth ?? 360),
|
||||
filter: props.blur ? commonFilter : null,
|
||||
builder: (_) {
|
||||
return builder(context, SheetType.sideSheet);
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,21 +79,16 @@ Future<T?> showExtend<T>(
|
||||
}) {
|
||||
final isMobile = globalState.appState.viewMode == ViewMode.mobile;
|
||||
return switch (isMobile || props.forceFull) {
|
||||
true => BaseNavigator.push(
|
||||
context,
|
||||
builder(context, SheetType.page),
|
||||
),
|
||||
true => BaseNavigator.push(context, builder(context, SheetType.page)),
|
||||
false => showModalSideSheet<T>(
|
||||
useSafeArea: props.useSafeArea,
|
||||
context: context,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: props.maxWidth ?? 360,
|
||||
),
|
||||
filter: props.blur ? commonFilter : null,
|
||||
builder: (context) {
|
||||
return builder(context, SheetType.sideSheet);
|
||||
},
|
||||
),
|
||||
useSafeArea: props.useSafeArea,
|
||||
context: context,
|
||||
constraints: BoxConstraints(maxWidth: props.maxWidth ?? 360),
|
||||
filter: props.blur ? commonFilter : null,
|
||||
builder: (context) {
|
||||
return builder(context, SheetType.sideSheet);
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -134,13 +121,11 @@ class _AdaptiveSheetScaffoldState extends State<AdaptiveSheetScaffold> {
|
||||
automaticallyImplyLeading: bottomSheet
|
||||
? false
|
||||
: widget.actions.isEmpty && sideSheet
|
||||
? false
|
||||
: true,
|
||||
? false
|
||||
: true,
|
||||
centerTitle: bottomSheet,
|
||||
backgroundColor: backgroundColor,
|
||||
title: Text(
|
||||
widget.title,
|
||||
),
|
||||
title: Text(widget.title),
|
||||
actions: genActions([
|
||||
if (widget.actions.isEmpty && sideSheet) CloseButton(),
|
||||
...widget.actions,
|
||||
@@ -150,9 +135,11 @@ class _AdaptiveSheetScaffoldState extends State<AdaptiveSheetScaffold> {
|
||||
final handleSize = Size(32, 4);
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
decoration: ShapeDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28.0)),
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28.0)),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -163,17 +150,16 @@ class _AdaptiveSheetScaffoldState extends State<AdaptiveSheetScaffold> {
|
||||
alignment: Alignment.center,
|
||||
height: handleSize.height,
|
||||
width: handleSize.width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(handleSize.height / 2),
|
||||
decoration: ShapeDecoration(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(handleSize.height / 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
appBar,
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: widget.body,
|
||||
)
|
||||
Flexible(flex: 1, child: widget.body),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -82,12 +82,12 @@ class _SideSheetState extends State<SideSheet> {
|
||||
final Color surfaceTintColor = colorScheme.surfaceTint;
|
||||
final Color shadowColor = widget.shadowColor ?? Colors.transparent;
|
||||
final double elevation = widget.elevation ?? 0;
|
||||
final ShapeBorder shape = widget.shape ??
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(0),
|
||||
);
|
||||
final ShapeBorder shape =
|
||||
widget.shape ??
|
||||
RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(0));
|
||||
|
||||
final BoxConstraints constraints = widget.constraints ??
|
||||
final BoxConstraints constraints =
|
||||
widget.constraints ??
|
||||
const BoxConstraints(maxWidth: 320, minWidth: 320);
|
||||
|
||||
final Clip clipBehavior = widget.clipBehavior ?? Clip.none;
|
||||
@@ -103,10 +103,7 @@ class _SideSheetState extends State<SideSheet> {
|
||||
child: widget.builder(context),
|
||||
);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: constraints,
|
||||
child: sideSheet,
|
||||
);
|
||||
return ConstrainedBox(constraints: constraints, child: sideSheet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +125,8 @@ class _SideSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget {
|
||||
|
||||
@override
|
||||
_RenderSideSheetLayoutWithSizeListener createRenderObject(
|
||||
BuildContext context) {
|
||||
BuildContext context,
|
||||
) {
|
||||
return _RenderSideSheetLayoutWithSizeListener(
|
||||
onChildSizeChanged: onChildSizeChanged,
|
||||
animationValue: animationValue,
|
||||
@@ -138,8 +136,10 @@ class _SideSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context,
|
||||
_RenderSideSheetLayoutWithSizeListener renderObject) {
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
_RenderSideSheetLayoutWithSizeListener renderObject,
|
||||
) {
|
||||
renderObject.onChildSizeChanged = onChildSizeChanged;
|
||||
renderObject.animationValue = animationValue;
|
||||
renderObject.isScrollControlled = isScrollControlled;
|
||||
@@ -155,12 +155,12 @@ class _RenderSideSheetLayoutWithSizeListener extends RenderShiftedBox {
|
||||
required double animationValue,
|
||||
required bool isScrollControlled,
|
||||
required double scrollControlDisabledMaxHeightRatio,
|
||||
}) : _onChildSizeChanged = onChildSizeChanged,
|
||||
_animationValue = animationValue,
|
||||
_isScrollControlled = isScrollControlled,
|
||||
_scrollControlDisabledMaxHeightRatio =
|
||||
scrollControlDisabledMaxHeightRatio,
|
||||
super(child);
|
||||
}) : _onChildSizeChanged = onChildSizeChanged,
|
||||
_animationValue = animationValue,
|
||||
_isScrollControlled = isScrollControlled,
|
||||
_scrollControlDisabledMaxHeightRatio =
|
||||
scrollControlDisabledMaxHeightRatio,
|
||||
super(child);
|
||||
|
||||
Size _lastSize = Size.zero;
|
||||
|
||||
@@ -219,8 +219,9 @@ class _RenderSideSheetLayoutWithSizeListener extends RenderShiftedBox {
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
final double width =
|
||||
_getSize(BoxConstraints.tightForFinite(height: height)).width;
|
||||
final double width = _getSize(
|
||||
BoxConstraints.tightForFinite(height: height),
|
||||
).width;
|
||||
if (width.isFinite) {
|
||||
return width;
|
||||
}
|
||||
@@ -229,8 +230,9 @@ class _RenderSideSheetLayoutWithSizeListener extends RenderShiftedBox {
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
final double width =
|
||||
_getSize(BoxConstraints.tightForFinite(height: height)).width;
|
||||
final double width = _getSize(
|
||||
BoxConstraints.tightForFinite(height: height),
|
||||
).width;
|
||||
if (width.isFinite) {
|
||||
return width;
|
||||
}
|
||||
@@ -239,8 +241,9 @@ class _RenderSideSheetLayoutWithSizeListener extends RenderShiftedBox {
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
final double height =
|
||||
_getSize(BoxConstraints.tightForFinite(width: width)).height;
|
||||
final double height = _getSize(
|
||||
BoxConstraints.tightForFinite(width: width),
|
||||
).height;
|
||||
if (height.isFinite) {
|
||||
return height;
|
||||
}
|
||||
@@ -249,8 +252,9 @@ class _RenderSideSheetLayoutWithSizeListener extends RenderShiftedBox {
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
final double height =
|
||||
_getSize(BoxConstraints.tightForFinite(width: width)).height;
|
||||
final double height = _getSize(
|
||||
BoxConstraints.tightForFinite(width: width),
|
||||
).height;
|
||||
if (height.isFinite) {
|
||||
return height;
|
||||
}
|
||||
@@ -263,9 +267,7 @@ class _RenderSideSheetLayoutWithSizeListener extends RenderShiftedBox {
|
||||
}
|
||||
|
||||
BoxConstraints _getConstraintsForChild(BoxConstraints constraints) {
|
||||
return BoxConstraints(
|
||||
maxHeight: constraints.maxHeight,
|
||||
);
|
||||
return BoxConstraints(maxHeight: constraints.maxHeight);
|
||||
}
|
||||
|
||||
Offset _getPositionForChild(Size size, Size childSize) {
|
||||
@@ -276,8 +278,9 @@ class _RenderSideSheetLayoutWithSizeListener extends RenderShiftedBox {
|
||||
void performLayout() {
|
||||
size = _getSize(constraints);
|
||||
if (child != null) {
|
||||
final BoxConstraints childConstraints =
|
||||
_getConstraintsForChild(constraints);
|
||||
final BoxConstraints childConstraints = _getConstraintsForChild(
|
||||
constraints,
|
||||
);
|
||||
assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true));
|
||||
child!.layout(
|
||||
childConstraints,
|
||||
@@ -288,8 +291,9 @@ class _RenderSideSheetLayoutWithSizeListener extends RenderShiftedBox {
|
||||
size,
|
||||
childConstraints.isTight ? childConstraints.smallest : child!.size,
|
||||
);
|
||||
final Size childSize =
|
||||
childConstraints.isTight ? childConstraints.smallest : child!.size;
|
||||
final Size childSize = childConstraints.isTight
|
||||
? childConstraints.smallest
|
||||
: child!.size;
|
||||
|
||||
if (_lastSize != childSize) {
|
||||
_lastSize = childSize;
|
||||
@@ -354,8 +358,9 @@ class _ModalSideSheetState<T> extends State<_ModalSideSheet<T>> {
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
final MaterialLocalizations localizations =
|
||||
MaterialLocalizations.of(context);
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(
|
||||
context,
|
||||
);
|
||||
final String routeLabel = _getRouteLabel(localizations);
|
||||
|
||||
return AnimatedBuilder(
|
||||
@@ -406,26 +411,27 @@ class _ModalSideSheetState<T> extends State<_ModalSideSheet<T>> {
|
||||
}
|
||||
|
||||
class ModalSideSheetRoute<T> extends PopupRoute<T> {
|
||||
ModalSideSheetRoute(
|
||||
{required this.builder,
|
||||
this.capturedThemes,
|
||||
this.barrierLabel,
|
||||
this.barrierOnTapHint,
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.shape,
|
||||
this.clipBehavior,
|
||||
this.constraints,
|
||||
this.modalBarrierColor,
|
||||
this.isDismissible = true,
|
||||
this.isScrollControlled = false,
|
||||
this.scrollControlDisabledMaxHeightRatio =
|
||||
_defaultScrollControlDisabledMaxHeightRatio,
|
||||
super.settings,
|
||||
this.transitionAnimationController,
|
||||
this.anchorPoint,
|
||||
this.useSafeArea = false,
|
||||
super.filter});
|
||||
ModalSideSheetRoute({
|
||||
required this.builder,
|
||||
this.capturedThemes,
|
||||
this.barrierLabel,
|
||||
this.barrierOnTapHint,
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.shape,
|
||||
this.clipBehavior,
|
||||
this.constraints,
|
||||
this.modalBarrierColor,
|
||||
this.isDismissible = true,
|
||||
this.isScrollControlled = false,
|
||||
this.scrollControlDisabledMaxHeightRatio =
|
||||
_defaultScrollControlDisabledMaxHeightRatio,
|
||||
super.settings,
|
||||
this.transitionAnimationController,
|
||||
this.anchorPoint,
|
||||
this.useSafeArea = false,
|
||||
super.filter,
|
||||
});
|
||||
|
||||
final WidgetBuilder builder;
|
||||
|
||||
@@ -504,8 +510,11 @@ class ModalSideSheetRoute<T> extends PopupRoute<T> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||
Animation<double> secondaryAnimation) {
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
final Widget content = DisplayFeatureSubScreen(
|
||||
anchorPoint: anchorPoint,
|
||||
child: Builder(
|
||||
@@ -539,9 +548,7 @@ class ModalSideSheetRoute<T> extends PopupRoute<T> {
|
||||
ColorTween(
|
||||
begin: barrierColor.opacity0,
|
||||
end: barrierColor,
|
||||
).chain(
|
||||
CurveTween(curve: barrierCurve),
|
||||
),
|
||||
).chain(CurveTween(curve: barrierCurve)),
|
||||
);
|
||||
return AnimatedModalBarrier(
|
||||
color: color,
|
||||
@@ -587,32 +594,39 @@ Future<T?> showModalSideSheet<T>({
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
|
||||
final NavigatorState navigator =
|
||||
Navigator.of(context, rootNavigator: useRootNavigator);
|
||||
final NavigatorState navigator = Navigator.of(
|
||||
context,
|
||||
rootNavigator: useRootNavigator,
|
||||
);
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
return navigator.push(ModalSideSheetRoute<T>(
|
||||
builder: builder,
|
||||
filter: filter,
|
||||
capturedThemes:
|
||||
InheritedTheme.capture(from: context, to: navigator.context),
|
||||
isScrollControlled: isScrollControlled,
|
||||
scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
|
||||
barrierLabel: barrierLabel ?? localizations.scrimLabel,
|
||||
barrierOnTapHint:
|
||||
localizations.scrimOnTapHint(localizations.bottomSheetLabel),
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: elevation,
|
||||
shape: shape,
|
||||
clipBehavior: clipBehavior,
|
||||
constraints: constraints,
|
||||
isDismissible: isDismissible,
|
||||
modalBarrierColor:
|
||||
barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor,
|
||||
settings: routeSettings,
|
||||
transitionAnimationController: transitionAnimationController,
|
||||
anchorPoint: anchorPoint,
|
||||
useSafeArea: useSafeArea,
|
||||
));
|
||||
return navigator.push(
|
||||
ModalSideSheetRoute<T>(
|
||||
builder: builder,
|
||||
filter: filter,
|
||||
capturedThemes: InheritedTheme.capture(
|
||||
from: context,
|
||||
to: navigator.context,
|
||||
),
|
||||
isScrollControlled: isScrollControlled,
|
||||
scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
|
||||
barrierLabel: barrierLabel ?? localizations.scrimLabel,
|
||||
barrierOnTapHint: localizations.scrimOnTapHint(
|
||||
localizations.bottomSheetLabel,
|
||||
),
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: elevation,
|
||||
shape: shape,
|
||||
clipBehavior: clipBehavior,
|
||||
constraints: constraints,
|
||||
isDismissible: isDismissible,
|
||||
modalBarrierColor:
|
||||
barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor,
|
||||
settings: routeSettings,
|
||||
transitionAnimationController: transitionAnimationController,
|
||||
anchorPoint: anchorPoint,
|
||||
useSafeArea: useSafeArea,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// class ModalAppBar extends StatelessWidget {
|
||||
|
||||
@@ -8,8 +8,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
const EdgeInsetsGeometry _kHorizontalItemPadding =
|
||||
EdgeInsets.symmetric(vertical: 2, horizontal: 3);
|
||||
const EdgeInsetsGeometry _kHorizontalItemPadding = EdgeInsets.symmetric(
|
||||
vertical: 2,
|
||||
horizontal: 3,
|
||||
);
|
||||
|
||||
const Radius _kCornerRadius = Radius.circular(9);
|
||||
|
||||
@@ -63,11 +65,11 @@ class CommonTabBar<T extends Object> extends StatefulWidget {
|
||||
this.padding = _kHorizontalItemPadding,
|
||||
this.backgroundColor,
|
||||
this.proportionalWidth = false,
|
||||
}) : assert(children.length >= 2),
|
||||
assert(
|
||||
groupValue == null || children.keys.contains(groupValue),
|
||||
'The groupValue must be either null or one of the keys in the children map.',
|
||||
);
|
||||
}) : assert(children.length >= 2),
|
||||
assert(
|
||||
groupValue == null || children.keys.contains(groupValue),
|
||||
'The groupValue must be either null or one of the keys in the children map.',
|
||||
);
|
||||
final Map<T, Widget> children;
|
||||
final Set<T> disabledChildren;
|
||||
final T? groupValue;
|
||||
@@ -190,8 +192,9 @@ class _CommonTabBarState<T extends Object> extends State<CommonTabBar<T>>
|
||||
void _playThumbScaleAnimation({required bool isExpanding}) {
|
||||
thumbScaleAnimation = thumbScaleController.drive(
|
||||
Tween<double>(
|
||||
begin: thumbScaleAnimation.value,
|
||||
end: isExpanding ? 1 : _kMinThumbScale),
|
||||
begin: thumbScaleAnimation.value,
|
||||
end: isExpanding ? 1 : _kMinThumbScale,
|
||||
),
|
||||
);
|
||||
thumbScaleController.animateWith(_kThumbSpringAnimationSimulation);
|
||||
}
|
||||
@@ -231,8 +234,9 @@ class _CommonTabBarState<T extends Object> extends State<CommonTabBar<T>>
|
||||
void onDown(DragDownDetails details) {
|
||||
final T touchDownSegment = segmentForXPosition(details.localPosition.dx);
|
||||
_startedOnSelectedSegment = touchDownSegment == highlighted;
|
||||
_startedOnDisabledSegment =
|
||||
widget.disabledChildren.contains(touchDownSegment);
|
||||
_startedOnDisabledSegment = widget.disabledChildren.contains(
|
||||
touchDownSegment,
|
||||
);
|
||||
if (widget.disabledChildren.contains(touchDownSegment)) {
|
||||
return;
|
||||
}
|
||||
@@ -373,8 +377,10 @@ class _CommonTabBarState<T extends Object> extends State<CommonTabBar<T>>
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
padding: widget.padding.resolve(Directionality.of(context)),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(_kCornerRadius),
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedSuperellipseBorder(
|
||||
borderRadius: const BorderRadius.all(_kCornerRadius),
|
||||
),
|
||||
color: widget.backgroundColor,
|
||||
),
|
||||
child: AnimatedBuilder(
|
||||
@@ -455,8 +461,9 @@ class _SegmentState<T> extends State<_Segment<T>>
|
||||
end: widget.shouldScaleContent ? _kMinThumbScale : 1.0,
|
||||
),
|
||||
);
|
||||
highlightPressScaleController
|
||||
.animateWith(_kThumbSpringAnimationSimulation);
|
||||
highlightPressScaleController.animateWith(
|
||||
_kThumbSpringAnimationSimulation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,20 +487,21 @@ class _SegmentState<T> extends State<_Segment<T>>
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
AnimatedOpacity(
|
||||
opacity:
|
||||
widget.shouldFadeoutContent ? _kContentPressedMinOpacity : 1,
|
||||
opacity: widget.shouldFadeoutContent
|
||||
? _kContentPressedMinOpacity
|
||||
: 1,
|
||||
duration: _kOpacityAnimationDuration,
|
||||
curve: Curves.ease,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.merge(
|
||||
TextStyle(
|
||||
fontWeight: widget.highlighted
|
||||
? _kHighlightedFontWeight
|
||||
: _kFontWeight,
|
||||
fontSize: _kFontSize,
|
||||
color: widget.enabled ? null : _kDisabledContentColor,
|
||||
),
|
||||
),
|
||||
TextStyle(
|
||||
fontWeight: widget.highlighted
|
||||
? _kHighlightedFontWeight
|
||||
: _kFontWeight,
|
||||
fontSize: _kFontSize,
|
||||
color: widget.enabled ? null : _kDisabledContentColor,
|
||||
),
|
||||
),
|
||||
duration: _kHighlightAnimationDuration,
|
||||
curve: Curves.ease,
|
||||
child: ScaleTransition(
|
||||
@@ -505,7 +513,9 @@ class _SegmentState<T> extends State<_Segment<T>>
|
||||
),
|
||||
DefaultTextStyle.merge(
|
||||
style: const TextStyle(
|
||||
fontWeight: _kHighlightedFontWeight, fontSize: _kFontSize),
|
||||
fontWeight: _kHighlightedFontWeight,
|
||||
fontSize: _kFontSize,
|
||||
),
|
||||
child: widget.child,
|
||||
),
|
||||
],
|
||||
@@ -570,9 +580,7 @@ class _SegmentSeparatorState extends State<_SegmentSeparator>
|
||||
return Padding(
|
||||
padding: _kSeparatorInset,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
decoration: BoxDecoration(color: Colors.transparent),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
@@ -612,7 +620,9 @@ class _CommonTabBarRenderWidget<T extends Object>
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context, _RenderSegmentedControl<T> renderObject) {
|
||||
BuildContext context,
|
||||
_RenderSegmentedControl<T> renderObject,
|
||||
) {
|
||||
assert(renderObject.state == state);
|
||||
renderObject
|
||||
..thumbColor = thumbColor
|
||||
@@ -629,20 +639,24 @@ enum _SegmentLocation { leftmost, rightmost, inbetween }
|
||||
|
||||
class _RenderSegmentedControl<T extends Object> extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox,
|
||||
ContainerBoxParentData<RenderBox>>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox,
|
||||
ContainerBoxParentData<RenderBox>> {
|
||||
ContainerRenderObjectMixin<
|
||||
RenderBox,
|
||||
ContainerBoxParentData<RenderBox>
|
||||
>,
|
||||
RenderBoxContainerDefaultsMixin<
|
||||
RenderBox,
|
||||
ContainerBoxParentData<RenderBox>
|
||||
> {
|
||||
_RenderSegmentedControl({
|
||||
required int? highlightedIndex,
|
||||
required Color thumbColor,
|
||||
required double thumbScale,
|
||||
required bool proportionalWidth,
|
||||
required this.state,
|
||||
}) : _highlightedIndex = highlightedIndex,
|
||||
_thumbColor = thumbColor,
|
||||
_thumbScale = thumbScale,
|
||||
_proportionalWidth = proportionalWidth;
|
||||
}) : _highlightedIndex = highlightedIndex,
|
||||
_thumbColor = thumbColor,
|
||||
_thumbScale = thumbScale,
|
||||
_proportionalWidth = proportionalWidth;
|
||||
|
||||
final _CommonTabBarState<T> state;
|
||||
|
||||
@@ -839,7 +853,9 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
|
||||
child = nonSeparatorChildAfter(child);
|
||||
}
|
||||
return math.min(
|
||||
childWidth, (constraints.maxWidth - totalSeparatorWidth) / childCount);
|
||||
childWidth,
|
||||
(constraints.maxWidth - totalSeparatorWidth) / childCount,
|
||||
);
|
||||
}
|
||||
|
||||
List<double> _getChildWidths(BoxConstraints constraints) {
|
||||
@@ -873,20 +889,28 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
|
||||
}
|
||||
|
||||
Size _computeOverallSize(BoxConstraints constraints) {
|
||||
final double maxChildHeight =
|
||||
_getMaxChildHeight(constraints, constraints.maxWidth);
|
||||
final double maxChildHeight = _getMaxChildHeight(
|
||||
constraints,
|
||||
constraints.maxWidth,
|
||||
);
|
||||
return constraints.constrain(
|
||||
Size(_getChildWidths(constraints).sum + totalSeparatorWidth,
|
||||
maxChildHeight),
|
||||
Size(
|
||||
_getChildWidths(constraints).sum + totalSeparatorWidth,
|
||||
maxChildHeight,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double? computeDryBaseline(
|
||||
covariant BoxConstraints constraints, TextBaseline baseline) {
|
||||
covariant BoxConstraints constraints,
|
||||
TextBaseline baseline,
|
||||
) {
|
||||
final List<double> segmentWidths = _getChildWidths(constraints);
|
||||
final double childHeight =
|
||||
_getMaxChildHeight(constraints, constraints.maxWidth);
|
||||
final double childHeight = _getMaxChildHeight(
|
||||
constraints,
|
||||
constraints.maxWidth,
|
||||
);
|
||||
|
||||
int index = 0;
|
||||
BaselineOffset baselineOffset = BaselineOffset.noBaseline;
|
||||
@@ -928,8 +952,10 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
|
||||
final BoxConstraints childConstraints = BoxConstraints.tight(
|
||||
Size(segmentWidths[index ~/ 2], childHeight),
|
||||
);
|
||||
child.layout(index.isEven ? childConstraints : separatorConstraints,
|
||||
parentUsesSize: true);
|
||||
child.layout(
|
||||
index.isEven ? childConstraints : separatorConstraints,
|
||||
parentUsesSize: true,
|
||||
);
|
||||
final _SegmentedControlContainerBoxParentData childParentData =
|
||||
child.parentData! as _SegmentedControlContainerBoxParentData;
|
||||
final Offset childOffset = Offset(start, 0);
|
||||
@@ -959,9 +985,9 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
|
||||
final double leftMost = firstChildOffset.dx;
|
||||
final double rightMost =
|
||||
(children.last.parentData! as _SegmentedControlContainerBoxParentData)
|
||||
.offset
|
||||
.dx +
|
||||
children.last.size.width;
|
||||
.offset
|
||||
.dx +
|
||||
children.last.size.width;
|
||||
assert(rightMost > leftMost);
|
||||
return Rect.fromLTRB(
|
||||
math.max(thumbRect.left, leftMost - _kThumbInsets.left),
|
||||
@@ -992,8 +1018,10 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
|
||||
if (thumbTween == null) {
|
||||
final Rect startingRect =
|
||||
moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect;
|
||||
state.thumbAnimatable =
|
||||
RectTween(begin: startingRect, end: newThumbRect);
|
||||
state.thumbAnimatable = RectTween(
|
||||
begin: startingRect,
|
||||
end: newThumbRect,
|
||||
);
|
||||
} else if (newThumbRect != thumbTween.transform(1)) {
|
||||
final Rect startingRect =
|
||||
moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect;
|
||||
@@ -1008,7 +1036,7 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
|
||||
|
||||
final Rect unscaledThumbRect =
|
||||
state.thumbAnimatable?.evaluate(state.thumbController) ??
|
||||
newThumbRect;
|
||||
newThumbRect;
|
||||
currentThumbRect = unscaledThumbRect;
|
||||
|
||||
final _SegmentLocation childLocation;
|
||||
@@ -1045,7 +1073,10 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
|
||||
final Paint separatorPaint = Paint();
|
||||
|
||||
void _paintSeparator(
|
||||
PaintingContext context, Offset offset, RenderBox child) {
|
||||
PaintingContext context,
|
||||
Offset offset,
|
||||
RenderBox child,
|
||||
) {
|
||||
final _SegmentedControlContainerBoxParentData childParentData =
|
||||
child.parentData! as _SegmentedControlContainerBoxParentData;
|
||||
context.paintChild(child, offset + childParentData.offset);
|
||||
@@ -1058,23 +1089,20 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
|
||||
}
|
||||
|
||||
void _paintThumb(PaintingContext context, Offset offset, Rect thumbRect) {
|
||||
// const List<BoxShadow> thumbShadow = <BoxShadow>[
|
||||
// BoxShadow(color: Color(0x1F000000), offset: Offset(0, 3), blurRadius: 8),
|
||||
// BoxShadow(color: Color(0x0A000000), offset: Offset(0, 3), blurRadius: 1),
|
||||
// ];
|
||||
final RSuperellipse thumbRSuperellipse = RSuperellipse.fromRectAndRadius(
|
||||
thumbRect.shift(offset),
|
||||
_kThumbRadius,
|
||||
);
|
||||
|
||||
final RRect thumbRRect =
|
||||
RRect.fromRectAndRadius(thumbRect.shift(offset), _kThumbRadius);
|
||||
context.canvas.drawRSuperellipse(
|
||||
thumbRSuperellipse.inflate(0.5),
|
||||
Paint()..color = const Color(0x0A000000),
|
||||
);
|
||||
|
||||
// for (final BoxShadow shadow in thumbShadow) {
|
||||
// context.canvas
|
||||
// .drawRRect(thumbRRect.shift(shadow.offset), shadow.toPaint());
|
||||
// }
|
||||
|
||||
context.canvas.drawRRect(
|
||||
thumbRRect.inflate(0.5), Paint()..color = const Color(0x0A000000));
|
||||
|
||||
context.canvas.drawRRect(thumbRRect, Paint()..color = thumbColor);
|
||||
context.canvas.drawRSuperellipse(
|
||||
thumbRSuperellipse,
|
||||
Paint()..color = thumbColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
export 'activate_box.dart';
|
||||
export 'animate_grid.dart';
|
||||
export 'pop_scope.dart';
|
||||
export 'builder.dart';
|
||||
export 'card.dart';
|
||||
export 'chip.dart';
|
||||
export 'color_scheme_box.dart';
|
||||
export 'container.dart';
|
||||
export 'dialog.dart';
|
||||
export 'disabled_mask.dart';
|
||||
export 'donut_chart.dart';
|
||||
export 'effect.dart';
|
||||
export 'fade_box.dart';
|
||||
export 'float_layout.dart';
|
||||
export 'grid.dart';
|
||||
@@ -13,23 +17,19 @@ export 'input.dart';
|
||||
export 'keep_scope.dart';
|
||||
export 'line_chart.dart';
|
||||
export 'list.dart';
|
||||
export 'notification.dart';
|
||||
export 'null_status.dart';
|
||||
export 'open_container.dart';
|
||||
export 'palette.dart';
|
||||
export 'pop_scope.dart';
|
||||
export 'popup.dart';
|
||||
export 'scaffold.dart';
|
||||
export 'scroll.dart';
|
||||
export 'setting.dart';
|
||||
export 'sheet.dart';
|
||||
export 'side_sheet.dart';
|
||||
export 'subscription_info_view.dart';
|
||||
export 'text.dart';
|
||||
export 'super_grid.dart';
|
||||
export 'donut_chart.dart';
|
||||
export 'activate_box.dart';
|
||||
export 'wave.dart';
|
||||
export 'scroll.dart';
|
||||
export 'dialog.dart';
|
||||
export 'effect.dart';
|
||||
export 'palette.dart';
|
||||
export 'tab.dart';
|
||||
export 'container.dart';
|
||||
export 'notification.dart';
|
||||
export 'text.dart';
|
||||
export 'wave.dart';
|
||||
|
||||
12
pubspec.lock
12
pubspec.lock
@@ -666,10 +666,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: a45bef33deb24839a51fb85a4d9e504ead2b1ad1c4779d02d09bf6a8857cdd52
|
||||
sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+2"
|
||||
version: "0.8.13+3"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1010,10 +1010,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
version: "1.5.2"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1471,10 +1471,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "07cffecb7d68cbc6437cd803d5f11a86fe06736735c3dfe46ff73bcb0f958eed"
|
||||
sha256: "199bc33e746088546a39cc5f36bac5a278c5e53b40cb3196f99e7345fdcfae6b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.21"
|
||||
version: "6.3.22"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: fl_clash
|
||||
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
|
||||
publish_to: 'none'
|
||||
version: 0.8.88+2025091902
|
||||
version: 0.8.88+2025092201
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user