mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-23 16:45:27 +01:00
opt.
This commit is contained in:
@@ -166,9 +166,9 @@ void showSnippetDialog(
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(s.chooseDestination),
|
||||
child: buildPicker(
|
||||
provider.snippets.map((e) => Text(e.name)).toList(),
|
||||
(idx) => snippet = provider.snippets[idx],
|
||||
child: Picker(
|
||||
items: provider.snippets.map((e) => Text(e.name)).toList(),
|
||||
onSelected: (idx) => snippet = provider.snippets[idx],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
@@ -5,28 +5,29 @@ import 'package:toolbox/locator.dart';
|
||||
|
||||
class PrivateKeyProvider extends BusyProvider {
|
||||
List<PrivateKeyInfo> get infos => _infos;
|
||||
final _store = locator<PrivateKeyStore>();
|
||||
late List<PrivateKeyInfo> _infos;
|
||||
|
||||
void loadData() {
|
||||
_infos = locator<PrivateKeyStore>().fetch();
|
||||
_infos = _store.fetch();
|
||||
}
|
||||
|
||||
void addInfo(PrivateKeyInfo info) {
|
||||
_infos.add(info);
|
||||
locator<PrivateKeyStore>().put(info);
|
||||
_store.put(info);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void delInfo(PrivateKeyInfo info) {
|
||||
_infos.removeWhere((e) => e.id == info.id);
|
||||
locator<PrivateKeyStore>().delete(info);
|
||||
_store.delete(info);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateInfo(PrivateKeyInfo old, PrivateKeyInfo newInfo) {
|
||||
final idx = _infos.indexWhere((e) => e.id == old.id);
|
||||
_infos[idx] = newInfo;
|
||||
locator<PrivateKeyStore>().put(newInfo);
|
||||
_store.put(newInfo);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,15 +22,18 @@ typedef ServersMap = Map<String, Server>;
|
||||
class ServerProvider extends BusyProvider {
|
||||
final ServersMap _servers = {};
|
||||
ServersMap get servers => _servers;
|
||||
|
||||
final _limiter = TryLimiter();
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
final _logger = Logger('SERVER');
|
||||
|
||||
final _store = locator<ServerStore>();
|
||||
|
||||
Future<void> loadLocalData() async {
|
||||
setBusyState(true);
|
||||
final infos = locator<ServerStore>().fetch();
|
||||
final infos = _store.fetch();
|
||||
for (final info in infos) {
|
||||
_servers[info.id] = genServer(info);
|
||||
}
|
||||
@@ -103,20 +106,20 @@ class ServerProvider extends BusyProvider {
|
||||
void addServer(ServerPrivateInfo spi) {
|
||||
_servers[spi.id] = genServer(spi);
|
||||
notifyListeners();
|
||||
locator<ServerStore>().put(spi);
|
||||
_store.put(spi);
|
||||
refreshData(spi: spi);
|
||||
}
|
||||
|
||||
void delServer(String id) {
|
||||
_servers.remove(id);
|
||||
notifyListeners();
|
||||
locator<ServerStore>().delete(id);
|
||||
_store.delete(id);
|
||||
}
|
||||
|
||||
Future<void> updateServer(
|
||||
ServerPrivateInfo old, ServerPrivateInfo newSpi) async {
|
||||
_servers.remove(old.id);
|
||||
locator<ServerStore>().update(old, newSpi);
|
||||
_store.update(old, newSpi);
|
||||
_servers[newSpi.id] = genServer(newSpi);
|
||||
_servers[newSpi.id]?.client = await genClient(newSpi);
|
||||
notifyListeners();
|
||||
|
||||
@@ -7,23 +7,24 @@ import 'package:toolbox/locator.dart';
|
||||
|
||||
class SnippetProvider extends BusyProvider {
|
||||
List<Snippet> get snippets => _snippets;
|
||||
final _store = locator<SnippetStore>();
|
||||
late List<Snippet> _snippets;
|
||||
|
||||
void loadData() {
|
||||
_snippets = locator<SnippetStore>().fetch();
|
||||
_snippets = _store.fetch();
|
||||
}
|
||||
|
||||
void add(Snippet snippet) {
|
||||
if (have(snippet)) return;
|
||||
_snippets.add(snippet);
|
||||
locator<SnippetStore>().put(snippet);
|
||||
_store.put(snippet);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void del(Snippet snippet) {
|
||||
if (!have(snippet)) return;
|
||||
_snippets.removeAt(index(snippet));
|
||||
locator<SnippetStore>().delete(snippet);
|
||||
_store.delete(snippet);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -38,7 +39,7 @@ class SnippetProvider extends BusyProvider {
|
||||
void update(Snippet old, Snippet newOne) {
|
||||
if (!have(old)) return;
|
||||
_snippets[index(old)] = newOne;
|
||||
locator<SnippetStore>().put(newOne);
|
||||
_store.put(newOne);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
class BuildData {
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 287;
|
||||
static const int build = 288;
|
||||
static const String engine = "3.7.11";
|
||||
static const String buildAt = "2023-05-07 18:25:45.312302";
|
||||
static const int modifications = 2;
|
||||
static const String buildAt = "2023-05-07 20:47:03.124092";
|
||||
static const int modifications = 1;
|
||||
}
|
||||
|
||||
@@ -33,3 +33,15 @@ const popMenuChild = Padding(
|
||||
size: 21,
|
||||
),
|
||||
);
|
||||
|
||||
const centerLoading = Center(child: CircularProgressIndicator());
|
||||
|
||||
const centerSizedLoading = SizedBox(
|
||||
width: 77,
|
||||
height: 77,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
|
||||
const loadingIcon = IconButton(onPressed: null, icon: centerLoading);
|
||||
|
||||
@@ -6,6 +6,7 @@ const issueUrl = '$myGithub/flutter_server_box/issues';
|
||||
|
||||
// Thanks
|
||||
const thanksMap = {
|
||||
'its-tom': 'https://github.com/its-tom',
|
||||
'RainSunMe': 'https://github.com/RainSunMe',
|
||||
'fecture': 'https://github.com/fecture',
|
||||
'Tao173': 'https://github.com/Tao173',
|
||||
|
||||
@@ -32,74 +32,78 @@ class BackupPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final media = MediaQuery.of(context);
|
||||
final s = S.of(context)!;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(s.backupAndRestore, style: textSize18),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(37),
|
||||
child: Text(
|
||||
s.backupTip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 107,
|
||||
),
|
||||
_buildCard(s.restore, Icons.download, media, () async {
|
||||
final path = await pickOneFile();
|
||||
if (path == null) {
|
||||
showSnackBar(context, Text(s.notSelected));
|
||||
return;
|
||||
}
|
||||
final file = File(path);
|
||||
if (!file.existsSync()) {
|
||||
showSnackBar(context, Text(s.fileNotExist(path)));
|
||||
return;
|
||||
}
|
||||
final text = await file.readAsString();
|
||||
_import(text, context, s);
|
||||
}),
|
||||
const SizedBox(height: 17),
|
||||
const SizedBox(
|
||||
width: 37,
|
||||
child: Divider(),
|
||||
),
|
||||
const SizedBox(height: 17),
|
||||
_buildCard(
|
||||
s.backup,
|
||||
Icons.file_upload,
|
||||
media,
|
||||
() async {
|
||||
final result = _diyEncrtpt(
|
||||
json.encode(
|
||||
Backup(
|
||||
backupFormatVersion,
|
||||
DateTime.now().toString().split('.').first,
|
||||
_server.fetch(),
|
||||
_snippet.fetch(),
|
||||
_privateKey.fetch(),
|
||||
_dockerHosts.fetch(),
|
||||
),
|
||||
),
|
||||
);
|
||||
final path = '${(await docDir).path}/srvbox_bak.json';
|
||||
await File(path).writeAsString(result);
|
||||
await shareFiles(context, [path]);
|
||||
},
|
||||
)
|
||||
],
|
||||
)),
|
||||
body: _buildBody(context, s),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context, S s) {
|
||||
final media = MediaQuery.of(context);
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(37),
|
||||
child: Text(
|
||||
s.backupTip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 107,
|
||||
),
|
||||
_buildCard(s.restore, Icons.download, media, () async {
|
||||
final path = await pickOneFile();
|
||||
if (path == null) {
|
||||
showSnackBar(context, Text(s.notSelected));
|
||||
return;
|
||||
}
|
||||
final file = File(path);
|
||||
if (!file.existsSync()) {
|
||||
showSnackBar(context, Text(s.fileNotExist(path)));
|
||||
return;
|
||||
}
|
||||
final text = await file.readAsString();
|
||||
_import(text, context, s);
|
||||
}),
|
||||
const SizedBox(height: 17),
|
||||
const SizedBox(
|
||||
width: 37,
|
||||
child: Divider(),
|
||||
),
|
||||
const SizedBox(height: 17),
|
||||
_buildCard(
|
||||
s.backup,
|
||||
Icons.file_upload,
|
||||
media,
|
||||
() async {
|
||||
final result = _diyEncrtpt(
|
||||
json.encode(
|
||||
Backup(
|
||||
backupFormatVersion,
|
||||
DateTime.now().toString().split('.').first,
|
||||
_server.fetch(),
|
||||
_snippet.fetch(),
|
||||
_privateKey.fetch(),
|
||||
_dockerHosts.fetch(),
|
||||
),
|
||||
),
|
||||
);
|
||||
final path = '${(await docDir).path}/srvbox_bak.json';
|
||||
await File(path).writeAsString(result);
|
||||
await shareFiles(context, [path]);
|
||||
},
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildCard(String text, IconData icon, MediaQueryData media,
|
||||
FutureOr Function() onTap) {
|
||||
final textColor = primaryColor.isBrightColor ? Colors.black : Colors.white;
|
||||
|
||||
@@ -89,7 +89,7 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
Widget _buildInputTop() {
|
||||
return SizedBox(
|
||||
height: _media.size.height * 0.33,
|
||||
child: buildInput(controller: _textEditingController),
|
||||
child: Input(controller: _textEditingController),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ class _ConvertPageState extends State<ConvertPage>
|
||||
Widget _buildResult() {
|
||||
return SizedBox(
|
||||
height: _media.size.height * 0.33,
|
||||
child: buildInput(controller: _textEditingControllerResult),
|
||||
child: Input(controller: _textEditingControllerResult),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import '../../data/res/ui.dart';
|
||||
import '../../data/res/url.dart';
|
||||
import '../../data/store/docker.dart';
|
||||
import '../../locator.dart';
|
||||
import '../widget/center_loading.dart';
|
||||
import '../widget/dropdown_menu.dart';
|
||||
import '../widget/round_rect_card.dart';
|
||||
import '../widget/two_line_text.dart';
|
||||
@@ -95,26 +94,23 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildInput(
|
||||
Input(
|
||||
type: TextInputType.text,
|
||||
label: _s.dockerImage,
|
||||
hint: 'xxx:1.1',
|
||||
controller: imageCtrl,
|
||||
autoCorrect: false,
|
||||
),
|
||||
buildInput(
|
||||
Input(
|
||||
type: TextInputType.text,
|
||||
controller: nameCtrl,
|
||||
label: _s.dockerContainerName,
|
||||
hint: 'xxx',
|
||||
autoCorrect: false,
|
||||
),
|
||||
buildInput(
|
||||
Input(
|
||||
type: TextInputType.text,
|
||||
controller: argsCtrl,
|
||||
label: _s.extraArgs,
|
||||
hint: '-p 2222:22 -v ~/.xxx/:/xxx',
|
||||
autoCorrect: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -206,7 +202,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
await showRoundDialog(
|
||||
context: context,
|
||||
title: Text(widget.spi.user),
|
||||
child: buildInput(
|
||||
child: Input(
|
||||
controller: _textController,
|
||||
type: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
@@ -379,9 +375,8 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
||||
await showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.dockerEditHost),
|
||||
child: buildInput(
|
||||
child: Input(
|
||||
maxLines: 1,
|
||||
autoCorrect: false,
|
||||
controller:
|
||||
TextEditingController(text: 'unix:///run/user/1000/docker.sock'),
|
||||
onSubmitted: (value) {
|
||||
|
||||
@@ -57,7 +57,7 @@ class _PingPageState extends State<PingPage>
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 13),
|
||||
buildInput(
|
||||
Input(
|
||||
controller: _textEditingController,
|
||||
hint: s.inputDomainHere,
|
||||
maxLines: 1,
|
||||
|
||||
@@ -12,7 +12,6 @@ import '../../data/provider/pkg.dart';
|
||||
import '../../data/provider/server.dart';
|
||||
import '../../data/res/ui.dart';
|
||||
import '../../locator.dart';
|
||||
import '../widget/center_loading.dart';
|
||||
import '../widget/round_rect_card.dart';
|
||||
import '../widget/two_line_text.dart';
|
||||
|
||||
@@ -31,7 +30,7 @@ class _PkgManagePageState extends State<PkgManagePage>
|
||||
final _scrollController = ScrollController();
|
||||
final _scrollControllerUpdate = ScrollController();
|
||||
final _textController = TextEditingController();
|
||||
final _aptProvider = locator<PkgProvider>();
|
||||
final _pkgProvider = locator<PkgProvider>();
|
||||
late S _s;
|
||||
|
||||
@override
|
||||
@@ -44,7 +43,7 @@ class _PkgManagePageState extends State<PkgManagePage>
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
locator<PkgProvider>().clear();
|
||||
_pkgProvider.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -57,7 +56,7 @@ class _PkgManagePageState extends State<PkgManagePage>
|
||||
return;
|
||||
}
|
||||
|
||||
_aptProvider.init(
|
||||
_pkgProvider.init(
|
||||
si.client!,
|
||||
si.status.sysVer.dist,
|
||||
() =>
|
||||
@@ -67,7 +66,21 @@ class _PkgManagePageState extends State<PkgManagePage>
|
||||
onPwdRequest,
|
||||
widget.spi.user,
|
||||
);
|
||||
_aptProvider.refresh();
|
||||
_pkgProvider.refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<PkgProvider>(builder: (_, pkg, __) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: _s.pkg, down: widget.spi.name),
|
||||
),
|
||||
body: _buildBody(pkg),
|
||||
floatingActionButton: _buildFAB(pkg),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void onSubmitted() {
|
||||
@@ -92,7 +105,7 @@ class _PkgManagePageState extends State<PkgManagePage>
|
||||
await showRoundDialog(
|
||||
context: context,
|
||||
title: Text(widget.spi.user),
|
||||
child: buildInput(
|
||||
child: Input(
|
||||
controller: _textController,
|
||||
type: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
@@ -118,20 +131,6 @@ class _PkgManagePageState extends State<PkgManagePage>
|
||||
return _textController.text.trim();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<PkgProvider>(builder: (_, pkg, __) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: TwoLineText(up: _s.pkg, down: widget.spi.name),
|
||||
),
|
||||
body: _buildBody(pkg),
|
||||
floatingActionButton: _buildFAB(pkg),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildFAB(PkgProvider pkg) {
|
||||
if (pkg.isBusy || (pkg.upgradeable?.isEmpty ?? true)) {
|
||||
return const SizedBox();
|
||||
|
||||
@@ -61,128 +61,138 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_s.edit, style: textSize18),
|
||||
actions: [
|
||||
widget.info != null
|
||||
? IconButton(
|
||||
tooltip: _s.delete,
|
||||
onPressed: () {
|
||||
_provider.delInfo(widget.info!);
|
||||
context.pop();
|
||||
},
|
||||
icon: const Icon(Icons.delete))
|
||||
: const SizedBox()
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(13),
|
||||
children: [
|
||||
buildInput(
|
||||
controller: _nameController,
|
||||
type: TextInputType.text,
|
||||
node: _nameNode,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_keyNode),
|
||||
label: _s.name,
|
||||
icon: Icons.info,
|
||||
),
|
||||
buildInput(
|
||||
controller: _keyController,
|
||||
autoCorrect: false,
|
||||
minLines: 3,
|
||||
maxLines: 10,
|
||||
type: TextInputType.text,
|
||||
node: _keyNode,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_pwdNode),
|
||||
label: _s.privateKey,
|
||||
icon: Icons.vpn_key,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final path = await pickOneFile();
|
||||
if (path == null) {
|
||||
showSnackBar(context, const Text('path is null'));
|
||||
return;
|
||||
}
|
||||
appBar: _buildAppBar(),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: _buildFAB(),
|
||||
);
|
||||
}
|
||||
|
||||
final file = File(path);
|
||||
if (!file.existsSync()) {
|
||||
showSnackBar(context, Text(_s.fileNotExist(path)));
|
||||
return;
|
||||
}
|
||||
final size = (await file.stat()).size;
|
||||
if (size > privateKeyMaxSize) {
|
||||
showSnackBar(
|
||||
context,
|
||||
Text(
|
||||
_s.fileTooLarge(
|
||||
path,
|
||||
size.convertBytes,
|
||||
privateKeyMaxSize.convertBytes,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
return AppBar(
|
||||
title: Text(_s.edit, style: textSize18),
|
||||
actions: [
|
||||
widget.info != null
|
||||
? IconButton(
|
||||
tooltip: _s.delete,
|
||||
onPressed: () {
|
||||
_provider.delInfo(widget.info!);
|
||||
context.pop();
|
||||
},
|
||||
icon: const Icon(Icons.delete))
|
||||
: const SizedBox()
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_keyController.text = await file.readAsString();
|
||||
},
|
||||
child: Text(_s.pickFile),
|
||||
),
|
||||
buildInput(
|
||||
controller: _pwdController,
|
||||
autoCorrect: false,
|
||||
type: TextInputType.text,
|
||||
node: _pwdNode,
|
||||
obscureText: true,
|
||||
label: _s.pwd,
|
||||
icon: Icons.password,
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
|
||||
_loading
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
tooltip: _s.save,
|
||||
onPressed: () async {
|
||||
final name = _nameController.text;
|
||||
final key = _keyController.text.trim();
|
||||
final pwd = _pwdController.text;
|
||||
if (name.isEmpty || key.isEmpty) {
|
||||
showSnackBar(context, Text(_s.fieldMustNotEmpty));
|
||||
return;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
Widget _buildFAB() {
|
||||
return FloatingActionButton(
|
||||
tooltip: _s.save,
|
||||
onPressed: () async {
|
||||
final name = _nameController.text;
|
||||
final key = _keyController.text.trim();
|
||||
final pwd = _pwdController.text;
|
||||
if (name.isEmpty || key.isEmpty) {
|
||||
showSnackBar(context, Text(_s.fieldMustNotEmpty));
|
||||
return;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
setState(() {
|
||||
_loading = const SizedBox(
|
||||
height: 50,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
});
|
||||
final info = PrivateKeyInfo(name, key, '');
|
||||
bool haveErr = false;
|
||||
try {
|
||||
info.privateKey = await compute(decyptPem, [key, pwd]);
|
||||
} catch (e) {
|
||||
showSnackBar(context, Text(e.toString()));
|
||||
haveErr = true;
|
||||
} finally {
|
||||
setState(() {
|
||||
_loading = const SizedBox(
|
||||
height: 50,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
_loading = const SizedBox();
|
||||
});
|
||||
final info = PrivateKeyInfo(name, key, '');
|
||||
bool haveErr = false;
|
||||
try {
|
||||
info.privateKey = await compute(decyptPem, [key, pwd]);
|
||||
} catch (e) {
|
||||
showSnackBar(context, Text(e.toString()));
|
||||
haveErr = true;
|
||||
} finally {
|
||||
setState(() {
|
||||
_loading = const SizedBox();
|
||||
});
|
||||
}
|
||||
if (haveErr) return;
|
||||
if (widget.info != null) {
|
||||
_provider.updateInfo(widget.info!, info);
|
||||
} else {
|
||||
_provider.addInfo(info);
|
||||
}
|
||||
context.pop();
|
||||
},
|
||||
child: const Icon(Icons.save),
|
||||
),
|
||||
}
|
||||
if (haveErr) return;
|
||||
if (widget.info != null) {
|
||||
_provider.updateInfo(widget.info!, info);
|
||||
} else {
|
||||
_provider.addInfo(info);
|
||||
}
|
||||
context.pop();
|
||||
},
|
||||
child: const Icon(Icons.save),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(13),
|
||||
children: [
|
||||
Input(
|
||||
controller: _nameController,
|
||||
type: TextInputType.text,
|
||||
node: _nameNode,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_keyNode),
|
||||
label: _s.name,
|
||||
icon: Icons.info,
|
||||
),
|
||||
Input(
|
||||
controller: _keyController,
|
||||
minLines: 3,
|
||||
maxLines: 10,
|
||||
type: TextInputType.text,
|
||||
node: _keyNode,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_pwdNode),
|
||||
label: _s.privateKey,
|
||||
icon: Icons.vpn_key,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final path = await pickOneFile();
|
||||
if (path == null) {
|
||||
showSnackBar(context, const Text('path is null'));
|
||||
return;
|
||||
}
|
||||
|
||||
final file = File(path);
|
||||
if (!file.existsSync()) {
|
||||
showSnackBar(context, Text(_s.fileNotExist(path)));
|
||||
return;
|
||||
}
|
||||
final size = (await file.stat()).size;
|
||||
if (size > privateKeyMaxSize) {
|
||||
showSnackBar(
|
||||
context,
|
||||
Text(
|
||||
_s.fileTooLarge(
|
||||
path,
|
||||
size.convertBytes,
|
||||
privateKeyMaxSize.convertBytes,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_keyController.text = await file.readAsString();
|
||||
},
|
||||
child: Text(_s.pickFile),
|
||||
),
|
||||
Input(
|
||||
controller: _pwdController,
|
||||
type: TextInputType.text,
|
||||
node: _pwdNode,
|
||||
obscureText: true,
|
||||
label: _s.pwd,
|
||||
icon: Icons.password,
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
|
||||
_loading
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,41 +30,44 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> {
|
||||
appBar: AppBar(
|
||||
title: Text(_s.privateKey, style: textSize18),
|
||||
),
|
||||
body: Consumer<PrivateKeyProvider>(
|
||||
builder: (_, key, __) {
|
||||
if (key.infos.isEmpty) {
|
||||
return Center(
|
||||
child: Text(_s.noSavedPrivateKey),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(13),
|
||||
itemCount: key.infos.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return RoundRectCard(
|
||||
ListTile(
|
||||
title: Text(
|
||||
key.infos[idx].id,
|
||||
),
|
||||
trailing: TextButton(
|
||||
onPressed: () => AppRoute(
|
||||
PrivateKeyEditPage(info: key.infos[idx]),
|
||||
'private key edit page',
|
||||
).go(context),
|
||||
child: Text(_s.edit),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () =>
|
||||
AppRoute(const PrivateKeyEditPage(), 'private key edit page')
|
||||
.go(context),
|
||||
onPressed: () => AppRoute(
|
||||
const PrivateKeyEditPage(),
|
||||
'private key edit page',
|
||||
).go(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return Consumer<PrivateKeyProvider>(
|
||||
builder: (_, key, __) {
|
||||
if (key.infos.isEmpty) {
|
||||
return Center(
|
||||
child: Text(_s.noSavedPrivateKey),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(13),
|
||||
itemCount: key.infos.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return RoundRectCard(
|
||||
ListTile(
|
||||
title: Text(key.infos[idx].id),
|
||||
trailing: TextButton(
|
||||
onPressed: () => AppRoute(
|
||||
PrivateKeyEditPage(info: key.infos[idx]),
|
||||
'private key edit page',
|
||||
).go(context),
|
||||
child: Text(_s.edit),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildInput(
|
||||
Input(
|
||||
controller: _nameController,
|
||||
type: TextInputType.text,
|
||||
node: _nameFocus,
|
||||
@@ -117,17 +117,16 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
label: _s.name,
|
||||
icon: Icons.info,
|
||||
),
|
||||
buildInput(
|
||||
Input(
|
||||
controller: _ipController,
|
||||
type: TextInputType.text,
|
||||
onSubmitted: (_) => _focusScope.requestFocus(_portFocus),
|
||||
node: _ipFocus,
|
||||
autoCorrect: false,
|
||||
label: _s.host,
|
||||
icon: Icons.storage,
|
||||
hint: 'example.com',
|
||||
),
|
||||
buildInput(
|
||||
Input(
|
||||
controller: _portController,
|
||||
type: TextInputType.number,
|
||||
node: _portFocus,
|
||||
@@ -136,11 +135,10 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
icon: Icons.format_list_numbered,
|
||||
hint: '22',
|
||||
),
|
||||
buildInput(
|
||||
Input(
|
||||
controller: _usernameController,
|
||||
type: TextInputType.text,
|
||||
node: _usernameFocus,
|
||||
autoCorrect: false,
|
||||
label: _s.user,
|
||||
icon: Icons.account_box,
|
||||
hint: 'root',
|
||||
@@ -158,7 +156,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
],
|
||||
),
|
||||
!usePublicKey
|
||||
? buildInput(
|
||||
? Input(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
type: TextInputType.text,
|
||||
|
||||
@@ -62,38 +62,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async =>
|
||||
await _serverProvider.refreshData(onlyFailed: true),
|
||||
child: Consumer<ServerProvider>(
|
||||
builder: (_, pro, __) {
|
||||
if (pro.servers.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_s.serverTabEmpty,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
final keys = pro.servers.keys.toList();
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(7, 10, 7, 7),
|
||||
controller: ScrollController(),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemBuilder: (ctx, idx) {
|
||||
if (idx == pro.servers.length) {
|
||||
return SizedBox(height: _media.padding.bottom);
|
||||
}
|
||||
return _buildEachServerCard(pro.servers[keys[idx]]);
|
||||
},
|
||||
itemCount: pro.servers.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(
|
||||
height: 3,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => AppRoute(
|
||||
const ServerEditPage(),
|
||||
@@ -106,6 +75,41 @@ class _ServerPageState extends State<ServerPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async =>
|
||||
await _serverProvider.refreshData(onlyFailed: true),
|
||||
child: Consumer<ServerProvider>(
|
||||
builder: (_, pro, __) {
|
||||
if (pro.servers.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_s.serverTabEmpty,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
final keys = pro.servers.keys.toList();
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(7, 10, 7, 7),
|
||||
controller: ScrollController(),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemBuilder: (ctx, idx) {
|
||||
if (idx == pro.servers.length) {
|
||||
return SizedBox(height: _media.padding.bottom);
|
||||
}
|
||||
return _buildEachServerCard(pro.servers[keys[idx]]);
|
||||
},
|
||||
itemCount: pro.servers.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(
|
||||
height: 3,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEachServerCard(Server? si) {
|
||||
if (si == null) {
|
||||
return const SizedBox();
|
||||
|
||||
@@ -8,7 +8,6 @@ import '../../../core/utils/ui.dart';
|
||||
import '../../../data/model/sftp/download_status.dart';
|
||||
import '../../../data/provider/sftp_download.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/center_loading.dart';
|
||||
import '../../widget/round_rect_card.dart';
|
||||
|
||||
class SFTPDownloadingPage extends StatefulWidget {
|
||||
|
||||
@@ -18,9 +18,9 @@ import '../../../data/model/sftp/download_item.dart';
|
||||
import '../../../data/provider/server.dart';
|
||||
import '../../../data/provider/sftp_download.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../data/store/private_key.dart';
|
||||
import '../../../locator.dart';
|
||||
import '../../widget/center_loading.dart';
|
||||
import '../../widget/fade_in.dart';
|
||||
import '../../widget/two_line_text.dart';
|
||||
import 'downloading.dart';
|
||||
@@ -144,7 +144,7 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildInput(
|
||||
Input(
|
||||
label: _s.path,
|
||||
hint: '/',
|
||||
onSubmitted: (value) => context.pop(value),
|
||||
@@ -378,7 +378,7 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.createFolder),
|
||||
child: buildInput(
|
||||
child: Input(
|
||||
controller: textController,
|
||||
label: _s.name,
|
||||
),
|
||||
@@ -422,7 +422,7 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.createFile),
|
||||
child: buildInput(
|
||||
child: Input(
|
||||
controller: textController,
|
||||
label: _s.name,
|
||||
),
|
||||
@@ -467,7 +467,7 @@ class _SFTPPageState extends State<SFTPPage> {
|
||||
showRoundDialog(
|
||||
context: context,
|
||||
title: Text(_s.rename),
|
||||
child: buildInput(
|
||||
child: Input(
|
||||
controller: textController,
|
||||
label: _s.name,
|
||||
),
|
||||
|
||||
@@ -57,47 +57,53 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
: const SizedBox()
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(13),
|
||||
children: [
|
||||
buildInput(
|
||||
controller: _nameController,
|
||||
type: TextInputType.text,
|
||||
onSubmitted: (_) =>
|
||||
FocusScope.of(context).requestFocus(_scriptNode),
|
||||
label: _s.name,
|
||||
icon: Icons.info,
|
||||
),
|
||||
buildInput(
|
||||
controller: _scriptController,
|
||||
autoCorrect: false,
|
||||
node: _scriptNode,
|
||||
minLines: 3,
|
||||
maxLines: 10,
|
||||
type: TextInputType.text,
|
||||
label: _s.snippet,
|
||||
icon: Icons.code,
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.send),
|
||||
onPressed: () {
|
||||
final name = _nameController.text;
|
||||
final script = _scriptController.text;
|
||||
if (name.isEmpty || script.isEmpty) {
|
||||
showSnackBar(context, Text(_s.fieldMustNotEmpty));
|
||||
return;
|
||||
}
|
||||
final snippet = Snippet(name, script);
|
||||
if (widget.snippet != null) {
|
||||
_provider.update(widget.snippet!, snippet);
|
||||
} else {
|
||||
_provider.add(snippet);
|
||||
}
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: _buildFAB(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFAB() {
|
||||
return FloatingActionButton(
|
||||
child: const Icon(Icons.send),
|
||||
onPressed: () {
|
||||
final name = _nameController.text;
|
||||
final script = _scriptController.text;
|
||||
if (name.isEmpty || script.isEmpty) {
|
||||
showSnackBar(context, Text(_s.fieldMustNotEmpty));
|
||||
return;
|
||||
}
|
||||
final snippet = Snippet(name, script);
|
||||
if (widget.snippet != null) {
|
||||
_provider.update(widget.snippet!, snippet);
|
||||
} else {
|
||||
_provider.add(snippet);
|
||||
}
|
||||
context.pop();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(13),
|
||||
children: [
|
||||
Input(
|
||||
controller: _nameController,
|
||||
type: TextInputType.text,
|
||||
onSubmitted: (_) => FocusScope.of(context).requestFocus(_scriptNode),
|
||||
label: _s.name,
|
||||
icon: Icons.info,
|
||||
),
|
||||
Input(
|
||||
controller: _scriptController,
|
||||
node: _scriptNode,
|
||||
minLines: 3,
|
||||
maxLines: 10,
|
||||
type: TextInputType.text,
|
||||
label: _s.snippet,
|
||||
icon: Icons.code,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,61 +76,6 @@ class _SSHPageState extends State<SSHPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _write(String p0) {
|
||||
_terminal.write('$p0\r\n');
|
||||
}
|
||||
|
||||
Future<void> initTerminal() async {
|
||||
_write('Connecting...\r\n');
|
||||
|
||||
_client = await genClient(
|
||||
widget.spi,
|
||||
onStatus: (p0) {
|
||||
switch (p0) {
|
||||
case GenSSHClientStatus.socket:
|
||||
_write('Destination: ${widget.spi.id}');
|
||||
return _write('Establishing socket...');
|
||||
case GenSSHClientStatus.key:
|
||||
return _write('Using private key to connect...');
|
||||
case GenSSHClientStatus.pwd:
|
||||
return _write('Sending password to auth...');
|
||||
}
|
||||
},
|
||||
);
|
||||
_write('Connected\r\n');
|
||||
_write('Terminal size: ${_terminal.viewWidth}x${_terminal.viewHeight}\r\n');
|
||||
_write('Starting shell...\r\n');
|
||||
|
||||
final session = await _client!.shell(
|
||||
pty: SSHPtyConfig(
|
||||
width: _terminal.viewWidth,
|
||||
height: _terminal.viewHeight,
|
||||
),
|
||||
);
|
||||
|
||||
_terminal.buffer.clear();
|
||||
_terminal.buffer.setCursor(0, 0);
|
||||
|
||||
_terminal.onOutput = (data) {
|
||||
session.write(utf8.encode(data) as Uint8List);
|
||||
};
|
||||
|
||||
_listen(session.stdout);
|
||||
_listen(session.stderr);
|
||||
|
||||
await session.done;
|
||||
if (mounted) {
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void _listen(Stream<Uint8List> stream) {
|
||||
stream
|
||||
.cast<List<int>>()
|
||||
.transform(const Utf8Decoder())
|
||||
.listen(_terminal.write);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = Scaffold(
|
||||
@@ -357,4 +302,59 @@ class _SSHPageState extends State<SSHPage> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _write(String p0) {
|
||||
_terminal.write('$p0\r\n');
|
||||
}
|
||||
|
||||
Future<void> initTerminal() async {
|
||||
_write('Connecting...\r\n');
|
||||
|
||||
_client = await genClient(
|
||||
widget.spi,
|
||||
onStatus: (p0) {
|
||||
switch (p0) {
|
||||
case GenSSHClientStatus.socket:
|
||||
_write('Destination: ${widget.spi.id}');
|
||||
return _write('Establishing socket...');
|
||||
case GenSSHClientStatus.key:
|
||||
return _write('Using private key to connect...');
|
||||
case GenSSHClientStatus.pwd:
|
||||
return _write('Sending password to auth...');
|
||||
}
|
||||
},
|
||||
);
|
||||
_write('Connected\r\n');
|
||||
_write('Terminal size: ${_terminal.viewWidth}x${_terminal.viewHeight}\r\n');
|
||||
_write('Starting shell...\r\n');
|
||||
|
||||
final session = await _client!.shell(
|
||||
pty: SSHPtyConfig(
|
||||
width: _terminal.viewWidth,
|
||||
height: _terminal.viewHeight,
|
||||
),
|
||||
);
|
||||
|
||||
_terminal.buffer.clear();
|
||||
_terminal.buffer.setCursor(0, 0);
|
||||
|
||||
_terminal.onOutput = (data) {
|
||||
session.write(utf8.encode(data) as Uint8List);
|
||||
};
|
||||
|
||||
_listen(session.stdout);
|
||||
_listen(session.stderr);
|
||||
|
||||
await session.done;
|
||||
if (mounted) {
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void _listen(Stream<Uint8List> stream) {
|
||||
stream
|
||||
.cast<List<int>>()
|
||||
.transform(const Utf8Decoder())
|
||||
.listen(_terminal.write);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const centerLoading = Center(child: CircularProgressIndicator());
|
||||
|
||||
const centerSizedLoading = SizedBox(
|
||||
width: 77,
|
||||
height: 77,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
|
||||
final loadingIcon = IconButton(onPressed: () {}, icon: centerLoading);
|
||||
@@ -1,38 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
Widget buildInput({
|
||||
TextEditingController? controller,
|
||||
int maxLines = 1,
|
||||
int? minLines,
|
||||
String? hint,
|
||||
String? label,
|
||||
Function(String)? onSubmitted,
|
||||
bool obscureText = false,
|
||||
IconData? icon,
|
||||
TextInputType? type,
|
||||
FocusNode? node,
|
||||
bool autoCorrect = true,
|
||||
}) {
|
||||
return RoundRectCard(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||
child: TextField(
|
||||
maxLines: maxLines,
|
||||
minLines: minLines,
|
||||
onSubmitted: onSubmitted,
|
||||
keyboardType: type,
|
||||
focusNode: node,
|
||||
autocorrect: autoCorrect,
|
||||
decoration: InputDecoration(
|
||||
label: label != null ? Text(label) : null,
|
||||
hintText: hint,
|
||||
icon: icon != null ? Icon(icon) : null,
|
||||
border: InputBorder.none,
|
||||
import 'round_rect_card.dart';
|
||||
|
||||
class Input extends StatelessWidget {
|
||||
final TextEditingController? controller;
|
||||
final int maxLines;
|
||||
final int? minLines;
|
||||
final String? hint;
|
||||
final String? label;
|
||||
final Function(String)? onSubmitted;
|
||||
final bool obscureText;
|
||||
final IconData? icon;
|
||||
final TextInputType? type;
|
||||
final FocusNode? node;
|
||||
final bool autoCorrect;
|
||||
final bool suggestiion;
|
||||
|
||||
const Input({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.maxLines = 1,
|
||||
this.minLines,
|
||||
this.hint,
|
||||
this.label,
|
||||
this.onSubmitted,
|
||||
this.obscureText = false,
|
||||
this.icon,
|
||||
this.type,
|
||||
this.node,
|
||||
this.autoCorrect = false,
|
||||
this.suggestiion = false,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoundRectCard(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||
child: TextField(
|
||||
maxLines: maxLines,
|
||||
minLines: minLines,
|
||||
onSubmitted: onSubmitted,
|
||||
keyboardType: type,
|
||||
focusNode: node,
|
||||
autocorrect: autoCorrect,
|
||||
enableSuggestions: suggestiion,
|
||||
decoration: InputDecoration(
|
||||
label: label != null ? Text(label!) : null,
|
||||
hintText: hint,
|
||||
icon: icon != null ? Icon(icon) : null,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
),
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget buildPicker(
|
||||
List<Widget> items,
|
||||
Function(int idx) onSelected, {
|
||||
double height = 157,
|
||||
}) {
|
||||
final pad = (height - 37) / 2;
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: pad,
|
||||
bottom: pad,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 37,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(7)),
|
||||
color: Colors.black12,
|
||||
class Picker extends StatelessWidget {
|
||||
final List<Widget> items;
|
||||
final Function(int idx) onSelected;
|
||||
final double height;
|
||||
|
||||
const Picker({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
this.height = 157,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pad = (height - 37) / 2;
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: pad,
|
||||
bottom: pad,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 37,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(7)),
|
||||
color: Colors.black12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListWheelScrollView.useDelegate(
|
||||
itemExtent: 37,
|
||||
diameterRatio: 2.7,
|
||||
controller: FixedExtentScrollController(initialItem: 0),
|
||||
onSelectedItemChanged: (idx) => onSelected(idx),
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
childDelegate: ListWheelChildBuilderDelegate(
|
||||
builder: (context, index) => Center(
|
||||
child: items[index],
|
||||
ListWheelScrollView.useDelegate(
|
||||
itemExtent: 37,
|
||||
diameterRatio: 2.7,
|
||||
controller: FixedExtentScrollController(initialItem: 0),
|
||||
onSelectedItemChanged: (idx) => onSelected(idx),
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
childDelegate: ListWheelChildBuilderDelegate(
|
||||
builder: (context, index) => Center(
|
||||
child: items[index],
|
||||
),
|
||||
childCount: items.length,
|
||||
),
|
||||
childCount: items.length,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user