for merge

This commit is contained in:
rustdesk
2022-05-12 16:50:30 +08:00
parent 9a3e6c63cd
commit 6de0fa781c
149 changed files with 0 additions and 357 deletions

View File

@@ -0,0 +1,81 @@
import 'package:dash_chat/dash_chat.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:provider/provider.dart';
import '../models/model.dart';
import 'home_page.dart';
ChatPage chatPage = ChatPage();
class ChatPage extends StatelessWidget implements PageShape {
@override
final title = translate("Chat");
@override
final icon = Icon(Icons.chat);
@override
final appBarActions = [
PopupMenuButton<int>(
icon: Icon(Icons.group),
itemBuilder: (context) {
final chatModel = FFI.chatModel;
final serverModel = FFI.serverModel;
return chatModel.messages.entries.map((entry) {
final id = entry.key;
final user = serverModel.clients[id]?.chatUser ?? chatModel.me;
return PopupMenuItem<int>(
child: Text("${user.name} ${user.uid}"),
value: id,
);
}).toList();
},
onSelected: (id) {
FFI.chatModel.changeCurrentID(id);
})
];
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: FFI.chatModel,
child: Container(
color: MyTheme.grayBg,
child: Consumer<ChatModel>(builder: (context, chatModel, child) {
final currentUser = chatModel.currentUser;
return Stack(
children: [
DashChat(
inputContainerStyle: BoxDecoration(color: Colors.white70),
sendOnEnter: false,
// if true,reload keyboard everytime,need fix
onSend: (chatMsg) {
chatModel.send(chatMsg);
},
user: chatModel.me,
messages: chatModel.messages[chatModel.currentID] ?? [],
// default scrollToBottom has bug https://github.com/fayeed/dash_chat/issues/53
scrollToBottom: false,
scrollController: chatModel.scroller,
),
chatModel.currentID == ChatModel.clientModeID
? SizedBox.shrink()
: Padding(
padding: EdgeInsets.all(12),
child: Row(
children: [
Icon(Icons.account_circle,
color: MyTheme.accent80),
SizedBox(width: 5),
Text(
"${currentUser.name ?? ""} ${currentUser.uid ?? ""}",
style: TextStyle(color: MyTheme.accent50),
),
],
)),
],
);
})));
}
}

View File

@@ -0,0 +1,342 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/pages/file_manager_page.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:async';
import '../common.dart';
import '../models/model.dart';
import 'home_page.dart';
import 'remote_page.dart';
import 'settings_page.dart';
import 'scan_page.dart';
class ConnectionPage extends StatefulWidget implements PageShape {
ConnectionPage({Key? key}) : super(key: key);
@override
final icon = Icon(Icons.connected_tv);
@override
final title = translate("Connection");
@override
final appBarActions = !isAndroid ? <Widget>[WebMenu()] : <Widget>[];
@override
_ConnectionPageState createState() => _ConnectionPageState();
}
class _ConnectionPageState extends State<ConnectionPage> {
final _idController = TextEditingController();
var _updateUrl = '';
var _menuPos;
@override
void initState() {
super.initState();
if (isAndroid) {
Timer(Duration(seconds: 5), () {
_updateUrl = FFI.getByName('software_update_url');
if (_updateUrl.isNotEmpty) setState(() {});
});
}
}
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
if (_idController.text.isEmpty) _idController.text = FFI.getId();
return SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
getUpdateUI(),
getSearchBarUI(),
Container(height: 12),
getPeers(),
]),
);
}
void onConnect() {
var id = _idController.text.trim();
connect(id);
}
void connect(String id, {bool isFileTransfer = false}) async {
if (id == '') return;
id = id.replaceAll(' ', '');
if (isFileTransfer) {
if (!await PermissionManager.check("file")) {
if (!await PermissionManager.request("file")) {
return;
}
}
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FileManagerPage(id: id),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => RemotePage(id: id),
),
);
}
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
}
Widget getUpdateUI() {
return _updateUrl.isEmpty
? SizedBox(height: 0)
: InkWell(
onTap: () async {
final url = _updateUrl + '.apk';
if (await canLaunch(url)) {
await launch(url);
}
},
child: Container(
alignment: AlignmentDirectional.center,
width: double.infinity,
color: Colors.pinkAccent,
padding: EdgeInsets.symmetric(vertical: 12),
child: Text(translate('Download new version'),
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.bold))));
}
Widget getSearchBarUI() {
var w = Padding(
padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0),
child: Container(
height: 84,
child: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Ink(
decoration: BoxDecoration(
color: MyTheme.white,
borderRadius: const BorderRadius.all(Radius.circular(13)),
),
child: Row(
children: <Widget>[
Expanded(
child: Container(
padding: const EdgeInsets.only(left: 16, right: 16),
child: TextField(
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
// keyboardType: TextInputType.number,
style: TextStyle(
fontFamily: 'WorkSans',
fontWeight: FontWeight.bold,
fontSize: 30,
color: MyTheme.idColor,
),
decoration: InputDecoration(
labelText: translate('Remote ID'),
// hintText: 'Enter your remote ID',
border: InputBorder.none,
helperStyle: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: MyTheme.darkGray,
),
labelStyle: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
letterSpacing: 0.2,
color: MyTheme.darkGray,
),
),
controller: _idController,
),
),
),
SizedBox(
width: 60,
height: 60,
child: IconButton(
icon: Icon(Icons.arrow_forward,
color: MyTheme.darkGray, size: 45),
onPressed: onConnect,
),
),
],
),
),
),
),
);
return Center(
child: Container(constraints: BoxConstraints(maxWidth: 600), child: w));
}
@override
void dispose() {
_idController.dispose();
super.dispose();
}
Widget getPlatformImage(String platform) {
platform = platform.toLowerCase();
if (platform == 'mac os')
platform = 'mac';
else if (platform != 'linux' && platform != 'android') platform = 'win';
return Image.asset('assets/$platform.png', width: 24, height: 24);
}
Widget getPeers() {
final size = MediaQuery.of(context).size;
final space = 8.0;
var width = size.width - 2 * space;
final minWidth = 320.0;
if (size.width > minWidth + 2 * space) {
final n = (size.width / (minWidth + 2 * space)).floor();
width = size.width / n - 2 * space;
}
final cards = <Widget>[];
var peers = FFI.peers();
peers.forEach((p) {
cards.add(Container(
width: width,
child: Card(
child: GestureDetector(
onTap: !isDesktop ? () => connect('${p.id}') : null,
onDoubleTap: isDesktop ? () => connect('${p.id}') : null,
onLongPressStart: (details) {
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
showPeerMenu(context, p.id);
},
child: ListTile(
contentPadding: const EdgeInsets.only(left: 12),
subtitle: Text('${p.username}@${p.hostname}'),
title: Text('${p.id}'),
leading: Container(
padding: const EdgeInsets.all(6),
child: getPlatformImage('${p.platform}'),
color: str2color('${p.id}${p.platform}', 0x7f)),
trailing: InkWell(
child: Padding(
padding: const EdgeInsets.all(12),
child: Icon(Icons.more_vert)),
onTapDown: (e) {
final x = e.globalPosition.dx;
final y = e.globalPosition.dy;
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
onTap: () {
showPeerMenu(context, p.id);
}),
)))));
});
return Wrap(children: cards, spacing: space, runSpacing: space);
}
void showPeerMenu(BuildContext context, String id) async {
var value = await showMenu(
context: context,
position: this._menuPos,
items: [
PopupMenuItem<String>(
child: Text(translate('Remove')), value: 'remove')
] +
(!isAndroid
? []
: [
PopupMenuItem<String>(
child: Text(translate('File transfer')), value: 'file')
]),
elevation: 8,
);
if (value == 'remove') {
setState(() => FFI.setByName('remove', '$id'));
() async {
removePreference(id);
}();
} else if (value == 'file') {
connect(id, isFileTransfer: true);
}
}
}
class WebMenu extends StatefulWidget {
@override
_WebMenuState createState() => _WebMenuState();
}
class _WebMenuState extends State<WebMenu> {
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
final username = getUsername();
return PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
itemBuilder: (context) {
return (isIOS
? [
PopupMenuItem(
child: Icon(Icons.qr_code_scanner, color: Colors.black),
value: "scan",
)
]
: <PopupMenuItem<String>>[]) +
[
PopupMenuItem(
child: Text(translate('ID/Relay Server')),
value: "server",
)
] +
(getUrl().contains('admin.rustdesk.com')
? <PopupMenuItem<String>>[]
: [
PopupMenuItem(
child: Text(username == null
? translate("Login")
: translate("Logout") + ' ($username)'),
value: "login",
)
]) +
[
PopupMenuItem(
child: Text(translate('About') + ' RustDesk'),
value: "about",
)
];
},
onSelected: (value) {
if (value == 'server') {
showServerSettings();
}
if (value == 'about') {
showAbout();
}
if (value == 'login') {
if (username == null) {
showLogin();
} else {
logout();
}
}
if (value == 'scan') {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => ScanPage(),
),
);
}
});
}
}

View File

@@ -0,0 +1,602 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:provider/provider.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
import 'package:wakelock/wakelock.dart';
import 'package:toggle_switch/toggle_switch.dart';
import '../common.dart';
import '../models/model.dart';
import '../widgets/dialog.dart';
class FileManagerPage extends StatefulWidget {
FileManagerPage({Key? key, required this.id}) : super(key: key);
final String id;
@override
State<StatefulWidget> createState() => _FileManagerPageState();
}
class _FileManagerPageState extends State<FileManagerPage> {
final model = FFI.fileModel;
final _selectedItems = SelectedItems();
Timer? _interval;
final _breadCrumbScroller = ScrollController();
@override
void initState() {
super.initState();
FFI.connect(widget.id, isFileTransfer: true);
WidgetsBinding.instance!.addPostFrameCallback((_) {
showLoading(translate('Connecting...'));
_interval = Timer.periodic(Duration(milliseconds: 30),
(timer) => FFI.ffiModel.update(widget.id));
});
Wakelock.enable();
}
@override
void dispose() {
model.onClose();
_interval?.cancel();
FFI.close();
SmartDialog.dismiss();
Wakelock.disable();
super.dispose();
}
@override
Widget build(BuildContext context) => ChangeNotifierProvider.value(
value: FFI.fileModel,
child: Consumer<FileModel>(builder: (_context, _model, _child) {
return WillPopScope(
onWillPop: () async {
if (model.selectMode) {
model.toggleSelectMode();
} else {
goBack();
}
return false;
},
child: Scaffold(
backgroundColor: MyTheme.grayBg,
appBar: AppBar(
leading: Row(children: [
IconButton(icon: Icon(Icons.close), onPressed: clientClose),
]),
centerTitle: true,
title: ToggleSwitch(
initialLabelIndex: model.isLocal ? 0 : 1,
activeBgColor: [MyTheme.idColor],
inactiveBgColor: MyTheme.grayBg,
inactiveFgColor: Colors.black54,
totalSwitches: 2,
minWidth: 100,
fontSize: 15,
iconSize: 18,
labels: [translate("Local"), translate("Remote")],
icons: [Icons.phone_android_sharp, Icons.screen_share],
onToggle: (index) {
final current = model.isLocal ? 0 : 1;
if (index != current) {
model.togglePage();
}
},
),
actions: [
PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
itemBuilder: (context) {
return [
PopupMenuItem(
child: Row(
children: [
Icon(Icons.refresh, color: Colors.black),
SizedBox(width: 5),
Text(translate("Refresh File"))
],
),
value: "refresh",
),
PopupMenuItem(
child: Row(
children: [
Icon(Icons.check, color: Colors.black),
SizedBox(width: 5),
Text(translate("Multi Select"))
],
),
value: "select",
),
PopupMenuItem(
child: Row(
children: [
Icon(Icons.folder_outlined,
color: Colors.black),
SizedBox(width: 5),
Text(translate("Create Folder"))
],
),
value: "folder",
),
PopupMenuItem(
child: Row(
children: [
Icon(
model.currentShowHidden
? Icons.check_box_outlined
: Icons.check_box_outline_blank,
color: Colors.black),
SizedBox(width: 5),
Text(translate("Show Hidden Files"))
],
),
value: "hidden",
)
];
},
onSelected: (v) {
if (v == "refresh") {
model.refresh();
} else if (v == "select") {
_selectedItems.clear();
model.toggleSelectMode();
} else if (v == "folder") {
final name = TextEditingController();
DialogManager.show(
(setState, close) => CustomAlertDialog(
title: Text(translate("Create Folder")),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
decoration: InputDecoration(
labelText: translate(
"Please enter the folder name"),
),
controller: name,
),
],
),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () => close(false),
child: Text(translate("Cancel"))),
ElevatedButton(
style: flatButtonStyle,
onPressed: () {
if (name.value.text.isNotEmpty) {
model.createDir(PathUtil.join(
model.currentDir.path,
name.value.text,
model.currentIsWindows));
close();
}
},
child: Text(translate("OK")))
]));
} else if (v == "hidden") {
model.toggleShowHidden();
}
}),
],
),
body: body(),
bottomSheet: bottomSheet(),
));
}));
bool needShowCheckBox() {
if (!model.selectMode) {
return false;
}
return !_selectedItems.isOtherPage(model.isLocal);
}
Widget body() {
final isLocal = model.isLocal;
final fd = model.currentDir;
final entries = fd.entries;
return Column(children: [
headTools(),
Expanded(
child: ListView.builder(
itemCount: entries.length + 1,
itemBuilder: (context, index) {
if (index >= entries.length) {
return listTail();
}
var selected = false;
if (model.selectMode) {
selected = _selectedItems.contains(entries[index]);
}
final sizeStr = entries[index].isFile
? readableFileSize(entries[index].size.toDouble())
: "";
return Card(
child: ListTile(
leading: Icon(
entries[index].isFile ? Icons.feed_outlined : Icons.folder,
size: 40),
title: Text(entries[index].name),
selected: selected,
subtitle: Text(
entries[index]
.lastModified()
.toString()
.replaceAll(".000", "") +
" " +
sizeStr,
style: TextStyle(fontSize: 12, color: MyTheme.darkGray),
),
trailing: needShowCheckBox()
? Checkbox(
value: selected,
onChanged: (v) {
if (v == null) return;
if (v && !selected) {
_selectedItems.add(isLocal, entries[index]);
} else if (!v && selected) {
_selectedItems.remove(entries[index]);
}
setState(() {});
})
: PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
itemBuilder: (context) {
return [
PopupMenuItem(
child: Text(translate("Delete")),
value: "delete",
),
PopupMenuItem(
child: Text(translate("Multi Select")),
value: "multi_select",
),
PopupMenuItem(
child: Text(translate("Properties")),
value: "properties",
enabled: false,
)
];
},
onSelected: (v) {
if (v == "delete") {
final items = SelectedItems();
items.add(isLocal, entries[index]);
model.removeAction(items);
} else if (v == "multi_select") {
_selectedItems.clear();
model.toggleSelectMode();
}
}),
onTap: () {
if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) {
if (selected) {
_selectedItems.remove(entries[index]);
} else {
_selectedItems.add(isLocal, entries[index]);
}
setState(() {});
return;
}
if (entries[index].isDirectory) {
model.openDirectory(entries[index].path);
breadCrumbScrollToEnd();
} else {
// Perform file-related tasks.
}
},
onLongPress: () {
_selectedItems.clear();
model.toggleSelectMode();
if (model.selectMode) {
_selectedItems.add(isLocal, entries[index]);
}
setState(() {});
},
),
);
},
))
]);
}
goBack() {
model.goToParentDirectory();
}
breadCrumbScrollToEnd() {
Future.delayed(Duration(milliseconds: 200), () {
_breadCrumbScroller.animateTo(
_breadCrumbScroller.position.maxScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.fastLinearToSlowEaseIn);
});
}
Widget headTools() => Container(
child: Row(
children: [
Expanded(
child: BreadCrumb(
items: getPathBreadCrumbItems(() => model.goHome(), (list) {
var path = "";
if (model.currentHome.startsWith(list[0])) {
// absolute path
for (var item in list) {
path = PathUtil.join(path, item, model.currentIsWindows);
}
} else {
path += model.currentHome;
for (var item in list) {
path = PathUtil.join(path, item, model.currentIsWindows);
}
}
model.openDirectory(path);
}),
divider: Icon(Icons.chevron_right),
overflow: ScrollableOverflow(controller: _breadCrumbScroller),
)),
Row(
children: [
IconButton(
icon: Icon(Icons.arrow_upward),
onPressed: goBack,
),
PopupMenuButton<SortBy>(
icon: Icon(Icons.sort),
itemBuilder: (context) {
return SortBy.values
.map((e) => PopupMenuItem(
child:
Text(translate(e.toString().split(".").last)),
value: e,
))
.toList();
},
onSelected: model.changeSortStyle),
],
)
],
));
Widget listTail() {
return Container(
height: 100,
child: Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(30, 5, 30, 0),
child: Text(
model.currentDir.path,
style: TextStyle(color: MyTheme.darkGray),
),
),
Padding(
padding: EdgeInsets.all(2),
child: Text(
"${translate("Total")}: ${model.currentDir.entries.length} ${translate("items")}",
style: TextStyle(color: MyTheme.darkGray),
),
)
],
),
);
}
Widget? bottomSheet() {
final state = model.jobState;
final isOtherPage = _selectedItems.isOtherPage(model.isLocal);
final selectedItemsLen = "${_selectedItems.length} ${translate("items")}";
final local = _selectedItems.isLocal == null
? ""
: " [${_selectedItems.isLocal! ? translate("Local") : translate("Remote")}]";
if (model.selectMode) {
if (_selectedItems.length == 0 || !isOtherPage) {
return BottomSheetBody(
leading: Icon(Icons.check),
title: translate("Selected"),
text: selectedItemsLen + local,
onCanceled: () => model.toggleSelectMode(),
actions: [
IconButton(
icon: Icon(Icons.compare_arrows),
onPressed: model.togglePage,
),
IconButton(
icon: Icon(Icons.delete_forever),
onPressed: () {
if (_selectedItems.length > 0) {
model.removeAction(_selectedItems);
}
},
)
]);
} else {
return BottomSheetBody(
leading: Icon(Icons.input),
title: translate("Paste here?"),
text: selectedItemsLen + local,
onCanceled: () => model.toggleSelectMode(),
actions: [
IconButton(
icon: Icon(Icons.compare_arrows),
onPressed: model.togglePage,
),
IconButton(
icon: Icon(Icons.paste),
onPressed: () {
model.toggleSelectMode();
model.sendFiles(_selectedItems);
},
)
]);
}
}
switch (state) {
case JobState.inProgress:
return BottomSheetBody(
leading: CircularProgressIndicator(),
title: translate("Waiting"),
text:
"${translate("Speed")}: ${readableFileSize(model.jobProgress.speed)}/s",
onCanceled: () => model.cancelJob(model.jobProgress.id),
);
case JobState.done:
return BottomSheetBody(
leading: Icon(Icons.check),
title: "${translate("Successful")}!",
text: "",
onCanceled: () => model.jobReset(),
);
case JobState.error:
return BottomSheetBody(
leading: Icon(Icons.error),
title: "${translate("Error")}!",
text: "",
onCanceled: () => model.jobReset(),
);
case JobState.none:
break;
}
return null;
}
List<BreadCrumbItem> getPathBreadCrumbItems(
void Function() onHome, void Function(List<String>) onPressed) {
final path = model.currentShortPath;
final list = PathUtil.split(path, model.currentIsWindows);
final breadCrumbList = [
BreadCrumbItem(
content: IconButton(
icon: Icon(Icons.home_filled),
onPressed: onHome,
))
];
breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem(
content: TextButton(
child: Text(e.value),
style:
ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))),
onPressed: () => onPressed(list.sublist(0, e.key + 1))))));
return breadCrumbList;
}
}
class BottomSheetBody extends StatelessWidget {
BottomSheetBody(
{required this.leading,
required this.title,
required this.text,
this.onCanceled,
this.actions});
final Widget leading;
final String title;
final String text;
final VoidCallback? onCanceled;
final List<IconButton>? actions;
@override
BottomSheet build(BuildContext context) {
final _actions = actions ?? [];
return BottomSheet(
builder: (BuildContext context) {
return Container(
height: 65,
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: MyTheme.accent50,
borderRadius: BorderRadius.vertical(top: Radius.circular(10))),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
leading,
SizedBox(width: 16),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 18)),
Text(text,
style: TextStyle(
fontSize: 14, color: MyTheme.grayBg))
],
)
],
),
Row(children: () {
_actions.add(IconButton(
icon: Icon(Icons.cancel_outlined),
onPressed: onCanceled,
));
return _actions;
}())
],
),
));
},
onClosing: () {},
backgroundColor: MyTheme.grayBg,
enableDrag: false,
);
}
}
class SelectedItems {
bool? _isLocal;
final List<Entry> _items = [];
List<Entry> get items => _items;
int get length => _items.length;
bool? get isLocal => _isLocal;
add(bool isLocal, Entry e) {
if (_isLocal == null) {
_isLocal = isLocal;
}
if (_isLocal != null && _isLocal != isLocal) {
return;
}
if (!_items.contains(e)) {
_items.add(e);
}
}
bool contains(Entry e) {
return _items.contains(e);
}
remove(Entry e) {
_items.remove(e);
if (_items.length == 0) {
_isLocal = null;
}
}
bool isOtherPage(bool currentIsLocal) {
if (_isLocal == null) {
return false;
} else {
return _isLocal != currentIsLocal;
}
}
clear() {
_items.clear();
_isLocal = null;
}
}

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/pages/chat_page.dart';
import 'package:flutter_hbb/pages/server_page.dart';
import 'package:flutter_hbb/pages/settings_page.dart';
import '../common.dart';
import '../widgets/overlay.dart';
import 'connection_page.dart';
abstract class PageShape extends Widget {
final String title = "";
final Icon icon = Icon(null);
final List<Widget> appBarActions = [];
}
class HomePage extends StatefulWidget {
HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var _selectedIndex = 0;
final List<PageShape> _pages = [];
@override
void initState() {
super.initState();
_pages.add(ConnectionPage());
if (isAndroid) {
_pages.addAll([chatPage, ServerPage()]);
}
_pages.add(SettingsPage());
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (_selectedIndex != 0) {
setState(() {
_selectedIndex = 0;
});
} else {
return true;
}
return false;
},
child: Scaffold(
backgroundColor: MyTheme.grayBg,
appBar: AppBar(
centerTitle: true,
title: Text("RustDesk"),
actions: _pages.elementAt(_selectedIndex).appBarActions,
),
bottomNavigationBar: BottomNavigationBar(
key: navigationBarKey,
items: _pages
.map((page) =>
BottomNavigationBarItem(icon: page.icon, label: page.title))
.toList(),
currentIndex: _selectedIndex,
type: BottomNavigationBarType.fixed,
selectedItemColor: MyTheme.accent,
unselectedItemColor: MyTheme.darkGray,
onTap: (index) => setState(() {
// close chat overlay when go chat page
if (index == 1 && _selectedIndex != index) {
hideChatIconOverlay();
hideChatWindowOverlay();
}
_selectedIndex = index;
}),
),
body: _pages.elementAt(_selectedIndex),
));
}
}
class WebHomePage extends StatelessWidget {
final connectionPage = ConnectionPage();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MyTheme.grayBg,
appBar: AppBar(
centerTitle: true,
title: Text("RustDesk" + (isWeb ? " (Beta) " : "")),
actions: connectionPage.appBarActions,
),
body: connectionPage,
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,258 @@
import 'package:flutter/material.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image/image.dart' as img;
import 'package:zxing2/qrcode.dart';
import 'dart:io';
import 'dart:async';
import 'dart:convert';
import '../common.dart';
import '../models/model.dart';
class ScanPage extends StatefulWidget {
@override
_ScanPageState createState() => _ScanPageState();
}
class _ScanPageState extends State<ScanPage> {
QRViewController? controller;
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
// In order to get hot reload to work we need to pause the camera if the platform
// is android, or resume the camera if the platform is iOS.
@override
void reassemble() {
super.reassemble();
if (isAndroid) {
controller!.pauseCamera();
}
controller!.resumeCamera();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Scan QR'),
actions: [
IconButton(
color: Colors.white,
icon: Icon(Icons.image_search),
iconSize: 32.0,
onPressed: () async {
final ImagePicker _picker = ImagePicker();
final XFile? file =
await _picker.pickImage(source: ImageSource.gallery);
if (file != null) {
var image = img.decodeNamedImage(
File(file.path).readAsBytesSync(), file.path)!;
LuminanceSource source = RGBLuminanceSource(
image.width,
image.height,
image
.getBytes(format: img.Format.abgr)
.buffer
.asInt32List());
var bitmap = BinaryBitmap(HybridBinarizer(source));
var reader = QRCodeReader();
try {
var result = reader.decode(bitmap);
showServerSettingFromQr(result.text);
} catch (e) {
showToast('No QR code found');
}
}
}),
IconButton(
color: Colors.yellow,
icon: Icon(Icons.flash_on),
iconSize: 32.0,
onPressed: () async {
await controller?.toggleFlash();
}),
IconButton(
color: Colors.white,
icon: Icon(Icons.switch_camera),
iconSize: 32.0,
onPressed: () async {
await controller?.flipCamera();
},
),
],
),
body: _buildQrView(context));
}
Widget _buildQrView(BuildContext context) {
// For this example we check how width or tall the device is and change the scanArea and overlay accordingly.
var scanArea = (MediaQuery.of(context).size.width < 400 ||
MediaQuery.of(context).size.height < 400)
? 150.0
: 300.0;
// To ensure the Scanner view is properly sizes after rotation
// we need to listen for Flutter SizeChanged notification and update controller
return QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
overlay: QrScannerOverlayShape(
borderColor: Colors.red,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: scanArea),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
);
}
void _onQRViewCreated(QRViewController controller) {
setState(() {
this.controller = controller;
});
controller.scannedDataStream.listen((scanData) {
if (scanData.code != null) {
showServerSettingFromQr(scanData.code!);
}
});
}
void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
if (!p) {
showToast('No permisssion');
}
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
void showServerSettingFromQr(String data) async {
backToHome();
await controller?.pauseCamera();
if (!data.startsWith('config=')) {
showToast('Invalid QR code');
return;
}
try {
Map<String, dynamic> values = json.decode(data.substring(7));
var host = values['host'] != null ? values['host'] as String : '';
var key = values['key'] != null ? values['key'] as String : '';
var api = values['api'] != null ? values['api'] as String : '';
Timer(Duration(milliseconds: 60), () {
showServerSettingsWithValue(host, '', key, api);
});
} catch (e) {
showToast('Invalid QR code');
}
}
}
void showServerSettingsWithValue(
String id, String relay, String key, String api) {
final formKey = GlobalKey<FormState>();
final id0 = FFI.getByName('option', 'custom-rendezvous-server');
final relay0 = FFI.getByName('option', 'relay-server');
final api0 = FFI.getByName('option', 'api-server');
final key0 = FFI.getByName('option', 'key');
DialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate('ID/Relay Server')),
content: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
initialValue: id,
decoration: InputDecoration(
labelText: translate('ID Server'),
),
validator: validate,
onSaved: (String? value) {
if (value != null) id = value.trim();
},
)
] +
(isAndroid
? [
TextFormField(
initialValue: relay,
decoration: InputDecoration(
labelText: translate('Relay Server'),
),
validator: validate,
onSaved: (String? value) {
if (value != null) relay = value.trim();
},
)
]
: []) +
[
TextFormField(
initialValue: api,
decoration: InputDecoration(
labelText: translate('API Server'),
),
validator: validate,
onSaved: (String? value) {
if (value != null) api = value.trim();
},
),
TextFormField(
initialValue: key,
decoration: InputDecoration(
labelText: 'Key',
),
validator: null,
onSaved: (String? value) {
if (value != null) key = value.trim();
},
),
])),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () {
close();
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: () {
if (formKey.currentState != null &&
formKey.currentState!.validate()) {
formKey.currentState!.save();
if (id != id0)
FFI.setByName('option',
'{"name": "custom-rendezvous-server", "value": "$id"}');
if (relay != relay0)
FFI.setByName(
'option', '{"name": "relay-server", "value": "$relay"}');
if (key != key0)
FFI.setByName('option', '{"name": "key", "value": "$key"}');
if (api != api0)
FFI.setByName(
'option', '{"name": "api-server", "value": "$api"}');
FFI.ffiModel.updateUser();
close();
}
},
child: Text(translate('OK')),
),
],
);
});
}
String? validate(value) {
value = value.trim();
if (value.isEmpty) {
return null;
}
final res = FFI.getByName('test_if_valid_server', value);
return res.isEmpty ? null : res;
}

View File

@@ -0,0 +1,500 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/widgets/dialog.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:provider/provider.dart';
import '../common.dart';
import '../models/server_model.dart';
import 'home_page.dart';
import '../models/model.dart';
class ServerPage extends StatelessWidget implements PageShape {
@override
final title = translate("Share Screen");
@override
final icon = Icon(Icons.mobile_screen_share);
@override
final appBarActions = [
PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
itemBuilder: (context) {
return [
PopupMenuItem(
child: Text(translate("Change ID")),
value: "changeID",
enabled: false,
),
PopupMenuItem(
child: Text(translate("Set your own password")),
value: "changePW",
enabled: FFI.serverModel.isStart,
),
PopupMenuItem(
child: Text(translate("Refresh random password")),
value: "refreshPW",
enabled: FFI.serverModel.isStart,
)
];
},
onSelected: (value) {
if (value == "changeID") {
// TODO
} else if (value == "changePW") {
updatePasswordDialog();
} else if (value == "refreshPW") {
() async {
showLoading(translate("Waiting"));
if (await FFI.serverModel.updatePassword("")) {
showSuccess();
} else {
showError();
}
debugPrint("end updatePassword");
}();
}
})
];
@override
Widget build(BuildContext context) {
checkService();
return ChangeNotifierProvider.value(
value: FFI.serverModel,
child: Consumer<ServerModel>(
builder: (context, serverModel, child) => SingleChildScrollView(
controller: FFI.serverModel.controller,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
ServerInfo(),
PermissionChecker(),
ConnectionManager(),
SizedBox.fromSize(size: Size(0, 15.0)),
],
),
),
)));
}
}
void checkService() async {
FFI.invokeMethod("check_service"); // jvm
// for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page
if (PermissionManager.isWaitingFile() && !FFI.serverModel.fileOk) {
PermissionManager.complete("file", await PermissionManager.check("file"));
debugPrint("file permission finished");
}
}
class ServerInfo extends StatefulWidget {
@override
_ServerInfoState createState() => _ServerInfoState();
}
class _ServerInfoState extends State<ServerInfo> {
final model = FFI.serverModel;
var _passwdShow = false;
@override
Widget build(BuildContext context) {
return model.isStart
? PaddingCard(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
readOnly: true,
style: TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.bold,
color: MyTheme.accent),
controller: model.serverId,
decoration: InputDecoration(
icon: const Icon(Icons.perm_identity),
labelText: translate("ID"),
labelStyle: TextStyle(
fontWeight: FontWeight.bold, color: MyTheme.accent50),
),
onSaved: (String? value) {},
),
TextFormField(
readOnly: true,
obscureText: !_passwdShow,
style: TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.bold,
color: MyTheme.accent),
controller: model.serverPasswd,
decoration: InputDecoration(
icon: const Icon(Icons.lock),
labelText: translate("Password"),
labelStyle: TextStyle(
fontWeight: FontWeight.bold, color: MyTheme.accent50),
suffix: IconButton(
icon: Icon(Icons.visibility),
onPressed: () {
setState(() {
_passwdShow = !_passwdShow;
});
})),
onSaved: (String? value) {},
),
],
))
: PaddingCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: Row(
children: [
Icon(Icons.warning_amber_sharp,
color: Colors.redAccent, size: 24),
SizedBox(width: 10),
Text(
translate("Service is not running"),
style: TextStyle(
fontFamily: 'WorkSans',
fontWeight: FontWeight.bold,
fontSize: 18,
color: MyTheme.accent80,
),
)
],
)),
SizedBox(height: 5),
Center(
child: Text(
translate("android_start_service_tip"),
style: TextStyle(fontSize: 12, color: MyTheme.darkGray),
))
],
));
}
}
class PermissionChecker extends StatefulWidget {
@override
_PermissionCheckerState createState() => _PermissionCheckerState();
}
class _PermissionCheckerState extends State<PermissionChecker> {
@override
Widget build(BuildContext context) {
final serverModel = Provider.of<ServerModel>(context);
final hasAudioPermission = androidVersion >= 30;
final status;
if (serverModel.connectStatus == -1) {
status = 'not_ready_status';
} else if (serverModel.connectStatus == 0) {
status = 'connecting_status';
} else {
status = 'Ready';
}
return PaddingCard(
title: translate("Permissions"),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PermissionRow(translate("Screen Capture"), serverModel.mediaOk,
serverModel.toggleService),
PermissionRow(translate("Input Control"), serverModel.inputOk,
serverModel.toggleInput),
PermissionRow(translate("File Transfer"), serverModel.fileOk,
serverModel.toggleFile),
hasAudioPermission
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
serverModel.toggleAudio)
: Text(
"* ${translate("android_version_audio_tip")}",
style: TextStyle(color: MyTheme.darkGray),
),
SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 0,
child: serverModel.mediaOk
? ElevatedButton.icon(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.red)),
icon: Icon(Icons.stop),
onPressed: serverModel.toggleService,
label: Text(translate("Stop service")))
: ElevatedButton.icon(
icon: Icon(Icons.play_arrow),
onPressed: serverModel.toggleService,
label: Text(translate("Start Service")))),
Expanded(
child: serverModel.mediaOk
? Row(
children: [
Expanded(
flex: 0,
child: Padding(
padding:
EdgeInsets.only(left: 20, right: 5),
child: Icon(Icons.circle,
color: serverModel.connectStatus > 0
? Colors.greenAccent
: Colors.deepOrangeAccent,
size: 10))),
Expanded(
child: Text(translate(status),
softWrap: true,
style: TextStyle(
fontSize: 14.0,
color: MyTheme.accent50)))
],
)
: SizedBox.shrink())
],
),
],
));
}
}
class PermissionRow extends StatelessWidget {
PermissionRow(this.name, this.isOk, this.onPressed);
final String name;
final bool isOk;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SizedBox(
width: 140,
child: Text(name,
style: TextStyle(fontSize: 16.0, color: MyTheme.accent50))),
SizedBox(
width: 50,
child: Text(isOk ? translate("ON") : translate("OFF"),
style: TextStyle(
fontSize: 16.0,
color: isOk ? Colors.green : Colors.grey)),
)
],
),
TextButton(
onPressed: onPressed,
child: Text(
translate(isOk ? "CLOSE" : "OPEN"),
style: TextStyle(fontWeight: FontWeight.bold),
)),
const Divider(height: 0)
],
);
}
}
class ConnectionManager extends StatelessWidget {
@override
Widget build(BuildContext context) {
final serverModel = Provider.of<ServerModel>(context);
return Column(
children: serverModel.clients.entries
.map((entry) => PaddingCard(
title: translate(entry.value.isFileTransfer
? "File Connection"
: "Screen Connection"),
titleIcon: entry.value.isFileTransfer
? Icons.folder_outlined
: Icons.mobile_screen_share,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: clientInfo(entry.value)),
Expanded(
flex: -1,
child: entry.value.isFileTransfer ||
!entry.value.authorized
? SizedBox.shrink()
: IconButton(
onPressed: () {
FFI.chatModel
.changeCurrentID(entry.value.id);
final bar =
navigationBarKey.currentWidget;
if (bar != null) {
bar as BottomNavigationBar;
bar.onTap!(1);
}
},
icon: Icon(
Icons.chat,
color: MyTheme.accent80,
)))
],
),
entry.value.authorized
? SizedBox.shrink()
: Text(
translate("android_new_connection_tip"),
style: TextStyle(color: Colors.black54),
),
entry.value.authorized
? ElevatedButton.icon(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.red)),
icon: Icon(Icons.close),
onPressed: () {
FFI.setByName("close_conn", entry.key.toString());
FFI.invokeMethod(
"cancel_notification", entry.key);
},
label: Text(translate("Close")))
: Row(children: [
TextButton(
child: Text(translate("Dismiss")),
onPressed: () {
serverModel.sendLoginResponse(
entry.value, false);
}),
SizedBox(width: 20),
ElevatedButton(
child: Text(translate("Accept")),
onPressed: () {
serverModel.sendLoginResponse(
entry.value, true);
}),
]),
],
)))
.toList());
}
}
class PaddingCard extends StatelessWidget {
PaddingCard({required this.child, this.title, this.titleIcon});
final String? title;
final IconData? titleIcon;
final Widget child;
@override
Widget build(BuildContext context) {
final children = [child];
if (title != null) {
children.insert(
0,
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: Row(
children: [
titleIcon != null
? Padding(
padding: EdgeInsets.only(right: 10),
child: Icon(titleIcon,
color: MyTheme.accent80, size: 30))
: SizedBox.shrink(),
Text(
title!,
style: TextStyle(
fontFamily: 'WorkSans',
fontWeight: FontWeight.bold,
fontSize: 20,
color: MyTheme.accent80,
),
)
],
)));
}
return Container(
width: double.maxFinite,
child: Card(
margin: EdgeInsets.fromLTRB(15.0, 15.0, 15.0, 0),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 30.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
));
}
}
Widget clientInfo(Client client) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(
children: [
Expanded(
flex: -1,
child: Padding(
padding: EdgeInsets.only(right: 12),
child: CircleAvatar(
child: Text(client.name[0]),
backgroundColor: MyTheme.border))),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(client.name,
style: TextStyle(color: MyTheme.idColor, fontSize: 18)),
SizedBox(width: 8),
Text(client.peerId,
style: TextStyle(color: MyTheme.idColor, fontSize: 10))
]))
],
),
]));
}
void toAndroidChannelInit() {
FFI.setMethodCallHandler((method, arguments) {
debugPrint("flutter got android msg,$method,$arguments");
try {
switch (method) {
case "start_capture":
{
SmartDialog.dismiss();
FFI.serverModel.updateClientState();
break;
}
case "on_state_changed":
{
var name = arguments["name"] as String;
var value = arguments["value"] as String == "true";
debugPrint("from jvm:on_state_changed,$name:$value");
FFI.serverModel.changeStatue(name, value);
break;
}
case "on_android_permission_result":
{
var type = arguments["type"] as String;
var result = arguments["result"] as bool;
PermissionManager.complete(type, result);
break;
}
case "on_media_projection_canceled":
{
FFI.serverModel.stopService();
break;
}
}
} catch (e) {
debugPrint("MethodCallHandler err:$e");
}
return "";
});
}

View File

@@ -0,0 +1,357 @@
import 'package:settings_ui/settings_ui.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:provider/provider.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../common.dart';
import '../widgets/dialog.dart';
import '../models/model.dart';
import 'home_page.dart';
import 'scan_page.dart';
class SettingsPage extends StatefulWidget implements PageShape {
@override
final title = translate("Settings");
@override
final icon = Icon(Icons.settings);
@override
final appBarActions = [ScanButton()];
@override
_SettingsState createState() => _SettingsState();
}
class _SettingsState extends State<SettingsPage> {
static const url = 'https://rustdesk.com/';
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
final username = getUsername();
return SettingsList(
sections: [
SettingsSection(
title: Text(translate("Account")),
tiles: [
SettingsTile.navigation(
title: Text(username == null
? translate("Login")
: translate("Logout") + ' ($username)'),
leading: Icon(Icons.person),
onPressed: (context) {
if (username == null) {
showLogin();
} else {
logout();
}
},
),
],
),
SettingsSection(
title: Text(translate("Settings")),
tiles: [
SettingsTile.navigation(
title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud),
onPressed: (context) {
showServerSettings();
},
),
],
),
SettingsSection(
title: Text(translate("About")),
tiles: [
SettingsTile.navigation(
onPressed: (context) async {
if (await canLaunch(url)) {
await launch(url);
}
},
title: Text(translate("Version: ") + version),
value: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text('rustdesk.com',
style: TextStyle(
decoration: TextDecoration.underline,
)),
),
leading: Icon(Icons.info)),
],
),
],
);
}
}
void showServerSettings() {
final id = FFI.getByName('option', 'custom-rendezvous-server');
final relay = FFI.getByName('option', 'relay-server');
final api = FFI.getByName('option', 'api-server');
final key = FFI.getByName('option', 'key');
showServerSettingsWithValue(id, relay, key, api);
}
void showAbout() {
DialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate('About') + ' RustDesk'),
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
Text('Version: $version'),
InkWell(
onTap: () async {
const url = 'https://rustdesk.com/';
if (await canLaunch(url)) {
await launch(url);
}
},
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text('rustdesk.com',
style: TextStyle(
decoration: TextDecoration.underline,
)),
)),
]),
actions: [],
);
}, clickMaskDismiss: true, backDismiss: true);
}
Future<String> login(String name, String pass) async {
/* js test CORS
const data = { username: 'example', password: 'xx' };
fetch('http://localhost:21114/api/login', {
method: 'POST', // or 'PUT'
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
})
.catch((error) => {
console.error('Error:', error);
});
*/
final url = getUrl();
final body = {
'username': name,
'password': pass,
'id': FFI.getByName('server_id'),
'uuid': FFI.getByName('uuid')
};
try {
final response = await http.post(Uri.parse('${url}/api/login'),
headers: {"Content-Type": "application/json"}, body: json.encode(body));
return parseResp(response.body);
} catch (e) {
print(e);
return 'Failed to access $url';
}
}
String parseResp(String body) {
final data = json.decode(body);
final error = data['error'];
if (error != null) {
return error!;
}
final token = data['access_token'];
if (token != null) {
FFI.setByName('option', '{"name": "access_token", "value": "$token"}');
}
final info = data['user'];
if (info != null) {
final value = json.encode(info);
FFI.setByName('option', json.encode({"name": "user_info", "value": value}));
FFI.ffiModel.updateUser();
}
return '';
}
void refreshCurrentUser() async {
final token = FFI.getByName("option", "access_token");
if (token == '') return;
final url = getUrl();
final body = {
'id': FFI.getByName('server_id'),
'uuid': FFI.getByName('uuid')
};
try {
final response = await http.post(Uri.parse('${url}/api/currentUser'),
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer $token"
},
body: json.encode(body));
final status = response.statusCode;
if (status == 401 || status == 400) {
resetToken();
return;
}
parseResp(response.body);
} catch (e) {
print('$e');
}
}
void logout() async {
final token = FFI.getByName("option", "access_token");
if (token == '') return;
final url = getUrl();
final body = {
'id': FFI.getByName('server_id'),
'uuid': FFI.getByName('uuid')
};
try {
await http.post(Uri.parse('${url}/api/logout'),
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer $token"
},
body: json.encode(body));
} catch (e) {
showToast('Failed to access $url');
}
resetToken();
}
void resetToken() {
FFI.setByName('option', '{"name": "access_token", "value": ""}');
FFI.setByName('option', '{"name": "user_info", "value": ""}');
FFI.ffiModel.updateUser();
}
String getUrl() {
var url = FFI.getByName('option', 'api-server');
if (url == '') {
url = FFI.getByName('option', 'custom-rendezvous-server');
if (url != '') {
if (url.contains(':')) {
final tmp = url.split(':');
if (tmp.length == 2) {
var port = int.parse(tmp[1]) - 2;
url = 'http://${tmp[0]}:$port';
}
} else {
url = 'http://${url}:21114';
}
}
}
if (url == '') {
url = 'https://admin.rustdesk.com';
}
return url;
}
void showLogin() {
final passwordController = TextEditingController();
final nameController = TextEditingController();
var loading = false;
var error = '';
DialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate('Login')),
content: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(
autofocus: true,
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
labelText: translate('Username'),
),
controller: nameController,
),
PasswordWidget(controller: passwordController),
]),
actions: (loading
? <Widget>[CircularProgressIndicator()]
: (error != ""
? <Widget>[
Text(translate(error),
style: TextStyle(color: Colors.red))
]
: <Widget>[])) +
<Widget>[
TextButton(
style: flatButtonStyle,
onPressed: loading
? null
: () {
close();
setState(() {
loading = false;
});
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: loading
? null
: () async {
final name = nameController.text.trim();
final pass = passwordController.text.trim();
if (name != "" && pass != "") {
setState(() {
loading = true;
});
final e = await login(name, pass);
setState(() {
loading = false;
error = e;
});
if (e == "") {
close();
}
}
},
child: Text(translate('OK')),
),
],
);
});
}
String? getUsername() {
final token = FFI.getByName("option", "access_token");
String? username;
if (token != "") {
final info = FFI.getByName("option", "user_info");
if (info != "") {
try {
Map<String, dynamic> tmp = json.decode(info);
username = tmp["name"];
} catch (e) {
print('$e');
}
}
}
return username;
}
class ScanButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(Icons.qr_code_scanner),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => ScanPage(),
),
);
},
);
}
}