mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-06 00:06:04 +08:00
desktop hosts setting (#206)
This commit is contained in:
@@ -101,6 +101,7 @@
|
||||
"matchRule": "Match Rule",
|
||||
"emptyMatchAll": "Empty means match all",
|
||||
"newBuilt": "New",
|
||||
"newFolder": "New Folder",
|
||||
"enableSelect": "Enable Select",
|
||||
"disableSelect": "Disable Select",
|
||||
"deleteSelect": "Delete Select",
|
||||
@@ -309,5 +310,7 @@
|
||||
"timestamp": "Timestamp",
|
||||
"convert": "Convert",
|
||||
"time": "DateTime",
|
||||
"nowTimestamp": "Now timestamp"
|
||||
"nowTimestamp": "Now timestamp",
|
||||
"hosts": "Hosts",
|
||||
"toAddress": "To Address"
|
||||
}
|
||||
@@ -101,6 +101,7 @@
|
||||
"matchRule": "匹配规则",
|
||||
"emptyMatchAll": "为空表示匹配全部",
|
||||
"newBuilt": "新建",
|
||||
"newFolder": "新建文件夹",
|
||||
"enableSelect": "启用选择",
|
||||
"disableSelect": "禁用选择",
|
||||
"deleteSelect": "删除选择",
|
||||
@@ -308,5 +309,7 @@
|
||||
"timestamp": "时间戳",
|
||||
"convert": "转换",
|
||||
"time": "时间",
|
||||
"nowTimestamp": "当前时间戳(秒)"
|
||||
"nowTimestamp": "当前时间戳(秒)",
|
||||
"hosts": "Hosts 映射",
|
||||
"toAddress": "映射地址"
|
||||
}
|
||||
@@ -22,15 +22,17 @@ import 'interceptor.dart';
|
||||
/// Hosts interceptor
|
||||
/// @author wanghongen
|
||||
class Hosts extends Interceptor {
|
||||
Future<HostsManager> get hostsManager async => await HostsManager.instance;
|
||||
|
||||
@override
|
||||
int get priority => -1000;
|
||||
|
||||
@override
|
||||
Future<HostAndPort> preConnect(HostAndPort hostAndPort) async {
|
||||
var host = hostAndPort.host;
|
||||
var hostsItem = await HostsManager.instance.getHosts(host);
|
||||
var hostsItem = await hostsManager.then((it) => it.getHosts(host));
|
||||
if (hostsItem != null) {
|
||||
return hostAndPort.copyWith(host: hostsItem.mappingAddress);
|
||||
return hostAndPort.copyWith(host: hostsItem.toAddress);
|
||||
}
|
||||
return hostAndPort;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,11 @@ class HostsManager {
|
||||
HostsManager._();
|
||||
|
||||
/// Singleton
|
||||
static HostsManager get instance {
|
||||
_instance ??= HostsManager._();
|
||||
static Future<HostsManager> get instance async {
|
||||
if (_instance == null) {
|
||||
_instance = HostsManager._();
|
||||
await _instance?.load();
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
@@ -73,8 +76,8 @@ class HostsManager {
|
||||
var hostsItem = HostsItem.fromJson(element);
|
||||
|
||||
if (hostsItem.parent != null) {
|
||||
var list = _folderMap[hostsItem.parent!] ??= [];
|
||||
list.add(hostsItem);
|
||||
var children = _folderMap[hostsItem.parent!] ??= [];
|
||||
children.add(hostsItem);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,14 +102,17 @@ class HostsManager {
|
||||
(await configFile).writeAsString(json);
|
||||
}
|
||||
|
||||
List<HostsItem> getFolderList(String parent) {
|
||||
return _folderMap[parent] ?? [];
|
||||
}
|
||||
|
||||
Future<void> addHosts(HostsItem item) async {
|
||||
if (item.parent == null) {
|
||||
list.add(item);
|
||||
} else {
|
||||
var list = _folderMap[item.parent!] ??= [];
|
||||
list.add(item);
|
||||
var children = _folderMap[item.parent!] ??= [];
|
||||
children.add(item);
|
||||
}
|
||||
await flushConfig();
|
||||
}
|
||||
|
||||
Future<HostsItem?> getHosts(String host) async {
|
||||
@@ -133,19 +139,39 @@ class HostsManager {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
removeHosts(Iterable<HostsItem> items) async {
|
||||
if (items.isEmpty) return;
|
||||
for (var item in items) {
|
||||
if (item.parent == null) {
|
||||
list.remove(item);
|
||||
if (item.isFolder) {
|
||||
_folderMap.remove(item.id);
|
||||
}
|
||||
} else {
|
||||
var children = _folderMap[item.parent!] ??= [];
|
||||
children.remove(item);
|
||||
}
|
||||
}
|
||||
flushConfig();
|
||||
}
|
||||
}
|
||||
|
||||
class HostsItem {
|
||||
bool enabled = true;
|
||||
bool isFolder = false;
|
||||
final String id;
|
||||
final String? parent;
|
||||
final String host;
|
||||
final String mappingAddress;
|
||||
String? parent;
|
||||
String host;
|
||||
String? toAddress;
|
||||
RegExp? _hostReg;
|
||||
|
||||
HostsItem(this.enabled, this.host, this.mappingAddress, {String? id, this.isFolder = false, this.parent})
|
||||
: id = id ?? DateTime.now().millisecondsSinceEpoch.toRadixString(36) + RandomUtil.randomString(4);
|
||||
HostsItem({String? id, required this.host, this.toAddress, required this.enabled, this.isFolder = false, this.parent})
|
||||
: id = id ?? generateId();
|
||||
|
||||
static String generateId() {
|
||||
return DateTime.now().millisecondsSinceEpoch.toRadixString(36) + RandomUtil.randomString(4);
|
||||
}
|
||||
|
||||
//匹配url
|
||||
bool match(String url) {
|
||||
@@ -156,11 +182,11 @@ class HostsItem {
|
||||
factory HostsItem.fromJson(Map<String, dynamic> json) {
|
||||
return HostsItem(
|
||||
id: json['id'],
|
||||
host: json['host'],
|
||||
toAddress: json['toAddress'],
|
||||
enabled: json['enabled'],
|
||||
parent: json['parent'],
|
||||
isFolder: json['isFolder'] == true,
|
||||
json['enabled'],
|
||||
json['host'],
|
||||
json['mappingAddress'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,7 +197,7 @@ class HostsItem {
|
||||
'enabled': enabled,
|
||||
'isFolder': isFolder,
|
||||
'host': host,
|
||||
'mappingAddress': mappingAddress,
|
||||
'toAddress': toAddress,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
@@ -57,8 +58,10 @@ class _FilterDialogState extends State<FilterDialog> {
|
||||
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||
scrollable: true,
|
||||
title: Row(children: [
|
||||
Text(localizations.domainFilter, style: const TextStyle(fontSize: 18)),
|
||||
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
|
||||
const Expanded(child: SizedBox()),
|
||||
Text(localizations.domainFilter, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
|
||||
const Expanded(child: SizedBox()),
|
||||
Align(alignment: Alignment.topRight, child: CloseButton())
|
||||
]),
|
||||
content: SizedBox(
|
||||
width: 680,
|
||||
@@ -266,7 +269,8 @@ class _DomainListState extends State<DomainList> {
|
||||
Map<int, bool> selected = {};
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
bool isPress = false;
|
||||
bool isPressed = false;
|
||||
Offset? lastPressPosition;
|
||||
bool changed = false;
|
||||
|
||||
onChanged() {
|
||||
@@ -291,8 +295,13 @@ class _DomainListState extends State<DomainList> {
|
||||
});
|
||||
},
|
||||
child: Listener(
|
||||
onPointerUp: (details) => isPress = false,
|
||||
onPointerDown: (details) => isPress = true,
|
||||
onPointerUp: (event) => isPressed = false,
|
||||
onPointerDown: (event) {
|
||||
lastPressPosition = event.localPosition;
|
||||
if (event.buttons == kPrimaryMouseButton) {
|
||||
isPressed = true;
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
height: 380,
|
||||
@@ -323,7 +332,7 @@ class _DomainListState extends State<DomainList> {
|
||||
//right click menus
|
||||
onDoubleTap: () => showEdit(index),
|
||||
onHover: (hover) {
|
||||
if (isPress && selected[index] != true) {
|
||||
if (isPressed && selected[index] != true) {
|
||||
setState(() {
|
||||
selected[index] = true;
|
||||
});
|
||||
@@ -347,7 +356,7 @@ class _DomainListState extends State<DomainList> {
|
||||
color: selected[index] == true
|
||||
? primaryColor.withOpacity(0.8)
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
: null,
|
||||
height: 38,
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
|
||||
@@ -15,13 +15,17 @@
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||||
import 'package:proxypin/network/components/manager/hosts_manager.dart';
|
||||
import 'package:proxypin/network/util/logger.dart';
|
||||
import 'package:proxypin/ui/component/utils.dart';
|
||||
import 'package:proxypin/ui/component/widgets.dart';
|
||||
|
||||
///hosts设置
|
||||
@@ -36,100 +40,306 @@ class HostsDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HostsDialogState extends State<HostsDialog> {
|
||||
bool changed = false;
|
||||
Set<HostsItem> selected = {};
|
||||
Set<String> offstage = {};
|
||||
|
||||
bool isPressed = false;
|
||||
Offset? lastPressPosition;
|
||||
|
||||
bool saving = false;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),
|
||||
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||
scrollable: true,
|
||||
title: Row(children: [
|
||||
Text('Hosts', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
|
||||
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
|
||||
]),
|
||||
content: SizedBox(
|
||||
width: 550,
|
||||
height: 500,
|
||||
child: Column(children: [
|
||||
Row(children: [
|
||||
Text(localizations.enable),
|
||||
const SizedBox(width: 10),
|
||||
SwitchWidget(
|
||||
scale: 0.8,
|
||||
value: widget.hostsManager.enabled,
|
||||
onChanged: (value) {
|
||||
widget.hostsManager.enabled = value;
|
||||
changed = true;
|
||||
}),
|
||||
const Expanded(child: SizedBox()),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.add, size: 14),
|
||||
onPressed: add,
|
||||
label: Text(localizations.add, style: const TextStyle(fontSize: 12))),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 14),
|
||||
onPressed: import,
|
||||
label: Text(localizations.import, style: const TextStyle(fontSize: 12))),
|
||||
const SizedBox(width: 5),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 430,
|
||||
decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 5),
|
||||
Row(children: [
|
||||
SizedBox(width: 80, child: Text(localizations.enable, style: const TextStyle(fontSize: 14))),
|
||||
Container(width: 15),
|
||||
Expanded(child: Text(localizations.domain, style: TextStyle(fontSize: 14))),
|
||||
Container(width: 18),
|
||||
Expanded(child: Text('To Address', style: const TextStyle(fontSize: 14))),
|
||||
]),
|
||||
const Divider(thickness: 0.5),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: widget.hostsManager.list.length, itemBuilder: (_, index) => row(index)))
|
||||
])),
|
||||
]),
|
||||
));
|
||||
saveConfig() {
|
||||
if (saving) return;
|
||||
Future.delayed(const Duration(milliseconds: 3000), () {
|
||||
saving = false;
|
||||
});
|
||||
}
|
||||
|
||||
Widget row(int index) {
|
||||
var primaryColor = Theme.of(context).colorScheme.primary;
|
||||
var list = widget.hostsManager.list;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onSecondaryTap: () {
|
||||
if (lastPressPosition == null) {
|
||||
return;
|
||||
}
|
||||
showGlobalMenu(lastPressPosition!);
|
||||
},
|
||||
onTapDown: (details) {
|
||||
if (selected.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
hoverColor: primaryColor.withOpacity(0.3),
|
||||
// onSecondaryTapDown: (details) => showMenus(details, index),
|
||||
// onDoubleTap: () => showEdit(index),
|
||||
child: Container(
|
||||
color: index.isEven ? Colors.grey.withOpacity(0.1) : null,
|
||||
height: 38,
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(list[index].host, style: const TextStyle(fontSize: 14))),
|
||||
const SizedBox(width: 20),
|
||||
SwitchWidget(
|
||||
scale: 0.65,
|
||||
value: list[index].enabled,
|
||||
onChanged: (val) {
|
||||
list[index].enabled = val;
|
||||
setState(() {
|
||||
changed = true;
|
||||
});
|
||||
}),
|
||||
const SizedBox(width: 40),
|
||||
SizedBox(width: 130, child: Text(list[index].mappingAddress, style: const TextStyle(fontSize: 14)))
|
||||
],
|
||||
)));
|
||||
if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
selected.clear();
|
||||
});
|
||||
},
|
||||
child: Listener(
|
||||
onPointerUp: (event) => isPressed = false,
|
||||
onPointerDown: (event) {
|
||||
lastPressPosition = event.localPosition;
|
||||
if (event.buttons == kPrimaryMouseButton) {
|
||||
isPressed = true;
|
||||
}
|
||||
},
|
||||
child: AlertDialog(
|
||||
titlePadding: const EdgeInsets.only(left: 20, top: 10, right: 15),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
scrollable: true,
|
||||
title: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
const Expanded(child: SizedBox()),
|
||||
Text('Hosts', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
|
||||
const Expanded(child: SizedBox()),
|
||||
Align(alignment: Alignment.topRight, child: CloseButton())
|
||||
]),
|
||||
content: SizedBox(
|
||||
width: 550,
|
||||
height: 500,
|
||||
child: Column(children: [
|
||||
Row(children: [
|
||||
Container(width: 15),
|
||||
Text(localizations.enable),
|
||||
const SizedBox(width: 10),
|
||||
SwitchWidget(
|
||||
scale: 0.8,
|
||||
value: widget.hostsManager.enabled,
|
||||
onChanged: (value) {
|
||||
widget.hostsManager.enabled = value;
|
||||
saveConfig();
|
||||
}),
|
||||
const Expanded(child: SizedBox()),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.add, size: 14),
|
||||
onPressed: showEdit,
|
||||
label: Text(localizations.newBuilt, style: const TextStyle(fontSize: 12))),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.folder_outlined, size: 14),
|
||||
onPressed: newFolder,
|
||||
label: Text(localizations.newFolder, style: const TextStyle(fontSize: 12))),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 14),
|
||||
onPressed: import,
|
||||
label: Text(localizations.import, style: const TextStyle(fontSize: 12))),
|
||||
const SizedBox(width: 5),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 430,
|
||||
decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))),
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 5),
|
||||
Row(children: [
|
||||
Container(width: 15),
|
||||
SizedBox(
|
||||
width: 50, child: Text(localizations.enable, style: const TextStyle(fontSize: 14))),
|
||||
Container(width: 15),
|
||||
Expanded(child: Text(localizations.domain, style: TextStyle(fontSize: 14))),
|
||||
Container(width: 15),
|
||||
Expanded(child: Text(localizations.toAddress, style: const TextStyle(fontSize: 14))),
|
||||
]),
|
||||
const Divider(thickness: 0.5),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.hostsManager.list.length,
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
itemBuilder: (_, index) => row(widget.hostsManager.list[index], index.isEven)))
|
||||
])),
|
||||
]),
|
||||
))));
|
||||
}
|
||||
|
||||
Widget row(HostsItem item, bool isEven, {EdgeInsetsGeometry? padding}) {
|
||||
var primaryColor = Theme.of(context).colorScheme.primary;
|
||||
|
||||
return Column(children: [
|
||||
InkWell(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
hoverColor: primaryColor.withOpacity(0.3),
|
||||
onSecondaryTapDown: (details) => showMenus(details, item),
|
||||
onDoubleTap: item.isFolder ? null : () => showEdit(item: item),
|
||||
onTap: () {
|
||||
if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) {
|
||||
setState(() {
|
||||
selected.contains(item) ? selected.remove(item) : selected.add(item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPressed && selected.isNotEmpty) {
|
||||
setState(() {
|
||||
selected.clear();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.isFolder) {
|
||||
setState(() {
|
||||
offstage.contains(item.id) ? offstage.remove(item.id) : offstage.add(item.id);
|
||||
});
|
||||
}
|
||||
},
|
||||
onHover: (hover) {
|
||||
if (isPressed && !selected.contains(item)) {
|
||||
setState(() {
|
||||
selected.add(item);
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
color: selected.contains(item)
|
||||
? primaryColor.withOpacity(0.6)
|
||||
: isEven
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
: null,
|
||||
height: 35,
|
||||
padding: padding ?? const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
children: [
|
||||
SwitchWidget(
|
||||
scale: 0.6,
|
||||
value: item.enabled,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
item.enabled = val;
|
||||
saveConfig();
|
||||
});
|
||||
}),
|
||||
Container(width: 15),
|
||||
Expanded(
|
||||
child: IconText(
|
||||
icon: item.isFolder
|
||||
? Icon(offstage.contains(item.id) ? Icons.folder : Icons.folder_outlined, size: 18)
|
||||
: null,
|
||||
text: item.host,
|
||||
textStyle: const TextStyle(fontSize: 14))),
|
||||
Container(width: 15),
|
||||
Expanded(child: Text(item.toAddress ?? '', style: const TextStyle(fontSize: 14)))
|
||||
],
|
||||
))),
|
||||
if (item.isFolder)
|
||||
Offstage(
|
||||
offstage: offstage.contains(item.id),
|
||||
child: Column(
|
||||
children: widget.hostsManager
|
||||
.getFolderList(item.id)
|
||||
.map((e) => row(e, !isEven, padding: EdgeInsets.only(left: 60)))
|
||||
.toList()))
|
||||
]);
|
||||
}
|
||||
|
||||
newFolder() {
|
||||
showDialog(context: context, builder: (BuildContext context) => FolderDialog(hostsManager: widget.hostsManager))
|
||||
.then((value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
saveConfig();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableStatus(bool enable) {
|
||||
if (selected.isEmpty) return;
|
||||
|
||||
for (var item in selected) {
|
||||
if (item.enabled == enable) continue;
|
||||
item.enabled = enable;
|
||||
}
|
||||
setState(() {
|
||||
saveConfig();
|
||||
});
|
||||
}
|
||||
|
||||
showGlobalMenu(Offset offset) {
|
||||
showContextMenu(context, offset, items: [
|
||||
PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit()),
|
||||
PopupMenuItem(
|
||||
height: 35, enabled: selected.isNotEmpty, child: Text(localizations.export), onTap: () => export(selected)),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(height: 35, child: Text(localizations.enableSelect), onTap: () => enableStatus(true)),
|
||||
PopupMenuItem(height: 35, child: Text(localizations.disableSelect), onTap: () => enableStatus(false)),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
height: 35,
|
||||
enabled: selected.isNotEmpty,
|
||||
child: Text(localizations.deleteSelect),
|
||||
onTap: () => removeRewrite(selected)),
|
||||
]);
|
||||
}
|
||||
|
||||
//点击菜单
|
||||
showMenus(TapDownDetails details, HostsItem item) {
|
||||
if (selected.length > 1) {
|
||||
showGlobalMenu(details.globalPosition);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
selected.add(item);
|
||||
});
|
||||
|
||||
showContextMenu(context, details.globalPosition, items: [
|
||||
if (item.isFolder)
|
||||
PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit(parent: item)),
|
||||
PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(item: item)),
|
||||
PopupMenuItem(height: 35, onTap: () => export([item]), child: Text(localizations.export)),
|
||||
PopupMenuItem(
|
||||
height: 35,
|
||||
child: item.enabled ? Text(localizations.disabled) : Text(localizations.enable),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
item.enabled = !item.enabled;
|
||||
saveConfig();
|
||||
});
|
||||
}),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
height: 35,
|
||||
child: Text(localizations.delete),
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
widget.hostsManager.removeHosts([item]);
|
||||
});
|
||||
})
|
||||
]).then((value) {
|
||||
setState(() {
|
||||
selected.remove(item);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
showEdit({HostsItem? item, HostsItem? parent}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => item?.isFolder == true
|
||||
? FolderDialog(hostsManager: widget.hostsManager, folder: item)
|
||||
: HostsEditDialog(item: item, parent: parent)).then((value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
saveConfig();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//删除
|
||||
Future<void> removeRewrite(Set<HostsItem> items) async {
|
||||
if (items.isEmpty) return;
|
||||
return showConfirmDialog(context, onConfirm: () async {
|
||||
await widget.hostsManager.removeHosts(items);
|
||||
setState(() {
|
||||
items.clear();
|
||||
});
|
||||
if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);
|
||||
});
|
||||
}
|
||||
|
||||
//导入
|
||||
@@ -143,11 +353,23 @@ class _HostsDialogState extends State<HostsDialog> {
|
||||
|
||||
try {
|
||||
List json = jsonDecode(await file.xFile.readAsString());
|
||||
Map<String, String> idMap = {};
|
||||
|
||||
for (var item in json) {
|
||||
// widget.hostList.add(item);
|
||||
//生成新的id 保存映射关系
|
||||
String newId = HostsItem.generateId();
|
||||
idMap[item['id']] = newId;
|
||||
item['id'] = newId;
|
||||
var hostsItem = HostsItem.fromJson(item);
|
||||
|
||||
if (hostsItem.parent != null) {
|
||||
hostsItem.parent = idMap[hostsItem.parent!];
|
||||
}
|
||||
|
||||
widget.hostsManager.addHosts(hostsItem);
|
||||
}
|
||||
|
||||
changed = true;
|
||||
saveConfig();
|
||||
if (mounted) {
|
||||
FlutterToastr.show(localizations.importSuccess, context);
|
||||
}
|
||||
@@ -160,16 +382,184 @@ class _HostsDialogState extends State<HostsDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
void add() {
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// barrierDismissible: false,
|
||||
// builder: (BuildContext context) => DomainAddDialog(hostList: widget.hostList)).then((value) {
|
||||
// if (value != null) {
|
||||
// setState(() {
|
||||
// changed = true;
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
//导出
|
||||
export(Iterable<HostsItem> items) async {
|
||||
if (items.isEmpty) return;
|
||||
|
||||
String fileName = 'hosts.json';
|
||||
var path = await FilePicker.platform.saveFile(fileName: fileName);
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var list = [];
|
||||
for (var item in items) {
|
||||
var json = item.toJson();
|
||||
list.add(json);
|
||||
}
|
||||
|
||||
await File(path).writeAsBytes(utf8.encode(jsonEncode(list)));
|
||||
if (mounted) FlutterToastr.show(localizations.exportSuccess, context);
|
||||
}
|
||||
}
|
||||
|
||||
class FolderDialog extends StatelessWidget {
|
||||
final HostsManager hostsManager;
|
||||
final HostsItem? folder;
|
||||
|
||||
const FolderDialog({super.key, required this.hostsManager, this.folder});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
AppLocalizations localizations = AppLocalizations.of(context)!;
|
||||
bool enabled = folder?.enabled ?? true;
|
||||
String name = folder?.host ?? '';
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(localizations.newFolder, style: const TextStyle(fontSize: 16)),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Row(children: [
|
||||
SizedBox(width: 55, child: Text(localizations.enable)),
|
||||
SwitchWidget(scale: 0.8, value: enabled, onChanged: (value) => enabled = value)
|
||||
]),
|
||||
SizedBox(height: 10),
|
||||
Row(children: [
|
||||
SizedBox(width: 55, child: Text(localizations.name)),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: name,
|
||||
onChanged: (val) => name = val,
|
||||
decoration: InputDecoration(border: OutlineInputBorder())))
|
||||
])
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HostsItem item;
|
||||
if (folder == null) {
|
||||
item = HostsItem(isFolder: true, host: name, enabled: enabled);
|
||||
hostsManager.addHosts(item);
|
||||
} else {
|
||||
folder!.enabled = enabled;
|
||||
folder!.host = name;
|
||||
item = folder!;
|
||||
}
|
||||
Navigator.pop(context, item);
|
||||
},
|
||||
child: Text(localizations.save)),
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HostsEditDialog extends StatefulWidget {
|
||||
final HostsItem? item;
|
||||
final HostsItem? parent;
|
||||
|
||||
const HostsEditDialog({super.key, this.item, this.parent});
|
||||
|
||||
@override
|
||||
State<HostsEditDialog> createState() => _HostsEditDialogState();
|
||||
}
|
||||
|
||||
class _HostsEditDialogState extends State<HostsEditDialog> {
|
||||
GlobalKey formKey = GlobalKey<FormState>();
|
||||
|
||||
bool enabled = true;
|
||||
TextEditingController hostController = TextEditingController();
|
||||
TextEditingController toAddressController = TextEditingController();
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.item != null) {
|
||||
enabled = widget.item!.enabled;
|
||||
hostController.text = widget.item!.host;
|
||||
toAddressController.text = widget.item!.toAddress ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
hostController.dispose();
|
||||
toAddressController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.only(left: 20, right: 20, top: 10),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (!(formKey.currentState as FormState).validate()) {
|
||||
FlutterToastr.show(
|
||||
"${localizations.domain} ${localizations.toAddress} ${localizations.cannotBeEmpty}", context,
|
||||
position: FlutterToastr.center);
|
||||
return;
|
||||
}
|
||||
|
||||
HostsItem? hostItem;
|
||||
if (widget.item == null) {
|
||||
hostItem = HostsItem(
|
||||
enabled: enabled,
|
||||
parent: widget.parent?.id,
|
||||
host: hostController.text,
|
||||
toAddress: toAddressController.text);
|
||||
HostsManager.instance.then((it) => it.addHosts(hostItem!));
|
||||
} else {
|
||||
widget.item!.enabled = enabled;
|
||||
widget.item!.host = hostController.text;
|
||||
widget.item!.toAddress = toAddressController.text;
|
||||
hostItem = widget.item;
|
||||
}
|
||||
|
||||
Navigator.pop(context, hostItem);
|
||||
},
|
||||
child: Text(localizations.save)),
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel))
|
||||
],
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
height: 180,
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(children: [
|
||||
Row(children: [
|
||||
SizedBox(width: 80, child: Text(localizations.enable)),
|
||||
Expanded(child: SwitchWidget(scale: 0.8, value: enabled, onChanged: (value) => enabled = value)),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Row(children: [
|
||||
SizedBox(width: 80, child: Text(localizations.domain)),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: hostController,
|
||||
validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '*.example.com',
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
errorStyle: TextStyle(height: 0, fontSize: 0),
|
||||
border: OutlineInputBorder()))),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
Row(children: [
|
||||
SizedBox(width: 80, child: Text(localizations.toAddress)),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: toAddressController,
|
||||
validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '202.108.22.5',
|
||||
errorStyle: TextStyle(height: 0, fontSize: 0),
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
border: OutlineInputBorder()))),
|
||||
]),
|
||||
])),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,10 @@ class _RequestBlockState extends State<RequestBlock> {
|
||||
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||
scrollable: true,
|
||||
title: Row(children: [
|
||||
Text(localizations.requestBlock, style: const TextStyle(fontSize: 16)),
|
||||
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
|
||||
const Expanded(child: SizedBox()),
|
||||
Text(localizations.requestBlock, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
const Expanded(child: SizedBox()),
|
||||
Align(alignment: Alignment.topRight, child: CloseButton())
|
||||
]),
|
||||
content: SizedBox(
|
||||
width: 550,
|
||||
@@ -110,8 +112,8 @@ class _RequestBlockState extends State<RequestBlock> {
|
||||
onSecondaryTapDown: (details) => showMenus(details, index),
|
||||
onDoubleTap: () => showEdit(index),
|
||||
child: Container(
|
||||
color: index.isEven ? Colors.grey.withOpacity(0.1) : null,
|
||||
height: 38,
|
||||
color: index.isEven ? Colors.grey.withOpacity(0.15) : null,
|
||||
height: 36,
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -219,7 +221,7 @@ class RequestBlockAddDialog extends StatelessWidget {
|
||||
onChanged: (val) {}),
|
||||
]))),
|
||||
actions: [
|
||||
FilledButton(
|
||||
TextButton(
|
||||
child: Text(localizations.save),
|
||||
onPressed: () {
|
||||
if (!(formKey.currentState as FormState).validate()) {
|
||||
@@ -236,7 +238,7 @@ class RequestBlockAddDialog extends StatelessWidget {
|
||||
}
|
||||
Navigator.of(context).pop(item);
|
||||
}),
|
||||
ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop())
|
||||
TextButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop())
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,8 @@ class RequestRuleList extends StatefulWidget {
|
||||
class _RequestRuleListState extends State<RequestRuleList> {
|
||||
Map<int, bool> selected = {};
|
||||
late List<RequestRewriteRule> rules;
|
||||
bool isPress = false;
|
||||
bool isPressed = false;
|
||||
Offset? lastPressPosition;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@@ -221,7 +222,12 @@ class _RequestRuleListState extends State<RequestRuleList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onSecondaryTapDown: (details) => showGlobalMenu(details.globalPosition),
|
||||
onSecondaryTap: () {
|
||||
if (lastPressPosition == null) {
|
||||
return;
|
||||
}
|
||||
showGlobalMenu(lastPressPosition!);
|
||||
},
|
||||
onTapDown: (details) {
|
||||
if (selected.isEmpty) {
|
||||
return;
|
||||
@@ -234,8 +240,13 @@ class _RequestRuleListState extends State<RequestRuleList> {
|
||||
});
|
||||
},
|
||||
child: Listener(
|
||||
onPointerUp: (details) => isPress = false,
|
||||
onPointerDown: (details) => isPress = true,
|
||||
onPointerUp: (event) => isPressed = false,
|
||||
onPointerDown: (event) {
|
||||
lastPressPosition = event.localPosition;
|
||||
if (event.buttons == kPrimaryMouseButton) {
|
||||
isPressed = true;
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
constraints: const BoxConstraints(maxHeight: 600, minHeight: 550),
|
||||
@@ -294,7 +305,7 @@ class _RequestRuleListState extends State<RequestRuleList> {
|
||||
onSecondaryTapDown: (details) => showMenus(details, index),
|
||||
onDoubleTap: () => showEdit(index),
|
||||
onHover: (hover) {
|
||||
if (isPress && selected[index] != true) {
|
||||
if (isPressed && selected[index] != true) {
|
||||
setState(() {
|
||||
selected[index] = true;
|
||||
});
|
||||
@@ -318,7 +329,7 @@ class _RequestRuleListState extends State<RequestRuleList> {
|
||||
color: selected[index] == true
|
||||
? primaryColor.withOpacity(0.8)
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
: null,
|
||||
height: 30,
|
||||
padding: const EdgeInsets.all(5),
|
||||
@@ -416,9 +427,9 @@ class _RequestRuleListState extends State<RequestRuleList> {
|
||||
showGlobalMenu(details.globalPosition);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
selected[index] = true;
|
||||
});
|
||||
// setState(() {
|
||||
// selected[index] = true;
|
||||
// });
|
||||
showContextMenu(context, details.globalPosition, items: [
|
||||
PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(index)),
|
||||
PopupMenuItem(height: 35, onTap: () => export([index]), child: Text(localizations.export)),
|
||||
@@ -438,9 +449,9 @@ class _RequestRuleListState extends State<RequestRuleList> {
|
||||
MultiWindow.invokeRefreshRewrite(Operation.delete, index: index);
|
||||
})
|
||||
]).then((value) {
|
||||
setState(() {
|
||||
selected.remove(index);
|
||||
});
|
||||
// setState(() {
|
||||
// selected.remove(index);
|
||||
// });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ class _UpdateListState extends State<UpdateList> {
|
||||
color: selected == index
|
||||
? primaryColor
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
: null,
|
||||
height: 30,
|
||||
padding: const EdgeInsets.all(5),
|
||||
|
||||
@@ -441,7 +441,8 @@ class ScriptList extends StatefulWidget {
|
||||
|
||||
class _ScriptListState extends State<ScriptList> {
|
||||
Set<int> selected = {};
|
||||
bool isPress = false;
|
||||
bool isPressed = false;
|
||||
Offset? lastPressPosition;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@@ -461,8 +462,13 @@ class _ScriptListState extends State<ScriptList> {
|
||||
});
|
||||
},
|
||||
child: Listener(
|
||||
onPointerUp: (details) => isPress = false,
|
||||
onPointerDown: (details) => isPress = true,
|
||||
onPointerUp: (event) => isPressed = false,
|
||||
onPointerDown: (event) {
|
||||
lastPressPosition = event.localPosition;
|
||||
if (event.buttons == kPrimaryMouseButton) {
|
||||
isPressed = true;
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
height: 530,
|
||||
@@ -495,7 +501,7 @@ class _ScriptListState extends State<ScriptList> {
|
||||
onDoubleTap: () => showEdit(index),
|
||||
onSecondaryTapDown: (details) => showMenus(details, index),
|
||||
onHover: (hover) {
|
||||
if (isPress && !selected.contains(index)) {
|
||||
if (isPressed && !selected.contains(index)) {
|
||||
setState(() {
|
||||
selected.add(index);
|
||||
});
|
||||
@@ -519,7 +525,7 @@ class _ScriptListState extends State<ScriptList> {
|
||||
color: selected.contains(index)
|
||||
? primaryColor.withOpacity(0.8)
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
: null,
|
||||
height: 30,
|
||||
padding: const EdgeInsets.all(5),
|
||||
|
||||
@@ -55,6 +55,7 @@ class _SettingState extends State<Setting> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return MenuAnchor(
|
||||
builder: (context, controller, child) {
|
||||
return IconButton(
|
||||
@@ -71,7 +72,7 @@ class _SettingState extends State<Setting> {
|
||||
menuChildren: [
|
||||
_ProxyMenu(proxyServer: widget.proxyServer),
|
||||
item(localizations.domainFilter, onPressed: hostFilter),
|
||||
item('Hosts', onPressed: hosts),
|
||||
item(localizations.hosts, onPressed: hosts),
|
||||
item(localizations.requestBlock, onPressed: showRequestBlock),
|
||||
item(localizations.requestRewrite, onPressed: requestRewrite),
|
||||
item(localizations.script,
|
||||
|
||||
@@ -247,11 +247,13 @@ class _SslState extends State<SslWidget> {
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
|
||||
title: Row(children: [
|
||||
const Expanded(child: SizedBox()),
|
||||
Text(isCN ? "电脑HTTPS抓包配置" : "Computer HTTPS Packet Capture Configuration",
|
||||
style: const TextStyle(fontSize: 16)),
|
||||
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
const Expanded(child: SizedBox()),
|
||||
Align(alignment: Alignment.topRight, child: CloseButton())
|
||||
]),
|
||||
alignment: Alignment.center,
|
||||
children: list);
|
||||
@@ -265,10 +267,13 @@ class _SslState extends State<SslWidget> {
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
|
||||
title: Row(children: [
|
||||
Text("iOS ${localizations.caInstallGuide}", style: const TextStyle(fontSize: 16)),
|
||||
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
|
||||
const Expanded(child: SizedBox()),
|
||||
Text("iOS ${localizations.caInstallGuide}",
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
const Expanded(child: SizedBox()),
|
||||
Align(alignment: Alignment.topRight, child: CloseButton())
|
||||
]),
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
@@ -315,8 +320,11 @@ class _SslState extends State<SslWidget> {
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
|
||||
contentPadding: const EdgeInsets.all(5),
|
||||
title: Row(children: [
|
||||
Text("Android ${localizations.caInstallGuide}", style: const TextStyle(fontSize: 16)),
|
||||
const Expanded(child: Align(alignment: Alignment.topRight, child: CloseButton()))
|
||||
const Expanded(child: SizedBox()),
|
||||
Text("Android ${localizations.caInstallGuide}",
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
const Expanded(child: SizedBox()),
|
||||
Align(alignment: Alignment.topRight, child: CloseButton())
|
||||
]),
|
||||
content: SizedBox(
|
||||
width: 600,
|
||||
|
||||
@@ -53,9 +53,10 @@ class _ToolbarState extends State<Toolbar> {
|
||||
}
|
||||
|
||||
bool onKeyEvent(KeyEvent event) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
|
||||
if (HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.escape)) {
|
||||
if (ModalRoute.of(context)?.isCurrent == false) {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.maybePop(context);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ class _DomainListState extends State<DomainList> {
|
||||
color: selected.contains(index)
|
||||
? primaryColor.withOpacity(0.8)
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
: null,
|
||||
height: 38,
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
|
||||
@@ -76,7 +76,7 @@ class _RequestBlockState extends State<MobileRequestBlock> {
|
||||
onLongPress: () => showMenus(index),
|
||||
onTap: () => showEdit(index),
|
||||
child: Container(
|
||||
color: index.isEven ? Colors.grey.withOpacity(0.1) : null,
|
||||
color: index.isEven ? Colors.grey.withOpacity(0.15) : null,
|
||||
height: 38,
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
|
||||
@@ -263,7 +263,7 @@ class _RequestRuleListState extends State<RequestRuleList> {
|
||||
color: selected.contains(index)
|
||||
? primaryColor.withOpacity(0.8)
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
: null,
|
||||
height: 45,
|
||||
padding: const EdgeInsets.all(5),
|
||||
|
||||
@@ -395,7 +395,7 @@ class _UpdateListState extends State<UpdateList> {
|
||||
color: selected == index
|
||||
? primaryColor
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
: null,
|
||||
constraints: const BoxConstraints(minHeight: 38, maxHeight: 45),
|
||||
padding: const EdgeInsets.all(5),
|
||||
|
||||
@@ -610,7 +610,7 @@ class _ScriptListState extends State<ScriptList> {
|
||||
color: selected.contains(index)
|
||||
? primaryColor.withOpacity(0.8)
|
||||
: index.isEven
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
? Colors.grey.withOpacity(0.15)
|
||||
: null,
|
||||
height: 45,
|
||||
padding: const EdgeInsets.all(5),
|
||||
|
||||
Reference in New Issue
Block a user