source code

This commit is contained in:
rustdesk
2021-03-29 15:59:14 +08:00
parent 002fce136c
commit d1013487e2
175 changed files with 35074 additions and 2 deletions

27
src/ui/chatbox.html Normal file
View File

@@ -0,0 +1,27 @@
<html window-resizable>
<head>
<style>
@import url(common.css);
@media platform != "OSX" {
body {
border-top: color(border) solid 1px;
}
}
</style>
<script type="text/tiscript">
include "common.tis";
var p = view.parameters;
view.refresh = function() {
$(body).content(<ChatBox msgs={p.msgs} callback={p.callback} />);
view.focus = $(input);
}
view.refresh();
function self.closing() {
view.windowState = View.WINDOW_HIDDEN;
return false;
}
view.windowIcon = self.url(p.icon);
</script>
</head>
<body></body>
</html>

218
src/ui/cm.css Normal file
View File

@@ -0,0 +1,218 @@
body {
behavior: connection-manager;
}
div.content {
flow: horizontal;
size: *;
}
div.left-panel {
size: *;
padding: 1em;
border-spacing: 1em;
overflow-x: scroll-indicator;
position: relative;
}
div.chaticon svg {
size: 24px;
margin: 4px;
}
div.chaticon {
position: absolute;
right: 0;
top: 0;
size: 32px;
}
div.chaticon.active {
opacity: 0.5;
}
div.chaticon:active {
background: white;
}
div.right-panel {
background: white;
border-left: color(border) 1px solid;
size: *;
}
div.icon-and-id {
flow: horizontal;
border-spacing: 1em;
}
div.icon {
size: 96px;
text-align: center;
font-size: 96px;
line-height: 96px;
color: white;
font-weight: bold;
}
div.id {
color: color(green-blue);
}
div.permissions {
flow: horizontal;
border-spacing: 0.5em;
}
div.permissions > div {
size: 48px;
background: color(accent);
}
div.permissions icon {
margin: *;
size: 32px;
background-size: cover;
background-repeat: no-repeat;
display: block;
}
div.permissions > div.disabled {
background: #ddd;
}
div.permissions > div:active {
opacity: 0.5;
}
icon.keyboard {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII=');
}
icon.clipboard {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII=');
}
icon.audio {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg==');
}
div.buttons {
width: *;
border-spacing: 0.5em;
text-align: center;
}
div.buttons button {
width: 80px;
height: 40px;
margin: 0.5em;
}
button#disconnect {
width: 160px;
background: color(blood-red);
border: none;
}
button#disconnect:active {
opacity: 0.5;
}
@media platform != "OSX" {
header .window-toolbar {
left: 40px;
top: 8px;
}
}
@media platform == "OSX" {
header .tabs-wrapper {
margin-left: 80px;
margin-top: 8px;
}
}
div.tabs-wrapper {
size: *;
position: relative;
overflow: hidden;
}
div.tabs {
size: *;
flow: horizontal;
white-space: nowrap;
overflow: hidden;
}
header {
height: 32px;
border-bottom: none;
}
div.border-bottom {
position: absolute;
bottom: 0;
left: 0;
width: *;
height: 1px;
background: color(border) 1px solid;
}
header div.window-icon {
size: 32px;
}
div.tabs > div {
display: inline-block;
height: 24px;
line-height: 24px;
}
div.tab {
width: 70px;
@ELLIPSIS;
text-align: center;
position: relative;
padding: 0 5px;
}
div.active-tab {
background: color(gray-bg);
border: color(border) 1px solid;
border-bottom: none;
font-weight: bold;
}
span.unreaded {
position: absolute;
font-size: 11px;
size: 15px;
border-radius: 15px;
line-height: 15px;
background: color(blood-red);
display: inline-block;
color: white;
}
div.left-panel {
background: color(gray-bg);
}
button.window#minimize {
right: 0px!important;
}
div.tab-arrows {
position: absolute;
right: 2px;
font-weight: bold;
}
div.tab-arrows span {
display: inline-block;
height: *;
margin: 0;
padding: 6px 2px;
}

21
src/ui/cm.html Normal file
View File

@@ -0,0 +1,21 @@
<html>
<head>
<style>
@import url(common.css);
@import url(cm.css);
</style>
<script type="text/tiscript">
include "common.tis";
include "cm.tis";
</script>
</head>
<header>
<div.window-icon role="window-icon"><icon /></div>
<caption role="window-caption" />
<div.border-bottom />
<div.window-toolbar />
<div.window-buttons />
</header>
<body #handler>
</body>
</html>

465
src/ui/cm.rs Normal file
View File

@@ -0,0 +1,465 @@
use crate::ipc::{self, new_listener, Connection, Data};
#[cfg(windows)]
use hbb_common::futures_util::stream::StreamExt;
use hbb_common::{
allow_err,
config::{Config, ICON},
fs, log,
message_proto::*,
protobuf::Message as _,
tokio::{self, sync::mpsc, task::spawn_blocking},
};
use sciter::{make_args, Element, Value, HELEMENT};
use std::{
collections::HashMap,
ops::Deref,
sync::{Arc, RwLock},
};
pub struct ConnectionManagerInner {
root: Option<Element>,
senders: HashMap<i32, mpsc::UnboundedSender<Data>>,
}
#[derive(Clone)]
pub struct ConnectionManager(Arc<RwLock<ConnectionManagerInner>>);
impl Deref for ConnectionManager {
type Target = Arc<RwLock<ConnectionManagerInner>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ConnectionManager {
pub fn new() -> Self {
#[cfg(target_os = "linux")]
std::thread::spawn(start_pa);
let inner = ConnectionManagerInner {
root: None,
senders: HashMap::new(),
};
let cm = Self(Arc::new(RwLock::new(inner)));
#[cfg(target_os = "macos")]
{
let cloned = cm.clone();
*super::macos::SHOULD_OPEN_UNTITLED_FILE_CALLBACK
.lock()
.unwrap() = Some(Box::new(move || {
cloned.call("awake", &make_args!());
}));
}
let cloned = cm.clone();
std::thread::spawn(move || start_ipc(cloned));
cm
}
fn get_icon(&mut self) -> String {
ICON.to_owned()
}
fn check_click_time(&mut self, id: i32) {
let lock = self.read().unwrap();
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(Data::ClickTime(crate::get_time())));
}
}
#[inline]
fn call(&self, func: &str, args: &[Value]) {
let r = self.read().unwrap();
if let Some(ref e) = r.root {
allow_err!(e.call_method(func, args));
}
}
fn add_connection(
&self,
id: i32,
is_file_transfer: bool,
port_forward: String,
peer_id: String,
name: String,
authorized: bool,
keyboard: bool,
clipboard: bool,
audio: bool,
tx: mpsc::UnboundedSender<Data>,
) {
self.call(
"addConnection",
&make_args!(
id,
is_file_transfer,
port_forward,
peer_id,
name,
authorized,
keyboard,
clipboard,
audio
),
);
self.write().unwrap().senders.insert(id, tx);
}
fn remove_connection(&self, id: i32) {
self.write().unwrap().senders.remove(&id);
self.call("removeConnection", &make_args!(id));
}
async fn handle_data(
&self,
id: i32,
data: Data,
write_jobs: &mut Vec<fs::TransferJob>,
conn: &mut Connection,
) {
match data {
Data::ChatMessage { text } => {
self.call("newMessage", &make_args!(id, text));
}
Data::ClickTime(ms) => {
self.call("resetClickCallback", &make_args!(ms as f64));
}
Data::FS(v) => match v {
ipc::FS::ReadDir {
dir,
include_hidden,
} => {
Self::read_dir(&dir, include_hidden, conn).await;
}
ipc::FS::RemoveDir {
path,
id,
recursive,
} => {
Self::remove_dir(path, id, recursive, conn).await;
}
ipc::FS::RemoveFile { path, id, file_num } => {
Self::remove_file(path, id, file_num, conn).await;
}
ipc::FS::CreateDir { path, id } => {
Self::create_dir(path, id, conn).await;
}
ipc::FS::NewWrite {
path,
id,
mut files,
} => {
write_jobs.push(fs::TransferJob::new_write(
id,
path,
files
.drain(..)
.map(|f| FileEntry {
name: f.0,
modified_time: f.1,
..Default::default()
})
.collect(),
));
}
ipc::FS::CancelWrite { id } => {
if let Some(job) = fs::get_job(id, write_jobs) {
job.remove_download_file();
fs::remove_job(id, write_jobs);
}
}
ipc::FS::WriteDone { id, file_num } => {
if let Some(job) = fs::get_job(id, write_jobs) {
job.modify_time();
Self::send(fs::new_done(id, file_num), conn).await;
fs::remove_job(id, write_jobs);
}
}
ipc::FS::WriteBlock {
id,
file_num,
data,
compressed,
} => {
if let Some(job) = fs::get_job(id, write_jobs) {
if let Err(err) = job
.write(FileTransferBlock {
id,
file_num,
data,
compressed,
..Default::default()
})
.await
{
Self::send(fs::new_error(id, err, file_num), conn).await;
}
}
}
},
_ => {}
}
}
async fn read_dir(dir: &str, include_hidden: bool, conn: &mut Connection) {
let path = {
if dir.is_empty() {
Config::get_home()
} else {
fs::get_path(dir)
}
};
if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await {
let mut msg_out = Message::new();
let mut file_response = FileResponse::new();
file_response.set_dir(fd);
msg_out.set_file_response(file_response);
Self::send(msg_out, conn).await;
}
}
async fn handle_result<F: std::fmt::Display, S: std::fmt::Display>(
res: std::result::Result<std::result::Result<(), F>, S>,
id: i32,
file_num: i32,
conn: &mut Connection,
) {
match res {
Err(err) => {
Self::send(fs::new_error(id, err, file_num), conn).await;
}
Ok(Err(err)) => {
Self::send(fs::new_error(id, err, file_num), conn).await;
}
Ok(Ok(())) => {
Self::send(fs::new_done(id, file_num), conn).await;
}
}
}
async fn remove_file(path: String, id: i32, file_num: i32, conn: &mut Connection) {
Self::handle_result(
spawn_blocking(move || fs::remove_file(&path)).await,
id,
file_num,
conn,
)
.await;
}
async fn create_dir(path: String, id: i32, conn: &mut Connection) {
Self::handle_result(
spawn_blocking(move || fs::create_dir(&path)).await,
id,
0,
conn,
)
.await;
}
async fn remove_dir(path: String, id: i32, recursive: bool, conn: &mut Connection) {
let path = fs::get_path(&path);
Self::handle_result(
spawn_blocking(move || {
if recursive {
fs::remove_all_empty_dir(&path)
} else {
std::fs::remove_dir(&path).map_err(|err| err.into())
}
})
.await,
id,
0,
conn,
)
.await;
}
async fn send(msg: Message, conn: &mut Connection) {
match msg.write_to_bytes() {
Ok(bytes) => allow_err!(conn.send(&Data::RawMessage(bytes)).await),
err => allow_err!(err),
}
}
fn switch_permission(&self, id: i32, name: String, enabled: bool) {
let lock = self.read().unwrap();
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(Data::SwitchPermission { name, enabled }));
}
}
fn close(&self, id: i32) {
let lock = self.read().unwrap();
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(Data::Close));
}
}
fn send_msg(&self, id: i32, text: String) {
let lock = self.read().unwrap();
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(Data::ChatMessage { text }));
}
}
fn authorize(&self, id: i32) {
let lock = self.read().unwrap();
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(Data::Authorize));
}
}
fn exit(&self) {
std::process::exit(0);
}
}
impl sciter::EventHandler for ConnectionManager {
fn attached(&mut self, root: HELEMENT) {
self.write().unwrap().root = Some(Element::from(root));
}
sciter::dispatch_script_call! {
fn check_click_time(i32);
fn get_icon();
fn close(i32);
fn authorize(i32);
fn switch_permission(i32, String, bool);
fn send_msg(i32, String);
fn exit();
}
}
#[tokio::main(basic_scheduler)]
async fn start_ipc(cm: ConnectionManager) {
match new_listener("_cm").await {
Ok(mut incoming) => {
while let Some(result) = incoming.next().await {
match result {
Ok(stream) => {
let mut stream = Connection::new(stream);
let cm = cm.clone();
tokio::spawn(async move {
let mut conn_id: i32 = 0;
let (tx, mut rx) = mpsc::unbounded_channel::<Data>();
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
loop {
tokio::select! {
res = stream.next() => {
match res {
Err(err) => {
log::info!("cm ipc connection closed: {}", err);
break;
}
Ok(Some(data)) => {
match data {
Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio} => {
conn_id = id;
cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, tx.clone());
}
_ => {
cm.handle_data(conn_id, data, &mut write_jobs, &mut stream).await;
}
}
}
_ => {}
}
}
Some(data) = rx.recv() => {
allow_err!(stream.send(&data).await);
}
}
}
cm.remove_connection(conn_id);
});
}
Err(err) => {
log::error!("Couldn't get cm client: {:?}", err);
}
}
}
}
Err(err) => {
log::error!("Failed to start cm ipc server: {}", err);
}
}
std::process::exit(-1);
}
#[cfg(target_os = "linux")]
#[tokio::main(basic_scheduler)]
async fn start_pa() {
use hbb_common::config::APP_NAME;
use libpulse_binding as pulse;
use libpulse_simple_binding as psimple;
match new_listener("_pa").await {
Ok(mut incoming) => {
loop {
if let Some(result) = incoming.next().await {
match result {
Ok(stream) => {
let mut stream = Connection::new(stream);
let mut device: String = "".to_owned();
if let Some(Ok(Some(Data::Config((_, Some(x)))))) =
stream.next_timeout2(1000).await
{
device = x;
}
if device == "Mute" {
break;
}
if !device.is_empty() {
device = crate::platform::linux::get_pa_source_name(&device);
}
if device.is_empty() {
device = crate::platform::linux::get_pa_monitor();
}
if device.is_empty() {
break;
}
let spec = pulse::sample::Spec {
format: pulse::sample::Format::F32be,
channels: 2,
rate: crate::platform::linux::PA_SAMPLE_RATE,
};
log::info!("pa monitor: {:?}", device);
if let Ok(s) = psimple::Simple::new(
None, // Use the default server
APP_NAME, // Our applications name
pulse::stream::Direction::Record, // We want a record stream
Some(&device), // Use the default device
APP_NAME, // Description of our stream
&spec, // Our sample format
None, // Use default channel map
None, // Use default buffering attributes
) {
loop {
if let Some(Err(_)) = stream.next_timeout2(1).await {
break;
}
let mut out: Vec<u8> = Vec::with_capacity(480 * 4);
unsafe {
out.set_len(out.capacity());
}
if let Ok(_) = s.read(&mut out) {
if out.iter().filter(|x| **x != 0).next().is_some() {
allow_err!(stream.send(&Data::RawMessage(out)).await);
}
}
}
} else {
log::error!("Could not create simple pulse");
}
}
Err(err) => {
log::error!("Couldn't get pa client: {:?}", err);
}
}
}
}
}
Err(err) => {
log::error!("Failed to start pa ipc server: {}", err);
}
}
}

409
src/ui/cm.tis Normal file
View File

@@ -0,0 +1,409 @@
view.windowFrame = is_osx ? #extended : #solid;
var body;
var connections = [];
var show_chat = false;
var click_callback;
var click_callback_time = 0;
class Body: Reactor.Component
{
this var cur = 0;
function this() {
body = this;
}
function render() {
if (connections.length == 0) return <div />;
var c = connections[this.cur];
this.connection = c;
this.cid = c.id;
var auth = c.authorized;
var me = this;
var callback = function(msg) {
me.sendMsg(msg);
};
self.timer(1ms, adaptSize);
var right_style = show_chat ? "" : "display: none";
return <div .content>
<div .left-panel>
<div .icon-and-id>
<div .icon style={"background: " + string2RGB(c.name, 1)}>
{c.name[0].toUpperCase()}
</div>
<div>
<div .id style="font-weight: bold; font-size: 1.2em;">{c.name}</div>
<div .id>({c.peer_id})</div>
<div style="margin-top: 1.2em">Connected <span #time>{getElaspsed(c.time)}</span></div>
</div>
</div>
<div />
{c.is_file_transfer || c.port_forward ? "" : <div>Permissions</div>}
{c.is_file_transfer || c.port_forward ? "" : <div .permissions>
<div class={!c.keyboard ? "disabled" : ""} title="Allow to use keyboard and mouse"><icon .keyboard /></div>
<div class={!c.clipboard ? "disabled" : ""} title="Allow to use clipboard"><icon .clipboard /></div>
<div class={!c.audio ? "disabled" : ""} title="Allow to hear sound"><icon .audio /></div>
</div>}
{c.port_forward ? <div>Port Forwarding: {c.port_forward}</div> : ""}
<div style="size:*"/>
<div .buttons>
{auth ? "" : <button .button tabindex="-1" #accept>Accept</button>}
{auth ? "" : <button .button tabindex="-1" .outline #dismiss>Dismiss</button>}
{auth ? <button .button tabindex="-1" #disconnect>Disconnect</button> : ""}
</div>
{c.is_file_transfer || c.port_forward ? "" : <div .chaticon>{svg_chat}</div>}
</div>
<div .right-panel style={right_style}>
{c.is_file_transfer || c.port_forward ? "" : <ChatBox msgs={c.msgs} callback={callback} />}
</div>
</div>;
}
function sendMsg(text) {
if (!text) return;
var { cid, connection } = this;
checkClickTime(function() {
connection.msgs.push({ name: "me", text: text, time: getNowStr()});
handler.send_msg(cid, text);
body.update();
});
}
event click $(icon.keyboard) (e) {
var { cid, connection } = this;
checkClickTime(function() {
connection.keyboard = !connection.keyboard;
body.update();
handler.switch_permission(cid, "keyboard", connection.keyboard);
});
}
event click $(icon.clipboard) {
var { cid, connection } = this;
checkClickTime(function() {
connection.clipboard = !connection.clipboard;
body.update();
handler.switch_permission(cid, "clipboard", connection.clipboard);
});
}
event click $(icon.audio) {
var { cid, connection } = this;
checkClickTime(function() {
connection.audio = !connection.audio;
body.update();
handler.switch_permission(cid, "audio", connection.audio);
});
}
event click $(button#accept) {
var { cid, connection } = this;
checkClickTime(function() {
connection.authorized = true;
body.update();
handler.authorize(cid);
});
}
event click $(button#dismiss) {
var cid = this.cid;
checkClickTime(function() {
handler.close(cid);
});
}
event click $(button#disconnect) {
var cid = this.cid;
checkClickTime(function() {
handler.close(cid);
});
}
}
$(body).content(<Body />);
var header;
class Header: Reactor.Component
{
function this() {
header = this;
}
function render() {
var me = this;
var conn = connections[body.cur];
if (conn && conn.unreaded > 0) {;
var el = me.select("#unreaded" + conn.id);
if (el) el.style.set {
display: "inline-block",
};
self.timer(300ms, function() {
conn.unreaded = 0;
var el = me.select("#unreaded" + conn.id);
if (el) el.style.set {
display: "none",
};
});
}
var tabs = connections.map(function(c, i) { return me.renderTab(c, i) });
return <div .tabs-wrapper><div .tabs>
{tabs}
</div>
<div .tab-arrows>
<span .left-arrow>&lt;</span>
<span .right-arrow>&gt;</span>
</div>
</div>;
}
function renderTab(c, i) {
var cur = body.cur;
return <div class={i == cur ? "active-tab tab" : "tab"}>
{c.name}
{c.unreaded > 0 ? <span .unreaded id={"unreaded" + c.id}>{c.unreaded}</span> : ""}
</div>;
}
function update_cur(idx) {
checkClickTime(function() {
body.cur = idx;
update();
self.timer(1ms, adjustHeader);
});
}
event click $(div.tab) (_, me) {
var idx = me.index;
if (idx == body.cur) return;
this.update_cur(idx);
}
event click $(span.left-arrow) {
var cur = body.cur;
if (cur == 0) return;
this.update_cur(cur - 1);
}
event click $(span.right-arrow) {
var cur = body.cur;
if (cur == connections.length - 1) return;
this.update_cur(cur + 1);
}
}
if (is_osx) {
$(header).content(<Header />);
$(header).attributes["role"] = "window-caption";
} else {
$(div.window-toolbar).content(<Header />);
setWindowButontsAndIcon(true);
}
event click $(div.chaticon) {
checkClickTime(function() {
show_chat = !show_chat;
adaptSize();
});
}
handler.resetClickCallback = function(ms) {
if (click_callback_time - ms < 120)
click_callback = null;
}
function checkClickTime(callback) {
click_callback_time = getTime();
click_callback = callback;
handler.check_click_time(body.cid);
self.timer(120ms, function() {
if (click_callback) {
click_callback();
click_callback = null;
}
});
}
function adaptSize() {
$(div.right-panel).style.set {
display: show_chat ? "block" : "none",
};
var el = $(div.chaticon);
if (el) el.attributes.toggleClass("active", show_chat);
var (x, y, w, h) = view.box(#rectw, #border, #screen);
if (show_chat && w < 600) {
view.move(x - (600 - w), y, 600, h);
} else if (!show_chat && w > 450) {
view.move(x + (w - 300), y, 300, h);
}
}
function update() {
header.update();
body.update();
}
function bring_to_top(idx=-1) {
if (view.windowState == View.WINDOW_HIDDEN || view.windowState == View.WINDOW_MINIMIZED) {
view.windowState = View.WINDOW_SHOWN;
if (idx >= 0) body.cur = idx;
} else {
view.windowTopmost = true;
view.windowTopmost = false;
}
}
handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio) {
var conn;
connections.map(function(c) {
if (c.id == id) conn = c;
});
if (conn) {
conn.authorized = authorized;
update();
return;
}
if (!name) name = "NA";
connections.push({
id: id, is_file_transfer: is_file_transfer, peer_id: peer_id,
port_forward: port_forward,
name: name, authorized: authorized, time: new Date(),
keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0,
audio: audio,
});
body.cur = connections.length - 1;
bring_to_top();
update();
self.timer(1ms, adjustHeader);
if (authorized) {
self.timer(3s, function() {
view.windowState = View.WINDOW_MINIMIZED;
});
}
}
handler.removeConnection = function(id) {
var i = -1;
connections.map(function(c, idx) {
if (c.id == id) i = idx;
});
connections.splice(i, 1);
if (connections.length == 0) {
handler.exit();
} else {
if (body.cur >= i && body.cur > 0) body.cur -= 1;
update();
}
}
handler.newMessage = function(id, text) {
var idx = -1;
connections.map(function(c, i) {
if (c.id == id) idx = i;
});
var conn = connections[idx];
if (!conn) return;
conn.msgs.push({name: conn.name, text: text, time: getNowStr()});
bring_to_top(idx);
if (idx == body.cur) show_chat = true;
conn.unreaded += 1;
update();
}
handler.awake = function() {
view.windowState = View.WINDOW_SHOWN;
view.focus = self;
}
view << event statechange {
adjustBorder();
}
function self.ready() {
adjustBorder();
var (sw, sh) = view.screenBox(#workarea, #dimension);
var w = 300;
var h = 400;
view.move(sw - w, 0, w, h);
}
function getElaspsed(time) {
var now = new Date();
var seconds = Date.diff(time, now, #seconds);
var hours = seconds / 3600;
var days = hours / 24;
hours = hours % 24;
var minutes = seconds % 3600 / 60;
seconds = seconds % 60;
var out = String.printf("%02d:%02d:%02d", hours, minutes, seconds);
if (days > 0) {
out = String.printf("%d day%s %s", days, days > 1 ? "s" : "", out);
}
return out;
}
function updateTime() {
self.timer(1s, function() {
var el = $(#time);
if (el) {
var c = connections[body.cur];
if (c) {
el.text = getElaspsed(c.time);
}
}
updateTime();
});
}
updateTime();
function self.closing() {
view.windowState = View.WINDOW_HIDDEN;
return false;
}
function adjustHeader() {
var hw = $(header).box(#width);
var tabswrapper = $(div.tabs-wrapper);
var tabs = $(div.tabs);
var arrows = $(div.tab-arrows);
if (!arrows) return;
var n = connections.length;
var wtab = 80;
var max = hw - 98;
var need_width = n * wtab + 2; // include border of active tab
if (need_width < max) {
arrows.style.set {
display: "none",
};
tabs.style.set {
width: need_width,
margin-left: 0,
};
tabswrapper.style.set {
width: need_width,
};
} else {
var margin = (body.cur + 1) * wtab - max + 30;
if (margin < 0) margin = 0;
arrows.style.set {
display: "block",
};
tabs.style.set {
width: (max - 20 + margin) + 'px',
margin-left: -margin + 'px'
};
tabswrapper.style.set {
width: (max + 10) + 'px',
};
}
}
view.on("size", adjustHeader);
// handler.addConnection(0, false, 0, "", "test1", true, false, false, false);
// handler.addConnection(1, false, 0, "", "test2--------", true, false, false, false);
// handler.addConnection(2, false, 0, "", "test3", true, false, false, false);
// handler.newMessage(0, 'h');

319
src/ui/common.css Normal file
View File

@@ -0,0 +1,319 @@
html {
var(accent): #0071ff;
var(button): #2C8CFF;
var(gray-bg): #eee;
var(bg): white;
var(border): #ccc;
var(text): #222;
var(placeholder): #aaa;
var(lighter-text): #888;
var(light-text): #666;
var(dark-red): #A72145;
var(dark-yellow): #FBC732;
var(dark-blue): #2E2459;
var(green-blue): #197260;
var(gray-blue): #2B3439;
var(blue-green): #4299bf;
var(light-green): #D4EAB7;
var(dark-green): #5CB85C;
var(blood-red): #F82600;
}
body {
margin: 0;
color: color(text);
}
button.button {
height: 2em;
border-radius: 0.5em;
background: color(button);
color: color(bg);
border-color: color(button);
min-width: 40px;
}
button[type=checkbox], button[type=checkbox]:active {
background: none;
border: none;
color: unset;
height: 1.4em;
}
button.outline {
border: color(border) solid 1px;
background: transparent;
color: color(text);
}
button.button:active, button.active {
background: color(accent);
color: color(bg);
border-color: color(accent);
}
input[type=text], input[type=password], input[type=number] {
width: *;
font-size: 1.5em;
border-color: color(border);
border-radius: 0;
color: black;
padding-left: 0.5em;
}
input:empty {
color: color(placeholder);
}
input.outline-focus:focus {
outline: color(button) solid 3px;
}
@set my-scrollbar
{
.prev { display:none; }
.next { display:none; }
.base, .next-page, .prev-page { background: white;}
.slider { background: #bbb; border: white solid 4px; }
.base:disabled { background: transparent; }
.slider:hover { background: grey; }
.slider:active { background: grey; }
.base { size: 16px; }
.corner { background: white; }
}
@mixin ELLIPSIS {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
}
div.password svg {
padding-left: 1em;
size: 16px;
color: #ddd;
background: none;
}
div.password input {
font-family: Consolas, Menlo, Monaco, 'Courier New';
font-size: 1.2em;
}
svg {
background: none;
}
header {
border-bottom: color(border) solid 1px;
height: 22px;
flow: horizontal;
overflow-x: hidden;
position: relative;
}
@media platform == "OSX" {
header {
background: linear-gradient(top,#E4E4E4,#D1D1D1);
}
}
header div.window-icon {
size: 22px;
}
@media platform != "OSX" {
header {
background: white;
height: 30px;
}
header div.window-icon {
size: 30px;
}
}
header div.window-icon icon {
display: block;
margin: *;
size: 16px;
background-size: cover;
background-repeat: no-repeat;
}
header caption {
size: *;
}
@media platform != "OSX" {
button.window {
top: 0;
padding: 0 10px;
width: 22px;
height: *;
position: absolute;
margin: 0;
color: black;
border: none;
background: none;
border-radius: 0;
}
button.window div {
size: 10px;
margin: *;
background-size: cover;
background-repeat: no-repeat;
}
button.window:hover {
background: color(gray-bg);
}
button.window#minimize {
right: 84px;
}
button.window#maximize {
right: 42px;
}
button.window#close {
right: 0px;
}
button.window#minimize div {
height: 3px;
border-bottom: black solid 1px;
width: 12px;
}
button.window#maximize div {
border: black solid 1px;
}
button.window#close:hover {
background: #F82600;
}
button.window#close:hover div {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAD///////////////+PQt5oAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC');
}
button.window#close div {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAAAAAAAAAAAAAAAAABPDueNAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC');
size: 12px;
}
button.window#maximize.restore div {
border: none;
size: 12px;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAQMAAABsu86kAAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAB1JREFUCNdjsP/AoCDA8P8CQ0MABipgaHBg+H8AAMfSC36WAZteAAAAAElFTkSuQmCC');
}
}
div.msgbox {
size: *;
}
div.msgbox div.send svg {
size: 16px;
}
div.msgbox div.send span:active {
opacity: 0.5;
}
div.msgbox div.send span {
display: inline-block;
padding: 6px;
}
div.msgbox .msgs {
border: none;
size: *;
border-bottom: color(border) 1px solid;
overflow-x: hidden;
overflow-y: scroll-indicator;
border-spacing: 1em;
padding: 1em;
}
div.msgbox div.send {
flow: horizontal;
height: 30px;
padding: 5px;
}
div.msgbox div.send input {
height: 20px !important;
}
div.msgbox div.name {
color: color(dark-green);
}
div.msgbox div.right-side div {
text-align: right;
}
div.msgbox div.text {
margin-top: 0.5em;
word-wrap: break-word;
word-break: break-all;
}
@media platform != "OSX" {
header .window-toolbar {
width: max-content;
background: transparent;
position: absolute;
bottom: 4px;
height: 24px;
}
}
header svg, menu svg {
size: 14px;
}
header span, menu span {
padding: 4px 8px;
margin: * 0.5em;
color: color(light-text);
}
progress {
display: inline-block;
aspect: Progress;
border: none;
margin-right: 1em;
height: 0.25em;
background: transparent;
}
menu div.separator {
height: 1px;
width: *;
margin: 5px 0;
background: color(gray-bg);
border: none;
}
menu li {
position: relative;
}
menu li span {
display: none;
}
menu li.selected span {
display: inline-block;
position: absolute;
left: -10px;
top: 2px;
}
.link {
cursor: pointer;
text-decoration: underline;
}
.link:active {
opacity: 0.5;
}

297
src/ui/common.tis Normal file
View File

@@ -0,0 +1,297 @@
include "sciter:reactor.tis";
var handler = $(#handler) || view;
try { view.windowIcon = self.url(handler.get_icon()); } catch(e) {}
var OS = view.mediaVar("platform");
var is_osx = OS == "OSX";
var is_win = OS == "Windows";
var is_linux = OS == "Linux";
var is_file_transfer;
function hashCode(str) {
var hash = 160 << 16 + 114 << 8 + 91;
for (var i = 0; i < str.length; i += 1) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
return hash % 16777216;
}
function intToRGB(i, a = 1) {
return 'rgba(' + ((i >> 16) & 0xFF) + ', ' + ((i >> 8) & 0xFF)
+ ',' + (i & 0xFF) + ',' + a + ')';
}
function string2RGB(s, a = 1) {
return intToRGB(hashCode(s), a);
}
function getTime() {
var now = new Date();
return now.valueOf();
}
function platformSvg(platform, color) {
platform = (platform || "").toLowerCase();
if (platform == "linux") {
return <svg viewBox="0 0 256 256">
<g transform="translate(0 256) scale(.1 -.1)" fill={color}>
<path d="m1215 2537c-140-37-242-135-286-278-23-75-23-131 1-383l18-200-54-60c-203-224-383-615-384-831v-51l-66-43c-113-75-194-199-194-300 0-110 99-234 244-305 103-50 185-69 296-69 100 0 156 14 211 54 26 18 35 19 78 10 86-18 233-24 335-12 85 10 222 38 269 56 9 4 19-7 29-35 20-50 52-64 136-57 98 8 180 52 282 156 124 125 180 244 180 380 0 80-28 142-79 179l-36 26 4 119c5 175-22 292-105 460-74 149-142 246-286 409-43 49-78 92-78 97 0 4-7 52-15 107-8 54-19 140-24 189-13 121-41 192-103 260-95 104-248 154-373 122zm172-112c62-19 134-80 163-140 15-31 28-92 41-193 27-214 38-276 57-304 9-14 59-74 111-134 92-106 191-246 236-334 69-137 115-339 101-451l-7-55-71 10c-100 13-234-5-265-36-54-55-85-207-82-412l1-141-51-17c-104-34-245-51-380-45-69 3-142 10-162 16-32 10-37 17-53 68-23 72-87 201-136 273-80 117-158 188-237 215-37 13-37 13-34 61 13 211 182 555 373 759 57 62 58 63 58 121 0 33-9 149-19 259-21 224-18 266 26 347 67 122 193 174 330 133zm687-1720c32-9 71-25 87-36 60-42 59-151-4-274-59-119-221-250-317-257-34-3-35-2-48 47-18 65-20 329-3 413 16 83 29 110 55 115 51 10 177 6 230-8zm-1418-80c79-46 187-195 247-340 41-99 43-121 12-141-39-25-148-30-238-10-142 32-264 112-307 202-20 41-21 50-10 87 24 83 102 166 192 207 54 25 53 25 104-5z"/>
<path d="m1395 1945c-92-16-220-52-256-70-28-15-29-18-29-89 0-247 165-397 345-312 60 28 77 46 106 111 54 123 0 378-80 374-9 0-47-7-86-14zm74-156c15-69 14-112-5-159s-55-70-111-70c-48 0-78 20-102 68-15 29-41 131-41 159 0 9 230 63 242 57 3-2 11-27 17-55z"/>
</g>
</svg>;
}
if (platform == "mac os") {
return <svg viewBox="0 0 384 512">
<path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" fill={color}/>
</svg>;
}
return <svg viewBox="0 0 448 512">
<path d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z" fill={color}/>
</svg>;
}
function centerize(w, h) {
var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw);
if (w > sw) w = sw;
if (h > sh) h = sh;
var x = (sx + sw - w) / 2;
var y = (sy + sh - h) / 2;
view.move(x, y, w, h);
}
function setWindowButontsAndIcon(only_min=false) {
if (only_min) {
$(div.window-buttons).content(<div>
<button.window tabindex="-1" role="window-minimize" #minimize><div /></button>
</div>);
} else {
$(div.window-buttons).content(<div>
<button.window tabindex="-1" role="window-minimize" #minimize><div /></button>
<button.window tabindex="-1" role="window-maximize" #maximize><div /></button>
<button.window tabindex="-1" role="window-close" #close><div /></button>
</div>);
}
$(div.window-icon>icon).style.set {
"background-image": "url('" + handler.get_icon() + "')",
};
}
function adjustBorder() {
if (is_osx) {
if (view.windowState == View.WINDOW_FULL_SCREEN) {
$(header).style.set {
display: "none",
};
} else {
$(header).style.set {
display: "block",
padding: "0",
};
}
return;
}
if (view.windowState == view.WINDOW_MAXIMIZED) {
self.style.set {
border: "window-frame-width solid transparent",
};
} else if (view.windowState == view.WINDOW_FULL_SCREEN) {
self.style.set {
border: "none",
};
} else {
self.style.set {
border: "black solid 1px",
};
}
var el = $(button#maximize);
if (el) el.attributes.toggleClass("restore", view.windowState == View.WINDOW_MAXIMIZED);
el = $(span#fullscreen);
if (el) el.attributes.toggleClass("active", view.windowState == View.WINDOW_FULL_SCREEN);
}
var svg_checkmark = <svg viewBox="0 0 492 492"><path d="M484 105l-16-17a27 27 0 00-38 0L204 315 62 173c-5-5-12-7-19-7s-14 2-19 7L8 189a27 27 0 000 38l160 160v1l16 16c5 5 12 8 19 8 8 0 14-3 20-8l16-16v-1l245-244a27 27 0 000-38z"/></svg>;
var svg_edit = <svg #edit viewBox="0 0 384 384">
<path d="M0 304v80h80l236-236-80-80zM378 56L328 6c-8-8-22-8-30 0l-39 39 80 80 39-39c8-8 8-22 0-30z"/>
</svg>;
var svg_eye = <svg viewBox="0 0 469.33 469.33">
<path d="m234.67 170.67c-35.307 0-64 28.693-64 64s28.693 64 64 64 64-28.693 64-64-28.694-64-64-64z"/>
<path d="m234.67 74.667c-106.67 0-197.76 66.346-234.67 160 36.907 93.653 128 160 234.67 160 106.77 0 197.76-66.347 234.67-160-36.907-93.654-127.89-160-234.67-160zm0 266.67c-58.88 0-106.67-47.787-106.67-106.67s47.787-106.67 106.67-106.67 106.67 47.787 106.67 106.67-47.787 106.67-106.67 106.67z"/>
</svg>;
var svg_send = <svg viewBox="0 0 448 448">
<polygon points="0.213 32 0 181.33 320 224 0 266.67 0.213 416 448 224"/>
</svg>;
var svg_chat = <svg viewBox="0 0 511.07 511.07">
<path d="m74.39 480.54h-36.213l25.607-25.607c13.807-13.807 22.429-31.765 24.747-51.246-36.029-23.644-62.375-54.751-76.478-90.425-14.093-35.647-15.864-74.888-5.121-113.48 12.89-46.309 43.123-88.518 85.128-118.85 45.646-32.963 102.47-50.387 164.33-50.387 77.927 0 143.61 22.389 189.95 64.745 41.744 38.159 64.734 89.63 64.734 144.93 0 26.868-5.471 53.011-16.26 77.703-11.165 25.551-27.514 48.302-48.593 67.619-46.399 42.523-112.04 65-189.83 65-28.877 0-59.01-3.855-85.913-10.929-25.465 26.123-59.972 40.929-96.086 40.929zm182-420c-124.04 0-200.15 73.973-220.56 147.28-19.284 69.28 9.143 134.74 76.043 175.12l7.475 4.511-0.23 8.727c-0.456 17.274-4.574 33.912-11.945 48.952 17.949-6.073 34.236-17.083 46.99-32.151l6.342-7.493 9.405 2.813c26.393 7.894 57.104 12.241 86.477 12.241 154.37 0 224.68-93.473 224.68-180.32 0-46.776-19.524-90.384-54.976-122.79-40.713-37.216-99.397-56.888-169.71-56.888z"/>
</svg>;
function scrollToBottom(el) {
var y = el.box(#height, #content) - el.box(#height, #client);
el.scrollTo(0, y);
}
function getNowStr() {
var now = new Date();
return String.printf("%02d:%02d:%02d", now.hour, now.minute, now.second);
}
/******************** start of chatbox ****************************************/
class ChatBox: Reactor.Component {
this var msgs = [];
this var callback;
function this(params) {
if (params) {
this.msgs = params.msgs || [];
this.callback = params.callback;
}
}
function renderMsg(msg) {
var cls = msg.name == "me" ? "right-side msg" : "left-side msg";
return <div class={cls}>
{msg.name == "me" ?
<div .name>{msg.time + " "} me</div> :
<div .name>{msg.name} {" " + msg.time}</div>
}
<div .text>{msg.text}</div>
</div>;
}
function render() {
var me = this;
var msgs = this.msgs.map(function(msg) { return me.renderMsg(msg); });
self.timer(1ms, function() {
scrollToBottom(me.msgs);
});
return <div .msgbox>
<htmlarea spellcheck="false" readonly .msgs @{this.msgs} >
{msgs}
</htmlarea>
<div .send>
<input|text .outline-focus />
<span>{svg_send}</span>
</div>
</div>;
}
function send() {
var el = this.$(input);
var value = (el.value || "").trim();
el.value = "";
if (!value) return;
if (this.callback) this.callback(value);
}
event keydown $(input) (evt) {
if (!evt.shortcutKey) {
if (evt.keyCode == Event.VK_ENTER ||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
this.send();
}
}
}
event click $(div.send span) {
this.send();
view.focus = $(input);
}
}
/******************** end of chatbox ****************************************/
/******************** start of msgbox ****************************************/
var remember_password = false;
var msgbox_params;
function getMsgboxParams() {
return msgbox_params;
}
function msgbox(type, title, text, callback, height, width) {
var has_msgbox = msgbox_params != null;
if (!has_msgbox && !type) return;
var remember = false;
try {
remember = handler.get_remember();
} catch(e) {}
msgbox_params = {
remember: remember, type: type, text: text, title: title,
getParams: getMsgboxParams,
callback: callback
};
if (has_msgbox) return;
var dialog = {
client: true,
parameters: msgbox_params,
width: width,
height: height,
};
var html = handler.get_msgbox();
if (html) dialog.html = html;
else dialog.url = self.url("msgbox.html");
var res = view.dialog(dialog);
msgbox_params = null;
stdout.printf("msgbox return, type: %s, res: %s\n", type, res);
if (type.indexOf("custom") >= 0) {
//
} else if (!res) {
if (!is_port_forward) view.close();
} else if (res == "!alive") {
// do nothing
} else if (res.type == "input-password") {
if (!is_port_forward) connecting();
handler.login(res.password, res.remember);
} else if (res.reconnect) {
if (!is_port_forward) connecting();
handler.reconnect();
}
}
function connecting() {
handler.msgbox("connecting", "Connecting...", "Connection in progress. Please wait.");
}
handler.msgbox = function(type, title, text, callback=null, height=180, width=500) {
// directly call view.Dialog from native may crash, add timer here, seem safe
// too short time, msgbox won't get focus, per my test, 150 is almost minimun
self.timer(150ms, function() { msgbox(type, title, text, callback, height, width); });
}
/******************** end of msgbox ****************************************/
function Progress()
{
var _val;
var pos = -0.25;
function step() {
if( _val !== undefined ) { this.refresh(); return false; }
pos += 0.02;
if( pos > 1.25)
pos = -0.25;
this.refresh();
return true;
}
function paintNoValue(gfx)
{
var (w,h) = this.box(#dimension,#inner);
var x = pos * w;
w = w * 0.25;
gfx.fillColor( this.style#color )
.pushLayer(#inner-box)
.rectangle(x,0,w,h)
.popLayer();
return true;
}
this[#value] = property(v) {
get return _val;
set {
_val = undefined;
pos = -0.25;
this.paintContent = paintNoValue;
this.animate(step);
this.refresh();
}
}
this.value = "";
}

255
src/ui/file_transfer.css Normal file
View File

@@ -0,0 +1,255 @@
div#file-transfer-wrapper {
size:*;
display: none;
}
div#file-transfer {
size: *;
margin: 0;
flow: horizontal;
background: color(gray-bg);
padding: 0.5em;
}
table
{
font: system;
border: 1px solid color(border);
flow: table-fixed;
prototype: Grid;
size: *;
padding:0;
border-spacing: 0;
overflow-x: auto;
overflow-y: hidden;
}
table > thead {
behavior: column-resizer;
border-bottom: color(border) solid 1px;
}
table > tbody {
overflow-y: scroll-indicator;
size: *;
background: white;
}
table th {
background-color: color(gray-bg);
}
table th
{
padding: 4px;
foreground-repeat: no-repeat;
foreground-position: 50% 3px auto auto;
border-left: color(border) solid 1px;
}
table th.sortable[sort=asc]
{
foreground-image: url(stock:arrow-down);
}
table th.sortable[sort=desc]
{
foreground-image: url(stock:arrow-up);
}
table th:nth-child(1) {
width: 32px;
}
table th:nth-child(2) {
width: *;
}
table th:nth-child(3) {
width: *;
}
table th:nth-child(4) {
width: 45px;
}
table.has_current thead th:current {
font-weight: bold;
}
table tr:nth-child(odd) { background-color: white; } /* each odd row */
table tr:nth-child(even) { background-color: #F4F5F6; } /* each even row */
table.has_current tr:current /* current row */
{
background-color: color(accent);
}
table td
{
padding: 4px;
text-align: left;
font-size: 1em;
height: 1.4em;
@ELLIPSIS;
}
table.folder-view td:nth-child(1) {
behavior:shell-icon;
}
table td:nth-child(3), table td:nth-child(4) {
color: color(lighter-text);
font-size: 0.9em;
}
table.has_current tr:current td {
color: white;
}
table td:nth-child(4) {
text-align: right;
}
section {
size: *;
margin: 1em;
border-spacing: 0.5em;
}
table td:nth-child(1) {
foreground-repeat: no-repeat;
foreground-position: 50% 50%
}
div.toolbar {
flow: horizontal;
}
div.toolbar svg {
size: 16px;
}
div.toolbar .spacer {
width: *;
}
div.toolbar > div.button {
padding: 4px 8px;
opacity: 0.66;
}
div.toolbar > div.button:active {
opacity: 1;
background-color: #ddd;
}
div.toolbar > div.button:hover {
opacity: 1;
}
div.toolbar > div.send {
flow: horizontal;
border-spacing: 0.5em;
}
div.remote > div.send svg {
transform: scale(-1, 1);
}
div.navbar {
border: color(border) solid 1px;
padding: 4px 0;
}
select.select-dir {
width: *;
padding: 0 4px;
}
div.title {
flow: horizontal;
border-spacing: 1em;
position: relative;
}
div.title svg.computer {
size: 48px;
}
div.title div {
margin: * 0;
color: color(light-text);
}
div.title div.platform {
position: absolute;
left: 12px;
top: 7px;
}
div.title div.platform svg {
size: 24px;
}
table.job-table tr td {
width: *;
padding: 0.5em 1em;
border-bottom: color(border) 1px solid;
flow: horizontal;
border-spacing: 1em;
height: 3em;
overflow-x: hidden;
}
table.job-table tr svg {
size: 16px;
}
table.job-table tr.is_remote svg {
transform: scale(-1, 1);
}
table.job-table tr td div.text {
width: *;
overflow-x: hidden;
}
table.job-table tr td div.path {
width: *;
color: color(light-text);
@ELLIPSIS;
}
table.job-table tr:current td div.path {
color: white;
}
table#port-forward thead tr th {
padding-left: 1em;
size: *;
}
table#port-forward tr td {
height: 3em;
text-align: left;
}
table#port-forward input[type=text], table#port-forward input[type=number] {
font-size: 1.2em;
}
table#port-forward td.right-arrow svg {
size: 1.2em;
transform: rotate(180deg);
}
table#port-forward td.remove svg {
size: 0.8em;
}
table#port-forward tr.value td {
padding-left: 1em;
font-size: 1.5em;
color: black;
}

617
src/ui/file_transfer.tis Normal file
View File

@@ -0,0 +1,617 @@
var remote_home_dir;
var svg_add_folder = <svg viewBox="0 0 443.29 443.29">
<path d="m277.06 332.47h27.706v-55.412h55.412v-27.706h-55.412v-55.412h-27.706v55.412h-55.412v27.706h55.412z"/>
<path d="m415.59 83.118h-202.06l-51.353-51.353c-2.597-2.597-6.115-4.058-9.794-4.058h-124.68c-15.274-1e-3 -27.706 12.431-27.706 27.705v332.47c0 15.273 12.432 27.706 27.706 27.706h387.88c15.273 0 27.706-12.432 27.706-27.706v-277.06c0-15.274-12.432-27.706-27.706-27.706zm0 304.76h-387.88v-332.47h118.94l51.354 51.353c2.597 2.597 6.115 4.058 9.794 4.058h207.79z"/>
</svg>;
var svg_trash = <svg viewBox="0 0 473.41 473.41">
<path d="m443.82 88.765h-88.765v-73.971c0-8.177-6.617-14.794-14.794-14.794h-207.12c-8.177 0-14.794 6.617-14.794 14.794v73.971h-88.764v29.588h14.39l57.116 342.69c1.185 7.137 7.354 12.367 14.592 12.367h241.64c7.238 0 13.407-5.23 14.592-12.367l57.116-342.69h14.794c-1e-3 0-1e-3 -29.588-1e-3 -29.588zm-295.88-59.177h177.53v59.176h-177.53zm196.85 414.24h-216.58l-54.241-325.47h325.06z"/>
<path transform="matrix(.064 -.998 .998 .064 -.546 19.418)" d="m-360.4 301.29h207.54v29.592h-207.54z"/>
<path transform="matrix(.998 -.064 .064 .998 -.628 .399)" d="m141.64 202.35h29.592v207.54h-29.592z"/>
</svg>;
var svg_arrow = <svg viewBox="0 0 482.24 482.24">
<path d="m206.81 447.79-206.81-206.67 206.74-206.67 24.353 24.284-165.17 165.17h416.31v34.445h-416.31l165.24 165.24z"/>
</svg>;
var svg_home = <svg viewBox="0 0 476.91 476.91">
<path d="m461.78 209.41-212.21-204.89c-6.182-6.026-16.042-6.026-22.224 0l-212.2 204.88c-3.124 3.015-4.888 7.17-4.888 11.512 0 8.837 7.164 16 16 16h28.2v224c0 8.837 7.163 16 16 16h112c8.837 0 16-7.163 16-16v-128h80v128c0 8.837 7.163 16 16 16h112c8.837 0 16-7.163 16-16v-224h28.2c4.338 0 8.489-1.761 11.504-4.88 6.141-6.354 5.969-16.483-0.384-22.624zm-39.32 11.504c-8.837 0-16 7.163-16 16v224h-112v-128c0-8.837-7.163-16-16-16h-80c-8.837 0-16 7.163-16 16v128h-112v-224c0-8.837-7.163-16-16-16h-28.2l212.2-204.88 212.28 204.88h-28.28z"/>
</svg>;
var svg_refresh = <svg viewBox="0 0 551.13 551.13">
<path d="m482.24 310.01c0 113.97-92.707 206.67-206.67 206.67s-206.67-92.708-206.67-206.67c0-102.21 74.639-187.09 172.23-203.56v65.78l86.114-86.114-86.114-86.115v71.641c-116.65 16.802-206.67 117.14-206.67 238.37 0 132.96 108.16 241.12 241.12 241.12s241.12-108.16 241.12-241.12z"/>
</svg>;
var svg_cancel = <svg .cancel viewBox="0 0 612 612"><polygon points="612 36.004 576.52 0.603 306 270.61 35.478 0.603 0 36.004 270.52 306.01 0 576 35.478 611.4 306 341.41 576.52 611.4 612 576 341.46 306.01"/></svg>;
var svg_computer = <svg .computer viewBox="0 0 480 480">
<g>
<path fill="#2C8CFF" d="m276 395v11.148c0 2.327-1.978 4.15-4.299 3.985-21.145-1.506-42.392-1.509-63.401-0.011-2.322 0.166-4.3-1.657-4.3-3.985v-11.137c0-2.209 1.791-4 4-4h64c2.209 0 4 1.791 4 4zm204-340v288c0 17.65-14.35 32-32 32h-416c-17.65 0-32-14.35-32-32v-288c0-17.65 14.35-32 32-32h416c17.65 0 32 14.35 32 32zm-125.62 386.36c-70.231-21.843-158.71-21.784-228.76 0-4.22 1.31-6.57 5.8-5.26 10.02 1.278 4.085 5.639 6.591 10.02 5.26 66.093-20.58 151.37-21.125 219.24 0 4.22 1.31 8.71-1.04 10.02-5.26s-1.04-8.71-5.26-10.02z"/>
</g>
</svg>;
function getSize(type, size) {
if (!size) {
if (type <= 3) return "";
return "0B";
}
size = size.toFloat();
var toFixed = function(size) {
size = (size * 100).toInteger();
var a = (size / 100).toInteger();
if (size % 100 == 0) return a;
if (size % 10 == 0) return a + '.' + (size % 10);
var b = size % 100;
if (b < 10) b = '0' + b;
return a + '.' + b;
}
if (size < 1024) return size.toInteger() + "B";
if (size < 1024 * 1024) return toFixed(size / 1024) + "K";
if (size < 1024 * 1024 * 1024) return toFixed(size / (1024 * 1024)) + "M";
return toFixed(size / (1024 * 1024 * 1024)) + "G";
}
function getParentPath(is_remote, path) {
var sep = handler.get_path_sep(is_remote);
var res = path.lastIndexOf(sep);
if (res <= 0) return "/";
return path.substr(0, res);
}
function getFileName(is_remote, path) {
var sep = handler.get_path_sep(is_remote);
var res = path.lastIndexOf(sep);
return path.substr(res + 1);
}
function getExt(name) {
if (name.indexOf(".") == 0) {
return "";
}
var i = name.lastIndexOf(".");
if (i > 0) return name.substr(i + 1);
return "";
}
var jobIdCounter = 1;
class JobTable: Reactor.Component {
this var jobs = [];
this var job_map = {};
function render() {
var me = this;
var rows = this.jobs.map(function(job, i) { return me.renderRow(job, i); });
return <section><table .has_current .job-table>
<tbody key={rows.length}>
{rows}
</tbody>
</table></section>;
}
event click $(svg.cancel) (_, me) {
var job = this.jobs[me.parent.parent.index];
var id = job.id;
handler.cancel_job(id);
delete this.job_map[id];
var i = -1;
this.jobs.map(function(job, idx) {
if (job.id == id) i = idx;
});
this.jobs.splice(i, 1);
this.update();
var is_remote = job.is_remote;
if (job.type != "del-dir") is_remote = !is_remote;
refreshDir(is_remote);
}
function send(path, is_remote) {
var to;
var show_hidden;
if (is_remote) {
to = file_transfer.local_folder_view.fd.path;
show_hidden = file_transfer.remote_folder_view.show_hidden;
} else {
to = file_transfer.remote_folder_view.fd.path;
show_hidden = file_transfer.local_folder_view.show_hidden;
}
if (!to) return;
to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path);
var id = jobIdCounter;
jobIdCounter += 1;
this.jobs.push({ type: "transfer",
id: id, path: path, to: to,
include_hidden: show_hidden,
is_remote: is_remote });
this.job_map[id] = this.jobs[this.jobs.length - 1];
handler.send_files(id, path, to, show_hidden, is_remote);
this.update();
}
function addDelDir(path, is_remote) {
var id = jobIdCounter;
jobIdCounter += 1;
this.jobs.push({ type: "del-dir", id: id, path: path, is_remote: is_remote });
this.job_map[id] = this.jobs[this.jobs.length - 1];
handler.remove_dir_all(id, path, is_remote);
this.update();
}
function getSvg(job) {
if (job.type == "transfer") {
return svg_send;
} else if (job.type == "del-dir") {
return svg_trash;
}
}
function getStatus(job) {
if (!job.entries) return "Waiting";
var i = job.file_num + 1;
var n = job.num_entries || job.entries.length;
if (i > n) i = n;
var res = i + ' / ' + n + " files";
if (job.total_size > 0) res += ", " + getSize(0, job.finished_size) + ' / ' + getSize(0, job.total_size);
// below has problem if some file skipped
var percent = (100. * job.finished_size / job.total_size).toInteger(); // (100. * i / (n || 1)).toInteger();
if (job.finished) percent = '100';
res += ", " + percent + "%";
if (job.finished) res = "Finished " + res;
if (job.speed) res += ", " + getSize(0, job.speed) + "/s";
return res;
}
function updateJob(job) {
var el = this.select("div[id=s" + job.id + "]");
if (el) el.text = this.getStatus(job);
}
function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) {
var job = this.job_map[id];
if (!job) return;
if (file_num < job.file_num) return;
job.file_num = file_num;
var n = job.num_entries || job.entries.length;
job.finished = job.file_num >= n - 1 || err == "cancel";
job.finished_size = finished_size;
job.speed = speed || 0;
this.updateJob(job);
if (job.type == "del-dir") {
if (job.finished) {
if (!err) {
handler.remove_dir(job.id, job.path, job.is_remote);
refreshDir(job.is_remote);
}
} else if (!job.no_confirm) {
handler.confirm_delete_files(id, job.file_num + 1);
}
} else if (job.finished || file_num == -1) {
refreshDir(!job.is_remote);
}
}
function renderRow(job, i) {
var svg = this.getSvg(job);
return <tr class={job.is_remote ? "is_remote" : ""}><td>
{svg}
<div .text>
<div .path>{job.path}</div>
<div id={"s" + job.id}>{this.getStatus(job)}</div>
</div>
{svg_cancel}
</td></tr>;
}
}
class FolderView : Reactor.Component {
this var fd = {};
this var history = [];
this var show_hidden = false;
function sep() {
return handler.get_path_sep(this.is_remote);
}
function this(params) {
this.is_remote = params.is_remote;
if (this.is_remote) {
this.show_hidden = !!handler.get_option("remote_show_hidden");
} else {
this.show_hidden = !!handler.get_option("local_show_hidden");
}
if (!this.is_remote) {
var dir = handler.get_option("local_dir");
if (dir) {
this.fd = handler.read_dir(dir, this.show_hidden);
if (this.fd) return;
}
this.fd = handler.read_dir(handler.get_home_dir(), this.show_hidden);
}
}
// sort predicate
function foldersFirst(a, b) {
if (a.type <= 3 && b.type > 3) return -1;
if (a.type > 3 && b.type <= 3) return +1;
if (a.name == b.name) return 0;
return a.name.toLowerCase().lexicalCompare(b.name.toLowerCase());
}
function render()
{
return <section>
{this.renderTitle()}
{this.renderNavBar()}
{this.renderOpBar()}
{this.renderTable()}
</section>;
}
function renderTitle() {
return <div .title>
{svg_computer}
<div .platform>{platformSvg(handler.get_platform(this.is_remote), "white")}</div>
<div><span>{this.is_remote ? "Remote Computer" : "Local Computer"}</span></div>
</div>
}
function renderNavBar() {
return <div .toolbar .navbar>
<div .home .button>{svg_home}</div>
<div .goback .button>{svg_arrow}</div>
<div .goup .button>{svg_arrow}</div>
{this.renderSelect()}
<div .refresh .button>{svg_refresh}</div>
</div>;
}
function renderSelect() {
return <select editable .select-dir @{this.select_dir}>
<option>/</option>
</select>;
}
function renderOpBar() {
if (this.is_remote) {
return <div .toolbar .remote>
<div .send .button>{svg_send}<span>Receive</span></div>
<div .spacer></div>
<div .add-folder .button>{svg_add_folder}</div>
<div .trash .button>{svg_trash}</div>
</div>;
}
return <div .toolbar>
<div .add-folder .button>{svg_add_folder}</div>
<div .trash .button>{svg_trash}</div>
<div .spacer></div>
<div .send .button><span>Send</span>{svg_send}</div>
</div>;
}
function get_updated() {
this.table.sortRows(false);
if (this.fd && this.fd.path) this.select_dir.value = this.fd.path;
}
function renderTable() {
var fd = this.fd;
var entries = fd.entries || [];
var table = this.table;
if (!table || !table.sortBy) {
entries.sort(this.foldersFirst);
}
var me = this;
var path = fd.path;
if (path != "/" && path) {
entries = [{ name: "..", type: 1 }].concat(entries);
}
var rows = entries.map(function(e) { return me.renderRow(e); });
var id = (this.is_remote ? "remote" : "local") + "-folder-view";
return <table @{this.table} .folder-view .has_current id={id}>
<thead>
<tr><th></th><th .sortable>Name</th><th .sortable>Modified</th><th .sortable>Size</th></tr>
</thead>
<tbody>
{rows}
</tbody>
<popup>
<menu.context id={id}>
<li #switch-hidden class={this.show_hidden ? "selected" : ""}><span>{svg_checkmark}</span>Show Hidden Files</li>
</menu>
</popup>
</table>;
}
function joinPath(name) {
var path = this.fd.path;
if (path == "/") {
if (this.sep() == "/") return this.sep() + name;
else return name;
}
return path + (path[path.length - 1] == this.sep() ? "" : this.sep()) + name;
}
function attached() {
var me = this;
this.table.onRowDoubleClick = function (row) {
var type = row[0].attributes["type"];
if (type > 3) return;
var name = row[1].text;
var path = name == ".." ? getParentPath(me.is_remote, me.fd.path) : me.joinPath(name);
me.goto(path, true);
}
this.get_updated();
}
function goto(path, push) {
if (!path) return;
if (this.sep() == "\\" && path.length == 2) { // windows drive
path += "\\";
}
if (push) this.pushHistory();
if (this.is_remote) {
handler.read_remote_dir(path, this.show_hidden);
} else {
var fd = handler.read_dir(path, this.show_hidden);
this.refresh({ fd: fd });
}
}
function refresh(data) {
if (!data.fd || !data.fd.path) return;
if (this.is_remote && !remote_home_dir) {
remote_home_dir = data.fd.path;
}
this.update(data);
var me = this;
self.timer(1ms, function() { me.get_updated(); });
}
function renderRow(entry) {
var path;
if (this.is_remote) {
path = handler.get_icon_path(entry.type, getExt(entry.name));
} else {
path = this.joinPath(entry.name);
}
var tm = entry.time ? new Date(entry.time.toFloat() * 1000.).toLocaleString() : 0;
return <tr>
<td type={entry.type} filename={path}></td>
<td>{entry.name}</td>
<td value={entry.time || 0}>{tm || ""}</td>
<td value={entry.size || 0}>{getSize(entry.type, entry.size)}</td>
</tr>;
}
event click $(#switch-hidden) {
this.show_hidden = !this.show_hidden;
this.refreshDir();
}
event click $(.goup) () {
var path = this.fd.path;
if (!path || path == "/") return;
path = getParentPath(this.is_remote, path);
this.goto(path, true);
}
event click $(.goback) () {
var path = this.history.pop();
if (!path) return;
this.goto(path, false);
}
event click $(.trash) () {
var row = this.getCurrentRow();
if (!row) return;
var path = row[0];
var type = row[1];
var new_history = [];
for (var i = 0; i < this.history.length; ++i) {
var h = this.history[i];
if ((h + this.sep()).indexOf(path + this.sep()) == -1) new_history.push(h);
}
this.history = new_history;
if (type == 1) {
file_transfer.job_table.addDelDir(path, this.is_remote);
} else {
confirmDelete(path, this.is_remote);
}
}
event click $(.add-folder) () {
var me = this;
handler.msgbox("custom", "Create Folder", "<div .form> \
<div>Please enter the folder name:</div> \
<div><input|text(name) .outline-focus /></div> \
</div>", function(res=null) {
if (!res) return;
if (!res.name) return;
var name = res.name.trim();
if (!name) return;
if (name.indexOf(me.sep()) >= 0) {
handler.msgbox("custom-error", "Create Folder", "Invalid folder name");
return;
}
var path = me.joinPath(name);
handler.create_dir(jobIdCounter, path, me.is_remote);
create_dir_jobs[jobIdCounter] = { is_remote: me.is_remote, path: path };
jobIdCounter += 1;
});
}
function refreshDir() {
this.goto(this.fd.path, false);
}
event click $(.refresh) () {
this.refreshDir();
}
event click $(.home) () {
var path = this.is_remote ? remote_home_dir : handler.get_home_dir();
if (!path) return;
if (path == this.fd.path) return;
this.goto(path, true);
}
function getCurrentRow() {
var row = this.table.getCurrentRow();
if (!row) return;
var name = row[1].text;
if (!name || name == "..") return;
var type = row[0].attributes["type"];
return [this.joinPath(name), type];
}
event click $(.send) () {
var cur = this.getCurrentRow();
if (!cur) return;
file_transfer.job_table.send(cur[0], this.is_remote);
}
event change $(.select-dir) (_, el) {
var x = getTime() - last_key_time;
if (x < 1000) return;
if (this.fd.path != el.value) {
this.goto(el.value, true);
}
}
event keydown $(.select-dir) (evt, me) {
if (evt.keyCode == Event.VK_ENTER ||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
this.goto(me.value, true);
}
}
function pushHistory() {
var path = this.fd.path;
if (!path) return;
if (path != this.history[this.history.length - 1]) this.history.push(path);
}
}
var file_transfer;
class FileTransfer: Reactor.Component {
function this(params) {
file_transfer = this;
}
function render() {
return <div #file-transfer>
<FolderView is_remote={false} @{this.local_folder_view} />
<FolderView is_remote={true} @{this.remote_folder_view}/>
<JobTable @{this.job_table} />
</div>;
}
}
function initializeFileTransfer()
{
$(#file-transfer-wrapper).content(<FileTransfer />);
$(#video-wrapper).style.set { visibility: "hidden", position: "absolute" };
$(#file-transfer-wrapper).style.set { display: "block" };
}
handler.updateFolderFiles = function(fd) {
fd.entries = fd.entries || [];
if (fd.id > 0) {
var jt = file_transfer.job_table;
var job = jt.job_map[fd.id];
if (job) {
job.file_num = -1;
job.total_size = fd.total_size;
job.entries = fd.entries;
job.num_entries = fd.num_entries;
file_transfer.job_table.updateJobStatus(job.id);
}
} else {
file_transfer.remote_folder_view.refresh({ fd: fd });
}
}
handler.jobProgress = function(id, file_num, speed, finished_size) {
file_transfer.job_table.updateJobStatus(id, file_num, null, speed, finished_size);
}
handler.jobDone = function(id, file_num = -1) {
var job = deleting_single_file_jobs[id] || create_dir_jobs[id];
if (job) {
refreshDir(job.is_remote);
return;
}
file_transfer.job_table.updateJobStatus(id, file_num);
}
handler.jobError = function(id, err, file_num = -1) {
var job = deleting_single_file_jobs[id];
if (job) {
handler.msgbox("custom-error", "Delete File", err);
return;
}
job = create_dir_jobs[id];
if (job) {
handler.msgbox("custom-error", "Create Folder", err);
return;
}
if (file_num < 0) {
handler.msgbox("custom-error", "Failed", err);
}
file_transfer.job_table.updateJobStatus(id, file_num, err);
}
function refreshDir(is_remote) {
if (is_remote) file_transfer.remote_folder_view.refreshDir();
else file_transfer.local_folder_view.refreshDir();
}
var deleting_single_file_jobs = {};
var create_dir_jobs = {}
function confirmDelete(path, is_remote) {
handler.msgbox("custom-skip", "Confirm Delete", "<div .form> \
<div>Are you sure you want to deelte this file?</div> \
<div.ellipsis style=\"font-weight: bold;\">" + path + "</div> \
</div>", function(res=null) {
if (res) {
handler.remove_file(jobIdCounter, path, 0, is_remote);
deleting_single_file_jobs[jobIdCounter] = { is_remote: is_remote, path: path };
jobIdCounter += 1;
}
});
}
handler.confirmDeleteFiles = function(id, i, name) {
var jt = file_transfer.job_table;
var job = jt.job_map[id];
if (!job) return;
var n = job.num_entries;
if (i >= n) return;
var file_path = job.path;
if (name) file_path += handler.get_path_sep(job.is_remote) + name;
handler.msgbox("custom-skip", "Confirm Delete", "<div .form> \
<div>Deleting #" + (i + 1) + " of " + n + " files.</div> \
<div>Are you sure you want to deelte this file?</div> \
<div.ellipsis style=\"font-weight: bold;\" .text>" + name + "</div> \
<div><button|checkbox(remember) {ts}>Do this for all conflicts</button></div> \
</div>", function(res=null) {
if (!res) {
jt.updateJobStatus(id, i - 1, "cancel");
} else if (res.skip) {
if (res.remember) jt.updateJobStatus(id, i, "cancel");
else handler.jobDone(id, i);
} else {
job.no_confirm = res.remember;
if (job.no_confirm) handler.set_no_confirm(id);
handler.remove_file(id, file_path, i, job.is_remote);
}
});
}
function save_file_transfer_close_state() {
var local_dir = file_transfer.local_folder_view.fd.path || "";
var local_show_hidden = file_transfer.local_folder_view.show_hidden ? "Y" : "";
var remote_dir = file_transfer.remote_folder_view.fd.path || "";
var remote_show_hidden = file_transfer.remote_folder_view.show_hidden ? "Y" : "";
handler.save_close_state("local_dir", local_dir);
handler.save_close_state("local_show_hidden", local_show_hidden);
handler.save_close_state("remote_dir", remote_dir);
handler.save_close_state("remote_show_hidden", remote_show_hidden);
}

234
src/ui/grid.tis Normal file
View File

@@ -0,0 +1,234 @@
class Grid: Behavior {
const TABLE_HEADER_CLICK = 0x81;
const TABLE_ROW_CLICK = 0x82;
const TABLE_ROW_DBL_CLICK = 0x83;
function onHeaderClick(headerCell)
{
this.postEvent(TABLE_HEADER_CLICK, headerCell.index, headerCell);
return true;
}
function onRowClick(row , reason)
{
this.postEvent(TABLE_ROW_CLICK, row.index, row);
return true;
}
function onRowDoubleClick(row)
{
this.postEvent(TABLE_ROW_DBL_CLICK, row.index, row);
return true;
}
function getCurrentRow()
{
return this.$(tbody>tr:current);
}
function getCurrentColumn()
{
return this.$(thead>:current); // return current cell in header row
}
function setCurrentRow(row, reason = #by_code, doubleClick = false)
{
if (!row) return;
// get previously selected row:
var prev = this.getCurrentRow();
if (prev)
{
if (prev === row && !doubleClick) return; // already here, nothing to do.
prev.state.current = false; // drop state flag
}
row.state.current = true;
row.scrollToView();
if (doubleClick)
this.onRowDoubleClick(row,reason);
else
this.onRowClick(row,reason);
}
function setCurrentColumn(col)
{
// get previously selected column:
var prev = this.getCurrentColumn();
if (prev)
{
if (prev === col) return; // already here, nothing to do.
prev.state.current = false; // drop state flag
}
col.state.current = true; // set state flag
col.scrollToView();
this.onHeaderClick(col);
}
function sortRows(sortClicked)
{
var col = this.sortBy;
if (!col) return;
var byColumn = col.index;
var nowDesc = (col.attributes["sort"] || "desc") == "desc";
if (sortClicked) (this.$(thead [sort]) || col).attributes["sort"] = undefined; // drop any other sort order.
var getValue = function(x) {
var value = x.attributes["value"];
if (value == undefined) return x.text.toLowerCase();
return value.toInteger();
}
var sort = function(r1, r2, asc) {
if (r1[1].text == "..") {
return -1;
}
if (r2[1].text == "..") {
return 1;
}
if (!asc)
return getValue(r1[byColumn]) < getValue(r2[byColumn]) ? -1 : 1;
else
return getValue(r1[byColumn]) > getValue(r2[byColumn]) ? -1 : 1;
}
if (nowDesc)
{
if (sortClicked) col.attributes["sort"] = "asc";
this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? true : false));
} else {
if (sortClicked) col.attributes["sort"] = "desc";
this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? false : true));
}
}
function attached()
{
assert this.tag == "table" : "wrong element type for grid, table expected";
this.body = this.$(:root>tbody);
assert this.body : "Grid require <tbody> element";
}
function onMouse(evt)
{
if ((evt.type != Event.MOUSE_DOWN) && (evt.type != Event.MOUSE_DCLICK))
return false;
if (!evt.mainButton)
return false;
// auxiliary function, returns row this target element belongs to
function targetRow(target) { return target.$p(tbody>tr); }
// auxiliary function, returns row this target element belongs to
function targetHeaderCell(target) { return target.$p(thead>tr>th); }
if (var row = targetRow(evt.target)) // click on the row
this.setCurrentRow(row, #by_mouse, evt.type == Event.MOUSE_DCLICK);
else if (var headerCell = targetHeaderCell(evt.target))
{
this.setCurrentColumn(headerCell); // click on the header cell
if (evt.type != Event.MOUSE_DCLICK && headerCell.$is(.sortable)) {
this.sortBy = headerCell;
this.sortRows(true);
}
}
//return true; // as it is always ours then stop event bubbling
}
function onFocus(evt)
{
return (evt.type == Event.GOT_FOCUS || evt.type == Event.LOST_FOCUS);
}
function onKey(evt)
{
if (evt.type != Event.KEY_DOWN)
return false;
switch(evt.keyCode)
{
case Event.VK_DOWN:
{
var crow = this.getCurrentRow();
var idx = crow? crow.index + 1 : 0;
if (idx < this.body.length) this.setCurrentRow(this.body[idx],#by_key);
}
return true;
case Event.VK_UP:
{
var crow = this.getCurrentRow();
var idx = crow? crow.index - 1 : this.length - 1;
if (idx >= 0) this.setCurrentRow(this.body[idx],#by_key);
}
return true;
case Event.VK_PRIOR:
{
var y = this.body.scroll(#top) - this.body.scroll(#height);
var r;
for(var i = this.body.length - 1; i >= 0; --i)
{
var pr = r; r = this.body[i];
if (r.box(#top, #inner, #content) < y)
{
// this row is further than scroll pos - height of scroll area
this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible
return true;
}
}
this.setCurrentRow(r,#by_key); // just in case
}
return true;
case Event.VK_NEXT:
{
var y = this.body.scroll(#top) + 2 * this.body.scroll(#height);
var lastScrollable = this.body.length - 1;
var r;
for(var i = 0; i <= lastScrollable; ++i)
{
var pr = r; r = this.body[i];
if (r.box(#bottom, #inner, #content) > y)
{
// this row is further than scroll pos - height of scroll area
this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible
return true;
}
}
this.setCurrentRow(r,#by_key); // just in case
}
return true;
case Event.VK_HOME:
{
if (this.body.length)
this.setCurrentRow(this.body.first,#by_key);
}
return true;
case Event.VK_END:
{
if (this.body.length)
this.setCurrentRow(this.body.last,#by_key);
}
return true;
}
var char = handler.get_char(keymap[evt.keyCode] || "", evt.keyCode);
if (char) {
var crow = this.getCurrentRow();
var idx = crow? crow.index + 1 : 0;
while (idx < this.body.length) {
var el = this.body[idx];
var text = el[1].text;
if (text && text[0].toLowerCase() == char) {
this.setCurrentRow(el, #by_key);
return true;
}
idx += 1;
}
}
if (evt.keyCode == Event.VK_ENTER ||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
this.onRowDoubleClick(this.getCurrentRow());
}
return false;
}
}

67
src/ui/header.css Normal file
View File

@@ -0,0 +1,67 @@
header #screens {
background: white;
border: #A9A9A9 1px solid;
height: 22px;
border-radius: 4px;
flow: horizontal;
border-spacing: 0.5em;
padding-right: 1em;
position: relative;
}
header #screen {
text-align: center;
margin: 3px 0;
width: 18px;
height: 14px;
border: color(border) solid 1px;
font-size: 11px;
color: color(light-text);
}
header #secure {
position: absolute;
left: -10px;
top: -2px;
}
header #secure svg {
size: 18px;
}
header .remote-id {
width: *;
padding-left: 30px;
padding-right: 4em;
margin: * 0;
}
header span:active, header #screen:active {
color: black;
background: color(gray-bg);
}
div#global-screens {
position: relative;
margin: 2px 0;
}
div#global-screens > div {
position: absolute;
border: color(border) solid 1px;
text-align: center;
color: color(light-text);
}
header #screen.current, div#global-screens > div.current {
background: #666;
color: white;
}
span#fullscreen.active {
border: color(border) solid 1px;
}
button:disabled {
opacity: 0.3;
}

377
src/ui/header.tis Normal file
View File

@@ -0,0 +1,377 @@
var pi = handler.get_default_pi(); // peer information
var chat_msgs = [];
var svg_fullscreen = <svg viewBox="0 0 357 357">
<path d="M51,229.5H0V357h127.5v-51H51V229.5z M0,127.5h51V51h76.5V0H0V127.5z M306,306h-76.5v51H357V229.5h-51V306z M229.5,0v51 H306v76.5h51V0H229.5z"/>
</svg>;
var svg_action = <svg viewBox="-91 0 512 512"><path d="M315 211H191L298 22a15 15 0 00-13-22H105c-6 0-12 4-14 10L1 281a15 15 0 0014 20h127L61 491a15 15 0 0025 16l240-271a15 15 0 00-11-25z"/></svg>;
var svg_display = <svg viewBox="0 0 640 512">
<path d="M592 0H48A48 48 0 0 0 0 48v320a48 48 0 0 0 48 48h240v32H112a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16H352v-32h240a48 48 0 0 0 48-48V48a48 48 0 0 0-48-48zm-16 352H64V64h512z"/>
</svg>;
var svg_secure = <svg viewBox="0 0 347.97 347.97">
<path fill="#3F7D46" d="m317.31 54.367c-59.376 0-104.86-16.964-143.33-54.367-38.461 37.403-83.947 54.367-143.32 54.367 0 97.405-20.155 236.94 143.32 293.6 163.48-56.666 143.33-196.2 143.33-293.6zm-155.2 171.41-47.749-47.756 21.379-21.378 26.37 26.376 50.121-50.122 21.378 21.378-71.499 71.502z"/>
</svg>;
var svg_insecure = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="none" stroke="red" stroke-width="14.827"/><g fill="red"><path d="M238.802 115.023l-111.573 114.68-8.6-8.367L230.2 106.656z"/><path d="M125.559 108.093l114.68 111.572-8.368 8.601-114.68-111.572z"/></g></svg>;
var svg_insecure_relay = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="none" stroke="red" stroke-width="14.827"/><g fill="red"><path d="M231.442 247.498l-7.754-10.205c-17.268 12.441-38.391 17.705-59.478 14.822-21.087-2.883-39.613-13.569-52.166-30.088-25.916-34.101-17.997-82.738 17.65-108.42 32.871-23.685 78.02-19.704 105.172 7.802l-32.052 7.987 3.082 12.369 48.722-12.142-11.712-46.998-12.822 3.196 4.496 18.039c-31.933-24.008-78.103-25.342-112.642-.458-31.361 22.596-44.3 60.436-35.754 94.723 2.77 11.115 7.801 21.862 15.192 31.588 30.19 39.727 88.538 47.705 130.066 17.785z"/></g></svg>;
var svg_secure_relay = <svg viewBox="0 0 347.97 347.97"><path d="M317.469 61.615c-59.442 0-104.976-16.082-143.489-51.539-38.504 35.457-84.04 51.539-143.479 51.539 0 92.337-20.177 224.612 143.479 278.324 163.661-53.717 143.489-185.992 143.489-278.324z" fill="#3f7d46" stroke="#3f7d46" stroke-width="14.827"/><g fill="red"><path d="M231.442 247.498l-7.754-10.205c-17.268 12.441-38.391 17.705-59.478 14.822-21.087-2.883-39.613-13.569-52.166-30.088-25.916-34.101-17.997-82.738 17.65-108.42 32.871-23.685 78.02-19.704 105.172 7.802l-32.052 7.987 3.082 12.369 48.722-12.142-11.712-46.998-12.822 3.196 4.496 18.039c-31.933-24.008-78.103-25.342-112.642-.458-31.361 22.596-44.3 60.436-35.754 94.723 2.77 11.115 7.801 21.862 15.192 31.588 30.19 39.727 88.538 47.705 130.066 17.785z" fill="#fff"/></g></svg>;
view << event statechange {
adjustBorder();
adaptDisplay();
view.focus = handler;
var fs = view.windowState == View.WINDOW_FULL_SCREEN;
var el = $(#fullscreen);
if (el) el.attributes.toggleClass("active", fs);
el = $(#maximize);
if (el) {
el.state.disabled = fs;
}
}
var header;
var old_window_state = View.WINDOW_SHOWN;
var input_blocked;
class Header: Reactor.Component {
function this(params) {
header = this;
}
function render() {
var icon_conn;
var title_conn;
if (this.secure_connection && this.direct_connection) {
icon_conn = svg_secure;
title_conn = "Direct and secure connection";
} else if (this.secure_connection && !this.direct_connection) {
icon_conn = svg_secure_relay;
title_conn = "Relayed and secure connection";
} else if (!this.secure_connection && this.direct_connection) {
icon_conn = svg_insecure;
title_conn = "Direct and insecure connection";
} else {
icon_conn = svg_insecure_relay;
title_conn = "Relayed and insecure connection";
}
var title = handler.get_id();
if (pi.hostname) title += "(" + pi.username + "@" + pi.hostname + ")";
if ((pi.displays || []).length == 0) {
return <div .ellipsis style={is_osx || is_win ? "size:*;text-align:center;margin:*;" : ""}>{title}</div>;
}
var screens = pi.displays.map(function(d, i) {
return <div #screen class={pi.current_display == i ? "current" : ""}>
{i+1}
</div>;
});
updateWindowToolbarPosition();
var style = "flow: horizontal;";
if (is_osx) style += "margin: *";
self.timer(1ms, toggleMenuState);
return <div style={style}>
{is_osx ? "" : <span #fullscreen>{svg_fullscreen}</span>}
<div #screens>
<span #secure title={title_conn}>{icon_conn}</span>
<div .remote-id>{handler.get_id()}</div>
<div style="flow:horizontal;border-spacing: 0.5em;">{screens}</div>
{this.renderGlobalScreens()}
</div>
<span #chat>{svg_chat}</span>
<span #action>{svg_action}</span>
<span #display>{svg_display}</span>
{this.renderDisplayPop()}
{this.renderActionPop()}
</div>;
}
function renderDisplayPop() {
return <popup>
<menu.context #display-options>
<li #adjust-window style="display:none">Adjust Window</li>
<div #adjust-window .separator style="display:none"/>
<li #original type="view-style"><span>{svg_checkmark}</span>Original</li>
<li #shrink type="view-style"><span>{svg_checkmark}</span>Shrink</li>
<li #stretch type="view-style"><span>{svg_checkmark}</span>Stretch</li>
<div .separator />
<li #best type="image-quality"><span>{svg_checkmark}</span>Good image quality</li>
<li #balanced type="image-quality"><span>{svg_checkmark}</span>Balanced</li>
<li #low type="image-quality"><span>{svg_checkmark}</span>Optimize reaction time</li>
<li #custom type="image-quality"><span>{svg_checkmark}</span>Custom</li>
<div .separator />
<li #show-remote-cursor .toggle-option><span>{svg_checkmark}</span>Show remote cursor</li>
{audio_enabled ? <li #disable-audio .toggle-option><span>{svg_checkmark}</span>Mute</li> : ""}
{keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>Disable clipboard</li> : ""}
{keyboard_enabled ? <li #lock-after-session-end .toggle-option><span>{svg_checkmark}</span>Lock after session end</li> : ""}
{false && pi.platform == "Windows" ? <li #privacy-mode .toggle-option><span>{svg_checkmark}</span>Privacy mode</li> : ""}
</menu>
</popup>;
}
function renderActionPop() {
return <popup>
<menu.context #action-options>
<li #transfer-file>Transfer File</li>
<li #tunnel>IP Tunneling</li>
{keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ? <li #ctrl-alt-del>Insert Ctrl + Alt + Del</li> : ""}
{keyboard_enabled ? <li #lock-screen>Insert Lock</li> : ""}
{false && pi.platform == "Windows" ? <li #block-input>Block user input </li> : ""}
{handler.support_refresh() ? <li #refresh>Refresh</li> : ""}
</menu>
</popup>;
}
function renderGlobalScreens() {
if (pi.displays.length < 2) return "";
var x0 = 9999999;
var y0 = 9999999;
var x = -9999999;
var y = -9999999;
pi.displays.map(function(d, i) {
if (d.x < x0) x0 = d.x;
if (d.y < y0) y0 = d.y;
var dx = d.x + d.width;
if (dx > x) x = dx;
var dy = d.y + d.height;
if (dy > y) y = dy;
});
var w = x - x0;
var h = y - y0;
var scale = 16. / h;
var screens = pi.displays.map(function(d, i) {
var min_wh = d.width > d.height ? d.height : d.width;
var style = "width:" + (d.width * scale) + "px;" +
"height:" + (d.height * scale) + "px;" +
"left:" + ((d.x - x0) * scale) + "px;" +
"top:" + ((d.y - y0) * scale) + "px;" +
"font-size:" + (min_wh * 0.9 * scale) + "px;";
return <div style={style} class={pi.current_display == i ? "current" : ""}>{i+1}</div>;
});
var style = "width:" + (w * scale) + "px; height:" + (h * scale) + "px;";
return <div #global-screens style={style}>
{screens}
</div>;
}
event click $(#fullscreen) (_, el) {
if (view.windowState == View.WINDOW_FULL_SCREEN) {
if (old_window_state == View.WINDOW_MAXIMIZED) {
view.windowState = View.WINDOW_SHOWN;
}
view.windowState = old_window_state;
} else {
old_window_state = view.windowState;
if (view.windowState == View.WINDOW_MAXIMIZED) {
view.windowState = View.WINDOW_SHOWN;
}
view.windowState = View.WINDOW_FULL_SCREEN;
}
}
event click $(#chat) {
startChat();
}
event click $(#action) (_, me) {
var menu = $(menu#action-options);
me.popup(menu);
}
event click $(#display) (_, me) {
var menu = $(menu#display-options);
me.popup(menu);
}
event click $(#screen) (_, me) {
if (pi.current_display == me.index) return;
handler.switch_display(me.index);
}
event click $(#transfer-file) {
handler.transfer_file();
}
event click $(#tunnel) {
handler.tunnel();
}
event click $(#ctrl-alt-del) {
handler.ctrl_alt_del();
}
event click $(#lock-screen) {
handler.lock_screen();
}
event click $(#refresh) {
handler.refresh_video();
}
event click $(#block-input) {
if (!input_blocked) {
handler.toggle_option("block-input");
input_blocked = true;
$(#block-input).text = "Unblock user input";
} else {
handler.toggle_option("unblock-input");
input_blocked = false;
$(#block-input).text = "Block user input";
}
}
event click $(menu#display-options>li) (_, me) {
if (me.id == "custom") {
handle_custom_image_quality();
} else if (me.attributes.hasClass("toggle-option")) {
handler.toggle_option(me.id);
toggleMenuState();
} else if (!me.attributes.hasClass("selected")) {
var type = me.attributes["type"];
if (type == "image-quality") {
handler.save_image_quality(me.id);
} else if (type == "view-style") {
handler.save_view_style(me.id);
adaptDisplay();
}
toggleMenuState();
}
}
}
function handle_custom_image_quality() {
var tmp = handler.get_custom_image_quality();
var bitrate0 = tmp[0] || 50;
var quantizer0 = tmp.length > 1 ? tmp[1] : 100;
handler.msgbox("custom", "Custom Image Quality", "<div .form> \
<div><input type=\"hslider\" style=\"width: 66%\" name=\"bitrate\" max=\"100\" min=\"10\" value=\"" + bitrate0 + "\"/ buddy=\"bitrate-buddy\"><b #bitrate-buddy>x</b>% bitrate</div> \
<div><input type=\"hslider\" style=\"width: 66%\" name=\"quantizer\" max=\"100\" min=\"0\" value=\"" + quantizer0 + "\"/ buddy=\"quantizer-buddy\"><b #quantizer-buddy>x</b>% quantizer</div> \
</div>", function(res=null) {
if (!res) return;
if (!res.bitrate) return;
handler.save_custom_image_quality(res.bitrate, res.quantizer);
toggleMenuState();
});
}
function toggleMenuState() {
var values = [];
var q = handler.get_image_quality();
if (!q) q = "balanced";
values.push(q);
var s = handler.get_view_style();
if (!s) s = "original";
values.push(s);
for (var el in $$(menu#display-options>li)) {
el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0);
}
for (var id in ["show-remote-cursor", "disable-audio", "disable-clipboard", "lock-after-session-end", "privacy-mode"]) {
var el = self.select('#' + id);
if (el) {
el.attributes.toggleClass("selected", handler.get_toggle_option(id));
}
}
}
if (is_osx) {
$(header).content(<Header />);
$(header).attributes["role"] = "window-caption";
} else {
if (is_file_transfer || is_port_forward) {
$(caption).content(<Header />);
} else {
$(div.window-toolbar).content(<Header />);
}
setWindowButontsAndIcon();
}
if (!(is_file_transfer || is_port_forward)) {
$(header).style.set {
height: "32px",
};
if (!is_osx) {
$(div.window-icon).style.set {
size: "32px",
};
}
}
handler.updatePi = function(v) {
pi = v;
header.update();
if (is_port_forward) {
view.windowState = View.WINDOW_MINIMIZED;
}
}
handler.switchDisplay = function(i) {
pi.current_display = i;
header.update();
}
function updateWindowToolbarPosition() {
if (is_osx) return;
self.timer(1ms, function() {
var el = $(div.window-toolbar);
var w1 = el.box(#width, #border);
var w2 = $(header).box(#width, #border);
var x = (w2 - w1) / 2;
el.style.set {
left: x + "px",
display: "block",
};
});
}
view.on("size", function() {
// ensure size is done, so add timer
self.timer(1ms, function() {
updateWindowToolbarPosition();
adaptDisplay();
});
});
handler.newMessage = function(text) {
chat_msgs.push({text: text, name: pi.username || "", time: getNowStr()});
startChat();
}
function sendMsg(text) {
chat_msgs.push({text: text, name: "me", time: getNowStr()});
handler.send_chat(text);
if (chatbox) chatbox.refresh();
}
var chatbox;
function startChat() {
if (chatbox) {
chatbox.windowState = View.WINDOW_SHOWN;
chatbox.refresh();
return;
}
var icon = handler.get_icon();
var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw);
var w = 300;
var h = 400;
var x = (sx + sw - w) / 2;
var y = sy + 80;
var params = {
type: View.FRAME_WINDOW,
x: x,
y: y,
width: w,
height: h,
client: true,
parameters: { msgs: chat_msgs, callback: sendMsg, icon: icon },
caption: handler.get_id(),
};
var html = handler.get_chatbox();
if (html) params.html = html;
else params.url = self.url("chatbox.html");
chatbox = view.window(params);
}
handler.setConnectionType = function(secured, direct) {
header.update({
secure_connection: secured,
direct_connection: direct,
});
}

261
src/ui/index.css Normal file
View File

@@ -0,0 +1,261 @@
html {
background-color: transparent;
var(gray-bg-osx): rgba(238, 238, 238, 0.75);
}
body {
overflow: hidden;
}
@media platform != "OSX" {
body {
border-top: color(border) solid 1px;
}
}
.title {
font-size: 1.4em;
}
.app {
flow: horizontal;
size: *;
}
.lighter-text {
color: color(lighter-text);
font-size: 0.9em;
}
.left-pane {
width: 200px;
height: *;
background: color(bg);
border-right: color(border) 1px solid;
}
.left-pane > div:nth-child(1) {
border-spacing: 1em;
padding: 20px;
}
.your-desktop {
border-spacing: 0.5em;
border-left: color(accent) solid 2px;
padding-left: 0.5em;
}
.your-desktop input[type=text] {
padding: 0;
border: none;
height: 1.5em;
}
.your-desktop > div {
color: color(light-text);
}
.right-pane {
size: *;
background: color(gray-bg);
}
.right-content {
overflow: scroll-indicator;
padding: 1.6em;
border-spacing: 1.6em;
size: *;
flow: vertical;
}
@media platform == "OSX" {
.right-pane {
background: color(gray-bg-osx);
}
}
@mixin CARD {
padding: 1.6em;
border-spacing: 1em;
background: color(bg);
border-radius: 1em;
}
.card-connect {
@CARD;
width: 320px;
}
.right-buttons {
text-align: right;
}
.right-buttons>button {
margin-left: 1.6em;
}
div.connect-status {
left: 240px;
border-top: color(border) solid 1px;
width: 100%;
background: color(gray-bg);
padding: 1em;
}
div.connect-status > span.connect-status-icon {
border-radius: 4px;
width: 8px;
height: 8px;
display: inline-block;
margin-right: 1em;
}
div.connect-status > span.link {
margin-left: 1em;
display: inline-block;
}
span.connect-status-1 {
background: #e04f5f;
}
span.connect-status1 {
background: #32bea6;
}
span.connect-status0 {
background: #F5853B;
}
div.recent-sessions-content {
border-spacing: 1em;
flow: horizontal-flow;
}
div.recent-sessions-title {
color: color(light-text);
padding-top: 0.5em;
border-top: color(border) solid 1px;
margin-bottom: 1em;
}
div.remote-session {
border-radius: 1em;
height: 140px;
width: 220px;
padding: 0;
position: relative;
border: none;
}
div.remote-session:hover {
outline: color(button) solid 2px -2px;
}
div.remote-session .platform {
width: *;
height: 120px;
padding: *;
position: relative;
}
div.remote-session .platform .username{
left: 0;
color: #eee;
position: absolute;
bottom: 38px;
font-size: 0.8em;
width: 220px;
overflow: hidden;
text-align: center;
}
div.remote-session .platform svg {
width: 60px;
height: 60px;
background: none;
}
div.remote-session .text {
background: white;
position: absolute;
height: 3em;
width: 100%;
border-radius: 0 0 1em 1em;
bottom: 0;
flow: horizontal;
}
div.remote-session .text > div {
padding-top: 1em;
padding-left: 1em;
width: *;
}
svg#menu {
size: 1em;
background: none;
padding: 0.5em;
margin: 0.5em;
color: color(light-text);
}
svg#menu:active {
color: black;
border-radius: 1em;
background: color(gray-bg);
}
svg#edit:active {
opacity: 0.5;
}
svg#edit {
display: inline-block;
margin-top: 0.25em;
margin-bottom: 0;
}
div.install-me, div.trust-me {
margin-top: 0.5em;
padding: 20px;
color: white;
background: linear-gradient(left,#e242bc,#f4727c);
}
div.install-me > div:nth-child(1) {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 0.5em;
}
div.install-me > div:nth-child(2) {
line-height: 1.4em;
}
div.trust-me > div:nth-child(1) {
font-size: 1.2em;
text-align: center;
font-weight: bold;
margin-bottom: 0.5em;
}
div.trust-me > div:nth-child(2) {
font-size: 0.9em;
margin-bottom: 1em;
}
div.trust-me > div:nth-child(3) {
text-align: center;
font-size: 1.5em;
font-weight: bold;
}
div#myid {
position: relative;
}
div#myid svg#menu {
position: absolute;
right: -1em;
}

30
src/ui/index.html Normal file
View File

@@ -0,0 +1,30 @@
<html>
<head>
<style>
@import url(common.css);
@import url(index.css);
</style>
<script type="text/tiscript">
include "common.tis";
include "index.tis";
</script>
<popup>
<menu.context #remote-context>
<li #connect>Connect</li>
<li #transfer>Transfer File</li>
<li #tunnel>TCP Tunneling</li>
<li #rdp>RDP</li>
<li #remove>Remove</li>
</menu>
</popup>
<popup><menu.context #edit-password-context>
<li #refresh-password>Refresh random password</li>
<li #set-password>Set your own password</li>
</menu></popup>
</head>
<body>
</body>
</html>

713
src/ui/index.tis Normal file
View File

@@ -0,0 +1,713 @@
if (is_osx) view.windowBlurbehind = #light;
stdout.println("current platform:", OS);
// html min-width, min-height not working on mac, below works for all
view.windowMinSize = (500, 300);
var app;
var tmp = handler.get_connect_status();
var connect_status = tmp[0];
var service_stopped = false;
var software_update_url = "";
var key_confirmed = tmp[1];
var system_error = "";
var svg_menu = <svg #menu viewBox="0 0 512 512">
<circle cx="256" cy="256" r="64"/>
<circle cx="256" cy="448" r="64"/>
<circle cx="256" cy="64" r="64"/>
</svg>;
class ConnectStatus: Reactor.Component {
function render() {
return
<div .connect-status>
<span class={"connect-status-icon connect-status" + (service_stopped ? 0 : connect_status)} />
{this.getConnectStatusStr()}
{service_stopped ? <span class="link">Start Service</span> : ""}
</div>;
}
function getConnectStatusStr() {
if (service_stopped) {
return "Service is not running";
} else if (connect_status == -1) {
return "Not ready. Please check your connection";
} else if (connect_status == 0) {
return "Connecting to the RustDesk network...";
}
return "Ready";
}
event click $(.connect-status .link) () {
var options = handler.get_options();
options["stop-service"] = "";
handler.set_options(options);
}
}
class RecentSessions: Reactor.Component {
function render() {
var sessions = handler.get_recent_sessions();
if (sessions.length == 0) return <span />;
sessions = sessions.map(this.getSession);
return <div style="width: *">
<div .recent-sessions-title>RECENT SESSIONS</div>
<div .recent-sessions-content key={sessions.length}>
{sessions}
</div>
</div>;
}
function getSession(s) {
var id = s[0];
var username = s[1];
var hostname = s[2];
var platform = s[3];
return <div .remote-session id={id} platform={platform} style={"background:"+string2RGB(id+platform, 0.5)}>
<div .platform>
{platformSvg(platform, "white")}
<div .username>{username}@{hostname}</div>
</div>
<div .text>
<div>{formatId(id)}</div>
{svg_menu}
</div>
</div>;
}
event dblclick $(div.remote-session) (evt, me) {
createNewConnect(me.id, "connect");
}
event click $(#menu) (_, me) {
var id = me.parent.parent.id;
var platform = me.parent.parent.attributes["platform"];
$(#rdp).style.set{
display: (platform == "Windows" && is_win) ? "block" : "none",
};
// https://sciter.com/forums/topic/replacecustomize-context-menu/
var menu = $(menu#remote-context);
menu.attributes["remote-id"] = id;
me.popup(menu);
}
}
event click $(menu#remote-context li) (evt, me) {
var action = me.id;
var id = me.parent.attributes["remote-id"];
if (action == "connect") {
createNewConnect(id, "connect");
} else if (action == "transfer") {
createNewConnect(id, "file-transfer");
} else if (action == "remove") {
handler.remove_peer(id);
app.recent_sessions.update();
} else if (action == "rdp") {
createNewConnect(id, "rdp");
} else if (action == "tunnel") {
createNewConnect(id, "port-forward");
}
}
function createNewConnect(id, type) {
id = id.replace(/\s/g, "");
app.remote_id.value = formatId(id);
if (!id) return;
if (id == handler.get_id()) {
handler.msgbox("custom-error", "Error", "Sorry, it is yourself");
return;
}
handler.set_remote_id(id);
handler.new_remote(id, type);
}
var myIdMenu;
var audioInputMenu;
var configOptions = {};
class AudioInputs: Reactor.Component {
function this() {
audioInputMenu = this;
}
function render() {
if (!this.show) return <li />;
var inputs = handler.get_sound_inputs();
if (is_win) inputs = ["System Sound"].concat(inputs);
if (!inputs.length) return <li style="display:hidden" />;
inputs = ["Mute"].concat(inputs);
var me = this;
self.timer(1ms, function() { me.toggleMenuState() });
return <li>Audio Input
<menu #audio-input key={inputs.length}>
{inputs.map(function(name) {
return <li id={name}><span>{svg_checkmark}</span>{name}</li>;
})}
</menu>
</li>;
}
function get_default() {
if (is_win) return "System Sound";
return "";
}
function get_value() {
return configOptions["audio-input"] || this.get_default();
}
function toggleMenuState() {
var v = this.get_value();
for (var el in $$(menu#audio-input>li)) {
var selected = el.id == v;
el.attributes.toggleClass("selected", selected);
}
}
event click $(menu#audio-input>li) (_, me) {
var v = me.id;
if (v == this.get_value()) return;
if (v == this.get_default()) v = "";
configOptions["audio-input"] = v;
handler.set_options(configOptions);
this.toggleMenuState();
}
}
class MyIdMenu: Reactor.Component {
function this() {
myIdMenu = this;
}
function render() {
var me = this;
return <div #myid>
{this.renderPop()}
ID{svg_menu}
</div>;
}
function renderPop() {
return <popup>
<menu.context #config-options>
<li #enable-keyboard><span>{svg_checkmark}</span>Enable Keyboard/Mouse</li>
<li #enable-clipboard><span>{svg_checkmark}</span>Enable Clipboard</li>
<li #enable-file-transfer><span>{svg_checkmark}</span>Enable File Transfer</li>
<li #enable-tunnel><span>{svg_checkmark}</span>Enable TCP Tunneling</li>
<AudioInputs />
<div .separator />
<li #whitelist title="Only whitelisted IP can access me">IP Whitelisting</li>
<li #custom-server>ID/Relay Server</li>
<div .separator />
<li #stop-service>{service_stopped ? "Start service" : "Stop service"}</li>
<div .separator />
<li #forum>Forum</li>
<li #about>About {handler.get_app_name()}</li>
</menu>
</popup>;
}
event click $(svg#menu) (_, me) {
audioInputMenu.update({ show: true });
configOptions = handler.get_options();
this.toggleMenuState();
var menu = $(menu#config-options);
me.popup(menu);
}
function toggleMenuState() {
for (var el in $$(menu#config-options>li)) {
if (el.id && el.id.indexOf("enable-") == 0) {
var enabled = configOptions[el.id] != "N";
el.attributes.toggleClass("selected", enabled);
}
}
}
event click $(menu#config-options>li) (_, me) {
if (me.id && me.id.indexOf("enable-") == 0) {
configOptions[me.id] = configOptions[me.id] == "N" ? "" : "N";
handler.set_options(configOptions);
this.toggleMenuState();
}
if (me.id == "whitelist") {
var old_value = (configOptions["whitelist"] || "").split(",").join("\n");
handler.msgbox("custom-whitelist", "IP Whitelisting", "<div .form> \
<textarea spellcheck=\"false\" name=\"text\" novalue=\"0.0.0.0\" style=\"overflow: scroll-indicator; height: 160px; font-size: 1.2em; padding: 0.5em;\">" + old_value + "</textarea>\
</div> \
", function(res=null) {
if (!res) return;
var value = (res.text || "").trim();
if (value) {
var values = value.split(/[\s,;]+/g);
for (var ip in values) {
if (!ip.match(/^\d+\.\d+\.\d+\.\d+$/)) {
return "Invalid ip: " + ip;
}
}
value = values.join("\n");
}
if (value == old_value) return;
configOptions["whitelist"] = value.replace("\n", ",");
stdin.println("whitelist updated");
handler.set_options(configOptions);
}, 300);
} else if (me.id == "custom-server") {
var old_relay = configOptions["relay-server"] || "";
var old_id = configOptions["custom-rendezvous-server"] || "";
handler.msgbox("custom-server", "ID/Relay Server", "<div .form> \
<div><span style='width: 100px; display:inline-block'>ID Server: </span><input style='width: 250px' name='id' value='" + old_id + "' /></div> \
<div><span style='width: 100px; display:inline-block'>Relay Server: </span><input style='width: 250px' name='relay' value='" + old_relay + "' /></div> \
</div> \
", function(res=null) {
if (!res) return;
var id = (res.id || "").trim();
var relay = (res.relay || "").trim();
if (id == old_id && relay == old_relay) return;
if (id) {
var err = handler.test_if_valid_server(id);
if (err) return "ID Server: " + err;
}
if (relay) {
var err = handler.test_if_valid_server(relay);
if (err) return "Relay Server: " + err;
}
configOptions["custom-rendezvous-server"] = id;
configOptions["relay-server"] = relay;
handler.set_options(configOptions);
});
} else if (me.id == "forum") {
handler.open_url("https:://forum.rustdesk.com");
} else if (me.id == "stop-service") {
configOptions["stop-service"] = service_stopped ? "" : "Y";
handler.set_options(configOptions);
} else if (me.id == "about") {
var name = handler.get_app_name();
handler.msgbox("custom-nocancel-nook-hasclose", "About " + name, "<div style='line-height: 2em'> \
<div>Version: " + handler.get_version() + " \
<div .link .custom-event url='http://rustdesk.com/privacy'>Privacy Statement</div> \
<div .link .custom-event url='http://forum.rustdesk.com'>Forum</div> \
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright &copy; 2020 CarrieZ Studio \
<br /> Author: Carrie \
<p style='font-weight: bold'>Made with heart in this chaotic world!</p>\
</div>\
</div>", function(el) {
if (el && el.attributes) {
handler.open_url(el.attributes['url']);
};
}, 400);
}
}
}
class App: Reactor.Component
{
function this() {
app = this;
}
function render() {
var is_can_screen_recording = handler.is_can_screen_recording(false);
return
<div .app>
<div .left-pane>
<div>
<div .title>Your Desktop</div>
<div .lighter-text>Your desktop can be accessed with this ID and password.</div>
<div .your-desktop>
<MyIdMenu />
{key_confirmed ? <input type="text" readonly value={formatId(handler.get_id())}/> : "Generating ..."}
</div>
<div .your-desktop>
<div>Password</div>
<Password />
</div>
</div>
{handler.is_installed() ? "": <InstalllMe />}
{handler.is_installed() && software_update_url ? <UpdateMe /> : ""}
{handler.is_installed() && !software_update_url && handler.is_installed_lower_version() ? <UpgradeMe /> : ""}
{is_can_screen_recording ? "": <CanScreenRecording />}
{is_can_screen_recording && !handler.is_process_trusted(false) ? <TrustMe /> : ""}
{system_error ? <SystemError /> : ""}
{!system_error && handler.is_login_wayland() ? <FixWayland /> : ""}
</div>
<div .right-pane>
<div .right-content>
<div .card-connect>
<div .title>Control Remote Desktop</div>
<ID @{this.remote_id} />
<div .right-buttons>
<button .button .outline #file-transfer>Transfer File</button>
<button .button #connect>Connect</button>
</div>
</div>
<RecentSessions @{this.recent_sessions} />
</div>
<ConnectStatus @{this.connect_status} />
</div>
</div>;
}
event click $(button#connect) {
this.newRemote("connect");
}
event click $(button#file-transfer) {
this.newRemote("file-transfer");
}
function newRemote(type) {
createNewConnect(this.remote_id.value, type);
}
}
class InstalllMe: Reactor.Component {
function render() {
return <div .install-me>
<div>Install RustDesk</div>
<div #install-me .link>Install RustDesk on this computer ...</div>
</div>;
}
event click $(#install-me) {
handler.goto_install();
}
}
const http = function() {
function makeRequest(httpverb) {
return function( params ) {
params.type = httpverb;
view.request(params);
};
}
function download(from, to, args..)
{
var rqp = { type:#get, url: from, toFile: to };
var fn = 0;
var on = 0;
for( var p in args )
if( p instanceof Function )
{
switch(++fn) {
case 1: rqp.success = p; break;
case 2: rqp.error = p; break;
case 3: rqp.progress = p; break;
}
} else if( p instanceof Object )
{
switch(++on) {
case 1: rqp.params = p; break;
case 2: rqp.headers = p; break;
}
}
view.request(rqp);
}
return {
get: makeRequest(#get),
post: makeRequest(#post),
put: makeRequest(#put),
del: makeRequest(#delete),
download: download
};
}();
class UpgradeMe: Reactor.Component {
function render() {
var update_or_download = is_osx ? "download" : "update";
return <div .install-me>
<div>{handler.get_app_name()} Status</div>
<div>Your installation is lower version.</div>
<div #install-me .link style="padding-top: 1em">Click to upgrade</div>
</div>;
}
event click $(#install-me) {
handler.update_me("");
}
}
class UpdateMe: Reactor.Component {
function render() {
var update_or_download = is_osx ? "download" : "update";
return <div .install-me>
<div>{handler.get_app_name()} Status</div>
<div>There is a newer version of {handler.get_app_name()} ({handler.get_new_version()}) available.</div>
<div #install-me .link style="padding-top: 1em">Click to {update_or_download}</div>
<div #download-percent style="display:hidden; padding-top: 1em;" />
</div>;
}
event click $(#install-me) {
if (is_osx) {
handler.open_url("http://rustdesk.com");
return;
}
var url = software_update_url + '.' + handler.get_software_ext();
var path = handler.get_software_store_path();
var onsuccess = function(md5) {
$(#download-percent).content("Installing ...");
handler.update_me(path);
};
var onerror = function(err) {
handler.msgbox("custom-error", "Download Error", "Failed to download");
};
var onprogress = function(loaded, total) {
if (!total) total = 5 * 1024 * 1024;
var el = $(#download-percent);
el.style.set{display: "block"};
el.content("Downloading %" + (loaded * 100 / total));
};
stdout.println("Downloading " + url + " to " + path);
http.download(
url,
self.url(path),
onsuccess, onerror, onprogress);
}
}
class SystemError: Reactor.Component {
function render() {
return <div .install-me>
<div>{system_error}</div>
</div>;
}
}
class TrustMe: Reactor.Component {
function render() {
return <div .trust-me>
<div>Configuration Permissions</div>
<div>In order to control your Desktop remotely, you need to grant RustDesk "Accessibility" permissions</div>
<div #trust-me .link>Configure</div>
</div>;
}
event click $(#trust-me) {
handler.is_process_trusted(true);
watch_trust();
}
}
class CanScreenRecording: Reactor.Component {
function render() {
return <div .trust-me>
<div>Configuration Permissions</div>
<div>In order to access your Desktop remotely, you need to grant RustDesk "Screen Recording" permissions</div>
<div #screen-recording .link>Configure</div>
</div>;
}
event click $(#screen-recording) {
handler.is_can_screen_recording(true);
watch_trust();
}
}
class FixWayland: Reactor.Component {
function render() {
return <div .trust-me>
<div>Warning</div>
<div>Login screen using Wayland is not supported</div>
<div #fix-wayland .link>Fix it</div>
</div>;
}
event click $(#fix-wayland) {
handler.fix_login_wayland();
app.update();
}
}
function watch_trust() {
// not use TrustMe::update, because it is buggy
var trusted = handler.is_process_trusted(false);
var el = $(div.trust-me);
if (el) {
el.style.set {
display: trusted ? "none" : "block",
};
}
// if (trusted) return;
self.timer(1s, watch_trust);
}
class PasswordEyeArea : Reactor.Component {
render() {
return
<div .eye-area style="width: *">
<input|text @{this.input} readonly value="******" />
{svg_eye}
</div>;
}
event mouseenter {
var me = this;
me.leaved = false;
me.timer(300ms, function() {
if (me.leaved) return;
me.input.value = handler.get_password();
});
}
event mouseleave {
this.leaved = true;
this.input.value = "******";
}
}
class Password: Reactor.Component {
function render() {
return <div .password style="flow:horizontal">
<PasswordEyeArea />
{svg_edit}
</div>;
}
event click $(svg#edit) (_, me) {
var menu = $(menu#edit-password-context);
me.popup(menu);
}
event click $(li#refresh-password) {
handler.update_password("");
this.update();
}
event click $(li#set-password) {
var me = this;
handler.msgbox("custom-password", "Set Password", "<div .form .set-password> \
<div><span>Password:</span><input|password(password) /></div> \
<div><span>Confirmation:</span><input|password(confirmation) /></div> \
</div> \
", function(res=null) {
if (!res) return;
var p0 = (res.password || "").trim();
var p1 = (res.confirmation || "").trim();
if (p0.length < 6) {
return "Too short, at least 6 characters.";
}
if (p0 != p1) {
return "The confirmation is not identical.";
}
handler.update_password(p0);
me.update();
});
}
}
class ID: Reactor.Component {
function render() {
return <input type="text" #remote_id .outline-focus novalue="Enter Remote ID" maxlength="13"
value={formatId(handler.get_remote_id())} />;
}
// https://github.com/c-smile/sciter-sdk/blob/master/doc/content/sciter/Event.htm
event change {
var fid = formatId(this.value);
var d = this.value.length - (this.old_value || "").length;
this.old_value = this.value;
var start = this.xcall(#selectionStart) || 0;
var end = this.xcall(#selectionEnd);
if (fid == this.value || d <= 0 || start != end) {
return;
}
// fix Caret position
this.value = fid;
var text_after_caret = this.old_value.substr(start);
var n = fid.length - formatId(text_after_caret).length;
this.xcall(#setSelection, n, n);
}
}
var reg = /^\d+$/;
function formatId(id) {
id = id.replace(/\s/g, "");
if (reg.test(id) && id.length > 3) {
var n = id.length;
var a = n % 3 || 3;
var new_id = id.substr(0, a);
for (var i = a; i < n; i += 3) {
new_id += " " + id.substr(i, 3);
}
return new_id;
}
return id;
}
event keydown (evt) {
if (!evt.shortcutKey) {
if (evt.keyCode == Event.VK_ENTER ||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
var el = $(button#connect);
view.focus = el;
el.sendEvent("click");
// simulate button click effect, windows does not have this issue
el.attributes.toggleClass("active", true);
self.timer(0.3s, function() {
el.attributes.toggleClass("active", false);
});
}
}
}
$(body).content(<App />);
function self.closing() {
// return false; // can prevent window close
var (x, y, w, h) = view.box(#rectw, #border, #screen);
handler.save_size(x, y, w, h);
}
function self.ready() {
var r = handler.get_size();
if (r[2] == 0) {
centerize(800, 600);
} else {
view.move(r[0], r[1], r[2], r[3]);
}
if (!handler.get_remote_id()) {
view.focus = $(#remote_id);
}
}
function checkConnectStatus() {
self.timer(1s, function() {
var tmp = !!handler.get_option("stop-service");
if (tmp != service_stopped) {
service_stopped = tmp;
app.connect_status.update();
myIdMenu.update();
}
tmp = handler.get_connect_status();
if (tmp[0] != connect_status) {
connect_status = tmp[0];
app.connect_status.update();
}
if (tmp[1] != key_confirmed) {
key_confirmed = tmp[1];
app.update();
}
tmp = handler.get_error();
if (system_error != tmp) {
system_error = tmp;
app.update();
}
tmp = handler.get_software_update_url();
if (tmp != software_update_url) {
software_update_url = tmp;
app.update();
}
if (handler.recent_sessions_updated()) {
stdout.println("recent sessions updated");
app.recent_sessions.update();
}
checkConnectStatus();
});
}
checkConnectStatus();

22
src/ui/install.html Normal file
View File

@@ -0,0 +1,22 @@
<html>
<head>
<style>
@import url(common.css);
div.content {
size: *;
background: white;
padding:2em 10em;
border-spacing: 1em;
}
input {
font-size: 1em;
}
</style>
<script type="text/tiscript">
include "common.tis";
include "install.tis";
</script>
</head>
<body>
</body>
</html>

45
src/ui/install.tis Normal file
View File

@@ -0,0 +1,45 @@
function self.ready() {
centerize(800, 600);
}
class Install: Reactor.Component {
function render() {
return <div .content>
<div style="font-size: 2em;">Installation</div>
<div style="margin: 2em 0;">Installation Path: <input|text disabled value={view.install_path()} /></div>
<div><button|checkbox #startmenu checked>Create start menu shortcuts</button></div>
<div><button|checkbox #desktopicon checked>Create desktop icon</button></div>
<div #aggrement .link style="margin-top: 2em;">End-user license agreement</div>
<div>By starting the installation, you accept the license agreement.</div>
<div style="height: 1px; background: gray; margin-top: 1em" />
<div style="text-align: right;">
<progress style={"color:" + color} style="display: none" />
<button .button id="cancel" .outline style="margin-right: 2em;">Cancel</button>
<button .button id="submit">Accept and Install</button>
</div>
</div>;
}
event click $(#cancel) {
view.close();
}
event click $(#aggrement) {
view.open_url("http://rustdesk.com/privacy");
}
event click $(#submit) {
for (var el in $$(button)) el.state.disabled = true;
$(progress).style.set{ display: "inline-block" };
var args = "";
if ($(#startmenu).value) {
args += "startmenu ";
}
if ($(#desktopicon).value) {
args += "desktopicon ";
}
view.install_me(args);
}
}
$(body).content(<Install />);

145
src/ui/macos.rs Normal file
View File

@@ -0,0 +1,145 @@
#[cfg(target_os = "macos")]
use cocoa::{
appkit::{NSApp, NSApplication, NSMenu, NSMenuItem},
base::{id, nil, YES},
foundation::{NSAutoreleasePool, NSString},
};
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Object, Sel, BOOL},
sel, sel_impl,
};
use std::{
ffi::c_void,
sync::{Arc, Mutex},
};
static APP_HANDLER_IVAR: &str = "GoDeskAppHandler";
lazy_static::lazy_static! {
pub static ref SHOULD_OPEN_UNTITLED_FILE_CALLBACK: Arc<Mutex<Option<Box<dyn Fn() + Send>>>> = Default::default();
}
trait AppHandler {
fn command(&mut self, cmd: u32);
}
struct DelegateState {
handler: Option<Box<dyn AppHandler>>,
}
impl DelegateState {
fn command(&mut self, command: u32) {
if command == 0 {
unsafe {
let () = msg_send!(NSApp(), terminate: nil);
}
} else if let Some(inner) = self.handler.as_mut() {
inner.command(command)
}
}
}
// https://github.com/xi-editor/druid/blob/master/druid-shell/src/platform/mac/application.rs
unsafe fn set_delegate(handler: Option<Box<dyn AppHandler>>) {
let mut decl =
ClassDecl::new("AppDelegate", class!(NSObject)).expect("App Delegate definition failed");
decl.add_ivar::<*mut c_void>(APP_HANDLER_IVAR);
decl.add_method(
sel!(applicationDidFinishLaunching:),
application_did_finish_launching as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(applicationShouldOpenUntitledFile:),
application_should_handle_open_untitled_file as extern "C" fn(&mut Object, Sel, id) -> BOOL,
);
decl.add_method(
sel!(handleMenuItem:),
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
);
let decl = decl.register();
let delegate: id = msg_send![decl, alloc];
let () = msg_send![delegate, init];
let state = DelegateState { handler };
let handler_ptr = Box::into_raw(Box::new(state));
(*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void);
let () = msg_send![NSApp(), setDelegate: delegate];
}
extern "C" fn application_did_finish_launching(_this: &mut Object, _: Sel, _notification: id) {
unsafe {
let () = msg_send![NSApp(), activateIgnoringOtherApps: YES];
}
}
extern "C" fn application_should_handle_open_untitled_file(
_this: &mut Object,
_: Sel,
_sender: id,
) -> BOOL {
if let Some(callback) = SHOULD_OPEN_UNTITLED_FILE_CALLBACK.lock().unwrap().as_ref() {
callback();
}
YES
}
/// This handles menu items in the case that all windows are closed.
extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
unsafe {
let tag: isize = msg_send![item, tag];
if tag == 0 {
let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR);
let inner = &mut *(inner as *mut DelegateState);
(*inner).command(tag as u32);
} else if tag == 1 {
crate::run_me(Vec::<String>::new()).ok();
}
}
}
pub fn make_menubar() {
unsafe {
let _pool = NSAutoreleasePool::new(nil);
set_delegate(None);
let menubar = NSMenu::new(nil).autorelease();
let app_menu_item = NSMenuItem::new(nil).autorelease();
menubar.addItem_(app_menu_item);
let app_menu = NSMenu::new(nil).autorelease();
let quit_title =
NSString::alloc(nil).init_str(&format!("Quit {}", hbb_common::config::APP_NAME));
let quit_action = sel!(handleMenuItem:);
let quit_key = NSString::alloc(nil).init_str("q");
let quit_item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(quit_title, quit_action, quit_key)
.autorelease();
let () = msg_send![quit_item, setTag: 0];
/*
if !enabled {
let () = msg_send![quit_item, setEnabled: NO];
}
if selected {
let () = msg_send![quit_item, setState: 1_isize];
}
let () = msg_send![item, setTag: id as isize];
*/
app_menu.addItem_(quit_item);
if std::env::args().len() > 1 {
let new_title = NSString::alloc(nil).init_str("New Window");
let new_action = sel!(handleMenuItem:);
let new_key = NSString::alloc(nil).init_str("n");
let new_item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(new_title, new_action, new_key)
.autorelease();
let () = msg_send![new_item, setTag: 1];
app_menu.addItem_(new_item);
}
app_menu_item.setSubmenu_(app_menu);
NSApp().setMainMenu_(menubar);
}
}

69
src/ui/msgbox.html Normal file
View File

@@ -0,0 +1,69 @@
<html window-frame="extended">
<head>
<style>
@import url(common.css);
html {
background-color: white;
}
body {
border: none;
color: black;
}
svg {
size: 80px;
background: white;
}
.form {
border-spacing: 0.5em;
}
caption {
@ELLIPSIS;
size: *;
text-align: center;
color: white;
padding-top: 0.33em;
font-weight: bold;
}
.form .text {
@ELLIPSIS;
}
button.button {
margin-left: 1.6em;
}
div.password {
position: relative;
}
div.password svg {
position: absolute;
right: 0.25em;
top: 0.25em;
padding: 0.5em;
color: color(text);
}
div.set-password > div {
flow: horizontal;
}
div.set-password > div > span {
width: 30%;
line-height: 2em;
}
div.set-password div.password {
width: *;
}
div.set-password input {
font-size: 1em;
}
#error {
color: red;
}
body div.ellipsis {
@ELLIPSIS;
}
</style>
<script type="text/tiscript">
include "common.tis";
include "msgbox.tis";
</script>
</head>
<body></body>
</html>

271
src/ui/msgbox.tis Normal file
View File

@@ -0,0 +1,271 @@
var type, title, text, getParams, remember, hasRetry, callback;
function updateParams(params) {
type = params.type;
title = params.title;
text = params.text;
getParams = params.getParams;
remember = params.remember;
callback = params.callback;
hasRetry = type == "error" &&
title == "Connection Error" &&
text.toLowerCase().indexOf("offline") < 0 &&
text.toLowerCase().indexOf("exist") < 0 &&
text.toLowerCase().indexOf("handshake") < 0 &&
text.toLowerCase().indexOf("failed") < 0 &&
text.toLowerCase().indexOf("resolve") < 0 &&
text.toLowerCase().indexOf("manually") < 0;
if (hasRetry) {
self.timer(1s, function() {
view.close({ reconnect: true });
});
}
}
var params = view.parameters;
updateParams(params);
var svg_eye_cross = <svg viewBox="0 -21 511.96 511">
<path d="m506.68 261.88c7.043-16.984 7.043-36.461 0-53.461-41.621-100.4-140.03-165.27-250.71-165.27-46.484 0-90.797 11.453-129.64 32.191l-68.605-68.609c-8.3438-8.3398-21.824-8.3398-30.168 0-8.3398 8.3398-8.3398 21.824 0 30.164l271.49 271.49 86.484 86.488 68.676 68.672c4.1797 4.1797 9.6406 6.2695 15.102 6.2695 5.4609 0 10.922-2.0898 15.082-6.25 8.3438-8.3398 8.3438-21.824 0-30.164l-62.145-62.145c36.633-27.883 66.094-65.109 84.438-109.38zm-293.91-100.1c12.648-7.5742 27.391-11.969 43.199-11.969 47.062 0 85.332 38.273 85.332 85.336 0 15.805-4.3945 30.547-11.969 43.199z"/>
<path d="m255.97 320.48c-47.062 0-85.336-38.273-85.336-85.332 0-3.0938 0.59766-6.0195 0.91797-9.0039l-106.15-106.16c-25.344 24.707-46.059 54.465-60.117 88.43-7.043 16.98-7.043 36.457 0 53.461 41.598 100.39 140.01 165.27 250.69 165.27 34.496 0 67.797-6.3164 98.559-18.027l-89.559-89.559c-2.9844 0.32031-5.9062 0.91797-9 0.91797z"/>
</svg>;
class Password: Reactor.Component {
this var visible = false;
function render() {
return <div .password>
<input name="password" type={this.visible ? "text" : "password"} .outline-focus />
{this.visible ? svg_eye_cross : svg_eye}
</div>;
}
event click $(svg) {
var el = this.$(input);
var value = el.value;
var start = el.xcall(#selectionStart) || 0;
var end = el.xcall(#selectionEnd);
this.update({ visible: !this.visible });
self.timer(30ms, function() {
var el = this.$(input);
view.focus = el;
el.value = value;
el.xcall(#setSelection, start, end);
});
}
}
var body;
class Body: Reactor.Component {
function this() {
body = this;
}
function getIcon(color) {
if (type == "input-password") {
return <svg viewBox="0 0 505 505"><circle cx="252.5" cy="252.5" r="252.5" fill={color}/><path d="M271.9 246.1c29.2 17.5 67.6 13.6 92.7-11.5 29.7-29.7 29.7-77.8 0-107.4s-77.8-29.7-107.4 0c-25.1 25.1-29 63.5-11.5 92.7L118.1 347.4l26.2 26.2 26.4 26.4 10.6-10.6-10.1-10.1 9.7-9.7 10.1 10.1 10.6-10.6-10.1-10 9.7-9.7 10.1 10.1 10.6-10.6-26.4-26.3 76.4-76.5z" fill="#fff"/><circle cx="337.4" cy="154.4" r="17.7" fill={color}/></svg>;
}
if (type == "connecting") {
return <svg viewBox="0 0 300 300"><g fill={color}><path d="m221.76 89.414h-143.51c-1.432 0-2.594 1.162-2.594 2.594v95.963c0 1.432 1.162 2.594 2.594 2.594h143.51c1.432 0 2.594-1.162 2.594-2.594v-95.964c0-1.431-1.162-2.593-2.594-2.593z"/><path d="m150 0c-82.839 0-150 67.161-150 150s67.156 150 150 150 150-67.163 150-150-67.164-150-150-150zm92.508 187.97c0 11.458-9.29 20.749-20.749 20.749h-47.144v11.588h23.801c4.298 0 7.781 3.483 7.781 7.781s-3.483 7.781-7.781 7.781h-96.826c-4.298 0-7.781-3.483-7.781-7.781s3.483-7.781 7.781-7.781h23.801v-11.588h-47.145c-11.458 0-20.749-9.29-20.749-20.749v-95.963c0-11.458 9.29-20.749 20.749-20.749h143.51c11.458 0 20.749 9.29 20.749 20.749v95.963z"/></g><path d="m169.62 154.35c-5.0276-5.0336-11.97-8.1508-19.624-8.1508-7.6551 0-14.597 3.1172-19.624 8.1508l-11.077-11.091c7.8656-7.8752 18.725-12.754 30.701-12.754s22.835 4.8788 30.701 12.754l-11.077 11.091zm-32.184 7.0728 12.56 12.576 12.56-12.576c-3.2147-3.2172-7.6555-5.208-12.56-5.208-4.9054 0-9.3457 1.9908-12.56 5.208zm12.56-39.731c14.403 0 27.464 5.8656 36.923 15.338l11.078-11.091c-12.298-12.314-29.276-19.94-48-19.94-18.724 0-35.703 7.626-48 19.94l11.077 11.091c9.4592-9.4728 22.52-15.338 36.923-15.338z" fill="#fff"/></svg>;
}
if (type == "success") {
return <svg viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill={color} /><path fill="#fff" d="M235.472 392.08l-121.04-94.296 34.416-44.168 74.328 57.904 122.672-177.016 46.032 31.888z"/></svg>;
}
if (type.indexOf("error") >= 0 || type == "re-input-password") {
return <svg viewBox="0 0 512 512"><ellipse cx="256" cy="256" rx="256" ry="255.832" fill={color}/><g fill="#fff"><path d="M376.812 337.18l-39.592 39.593-201.998-201.999 39.592-39.592z"/><path d="M376.818 174.825L174.819 376.824l-39.592-39.592 201.999-201.999z"/></g></svg>;
}
return <span />;
}
function getInputPasswordContent() {
var ts = remember ? { checked: true } : {};
return <div .form>
<div>Please enter your password</div>
<Password />
<div><button|checkbox(remember) {ts}>Remember password</button></div>
</div>;
}
function getContent() {
if (type == "input-password") {
return this.getInputPasswordContent();
}
return text;
}
function getColor() {
if (type == "input-password") {
return "#AD448E";
}
if (type == "success") {
return "#32bea6";
}
if (type.indexOf("error") >= 0 || type == "re-input-password") {
return "#e04f5f";
}
return "#2C8CFF";
}
function hasSkip() {
return type.indexOf("skip") >= 0;
}
function render() {
var color = this.getColor();
var icon = this.getIcon(color);
var content = this.getContent();
var hasCancel = type.indexOf("error") < 0 && type != "success" && type.indexOf("nocancel") < 0;
var hasOk = type != "connecting" && type.indexOf("nook") < 0;
var hasClose = type.indexOf("hasclose") >= 0;
var show_progress = type == "connecting";
self.style.set { border: color + " solid 1px" };
var me = this;
self.timer(1ms, function() {
if (typeof content == "string")
me.$(#content).html = content;
else
me.$(#content).content(content);
});
return (
<div style="size: *">
<header style={"height: 2em; background: " + color}>
<caption role="window-caption">{title}</caption>
</header>
<div style="padding: 1em 2em; size: *;">
<div style="height: *; flow: horizontal">
{icon && <div style="height: *; margin: * 0; padding-right: 2em;">{icon}</div>}
<div style="size: *; margin: * 0;" #content />
</div>
<div style="text-align: right;">
<span #error />
{show_progress ? <progress style={"color:" + color} /> : ""}
{hasCancel || hasRetry ? <button .button #cancel .outline>{hasRetry ? "OK" : "Cancel"}</button> : ""}
{this.hasSkip() ? <button .button #skip .outline>Skip</button> : ""}
{hasOk || hasRetry ? <button .button #submit>{hasRetry ? "Retry" : "OK"}</button> : ""}
{hasClose ? <button .button #cancel .outline>Close</button> : ""}
</div>
</div>
</div>);
}
event click $(.custom-event) (_, me) {
if (callback) callback(me);
}
}
$(body).content(<Body />);
function submit() {
if ($(button#submit)) {
$(button#submit).sendEvent("click");
}
}
function cancel() {
if ($(button#cancel)) {
$(button#cancel).sendEvent("click");
}
}
event click $(button#cancel) {
view.close();
if (callback) callback(null);
}
event click $(button#skip) {
var values = getValues();
values.skip = true;
view.close(values);
if (callback) callback(values);
}
function getValues() {
var values = { type: type };
for (var el in $$(.form input)) {
values[el.attributes["name"]] = el.value;
}
for (var el in $$(.form textarea)) {
values[el.attributes["name"]] = el.value;
}
for (var el in $$(.form button)) {
values[el.attributes["name"]] = el.value;
}
if (type == "input-password") {
values.password = (values.password || "").trim();
if (!values.password) {
return;
}
}
return values;
}
event click $(button#submit) {
if (type == "error") {
if (hasRetry) {
view.close({ reconnect: true });
} else {
view.close();
if (callback) callback(null);
}
return;
}
if (type == "re-input-password") {
type = "input-password";
body.update();
set_outline_focus();
return;
}
var values = getValues();
if (callback) {
var err = callback(values);
if (err) {
$(#error).text = err;
return;
}
}
view.close(values);
}
event keydown (evt) {
if (!evt.shortcutKey) {
if (evt.keyCode == Event.VK_ENTER ||
(view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) {
submit();
}
if (evt.keyCode == Event.VK_ESCAPE) {
cancel();
}
}
}
function set_outline_focus() {
self.timer(30ms, function() {
var el = $(input.outline-focus);
if (el) view.focus = el;
else {
el = $(#submit);
if (el) view.focus = el;
}
});
}
set_outline_focus();
function checkParams() {
self.timer(30ms, function() {
var tmp = getParams();
if (!tmp || !tmp.type) {
view.close("!alive");
return;
} else if (tmp != params) {
params = tmp;
updateParams(params);
body.update();
set_outline_focus();
}
checkParams();
});
}
checkParams();

77
src/ui/port_forward.tis Normal file
View File

@@ -0,0 +1,77 @@
class PortForward: Reactor.Component {
function render() {
var args = handler.get_args();
var is_rdp = handler.is_rdp();
if (is_rdp) {
this.pfs = [["", "", "RDP"]];
args = ["rdp"];
} else if (args.length) {
this.pfs = [args];
} else {
this.pfs = handler.get_port_forwards();
}
var pfs = this.pfs.map(function(pf, i) {
return <tr key={i} .value>
<td>{is_rdp ? <button .button #new-rdp>New RDP</button> : pf[0]}</td>
<td .right-arrow style="text-align: center; padding-left: 0">{args.length ? svg_arrow : ""}</td>
<td>{pf[1] || "localhost"}</td>
<td>{pf[2]}</td>
{args.length ? "" : <td .remove>{svg_cancel}</td>}
</tr>;
});
return <div #file-transfer><section>
{pfs.length ? <div style="background: green; color: white; text-align: center; padding: 0.5em;">
<span style="font-size: 1.2em">Listenning ...</span><br/>
<span style="font-size: 0.8em; color: #ddd">Don't close this window while your are using tunnel</span>
</div> : ""}
<table #port-forward>
<thead>
<tr>
<th>Local Port</th>
<th style="width: 1em" />
<th>Remote Host</th>
<th>Remote Port</th>
{args.length ? "" : <th style="width: 6em">Action</th>}
</tr>
</thead>
<tbody key={pfs.length}>
{args.length ? "" :
<tr>
<td><input|number #port /></td>
<td .right-arrow style="text-align: center">{svg_arrow}</td>
<td><input|text #remote-host novalue="localhost" /></td>
<td><input|number #remote-port /></td>
<td style="margin:0;"><button .button #add>Add</button></td>
</tr>
}
{pfs}
</tbody>
</table></section></div>;
}
event click $(#add) () {
var port = ($(#port).value || "").toInteger() || 0;
var remote_host = $(#remote-host).value || "";
var remote_port = ($(#remote-port).value || "").toInteger() || 0;
if (port <= 0 || remote_port <= 0) return;
handler.add_port_forward(port, remote_host, remote_port);
this.update();
}
event click $(#new-rdp) {
handler.new_rdp();
}
event click $(.remove svg) (_, me) {
var pf = this.pfs[me.parent.parent.index - 1];
handler.remove_port_forward(pf[0]);
this.update();
}
}
function initializePortForward()
{
$(#file-transfer-wrapper).content(<PortForward />);
$(#video-wrapper).style.set { visibility: "hidden", position: "absolute" };
$(#file-transfer-wrapper).style.set { display: "block" };
}

37
src/ui/remote.css Normal file
View File

@@ -0,0 +1,37 @@
body {
margin: 0;
color: black;
overflow: scroll-indicator;
}
div#video-wrapper {
size: *;
background: #212121;
}
video#handler {
behavior: native-remote video;
size: *;
margin: *;
foreground-size: contain;
position: relative;
}
img#cursor {
position: absolute;
display: none;
//opacity: 0.66,
//transform: scale(0.8);
}
.goup {
transform: rotate(90deg);
}
table#remote-folder-view {
context-menu: selector(menu#remote-folder-view);
}
table#local-folder-view {
context-menu: selector(menu#local-folder-view);
}

33
src/ui/remote.html Normal file
View File

@@ -0,0 +1,33 @@
<html window-resizable window-frame="extended">
<head>
<style>
@import url(common.css);
@import url(remote.css);
@import url(file_transfer.css);
@import url(header.css);
</style>
<script type="text/tiscript">
include "common.tis";
include "remote.tis";
include "file_transfer.tis";
include "port_forward.tis";
include "grid.tis";
include "header.tis";
</script>
</head>
<header>
<div.window-icon role="window-icon"><icon /></div>
<caption role="window-caption" />
<div.window-toolbar />
<div.window-buttons />
</header>
<body>
<div #video-wrapper>
<video #handler>
<img #cursor src="in-memory:cursor" />
</video>
</div>
<div #file-transfer-wrapper>
</div>
</body>
</html>

1660
src/ui/remote.rs Normal file

File diff suppressed because it is too large Load Diff

434
src/ui/remote.tis Normal file
View File

@@ -0,0 +1,434 @@
var cursor_img = $(img#cursor);
var last_key_time = 0;
is_file_transfer = handler.is_file_transfer();
var is_port_forward = handler.is_port_forward();
var display_width = 0;
var display_height = 0;
var display_origin_x = 0;
var display_origin_y = 0;
var display_scale = 1;
var keyboard_enabled = true; // server side
var clipboard_enabled = true; // server side
var audio_enabled = true; // server side
handler.setDisplay = function(x, y, w, h) {
display_width = w;
display_height = h;
display_origin_x = x;
display_origin_y = y;
adaptDisplay();
}
function adaptDisplay() {
var w = display_width;
var h = display_height;
if (!w || !h) return;
var style = handler.get_view_style();
display_scale = 1.;
var (sx, sy, sw, sh) = view.screenBox(view.windowState == View.WINDOW_FULL_SCREEN ? #frame : #workarea, #rectw);
if (sw >= w && sh > h) {
var hh = $(header).box(#height, #border);
var el = $(div#adjust-window);
if (sh > h + hh && el) {
el.style.set{ display: "block" };
el = $(li#adjust-window);
el.style.set{ display: "block" };
el.onClick = function() {
view.windowState == View.WINDOW_SHOWN;
var (x, y) = view.box(#position, #border, #screen);
// extra for border
var extra = 2;
view.move(x, y, w + extra, h + hh + extra);
}
}
}
if (style != "original") {
var bw = $(body).box(#width, #border);
var bh = $(body).box(#height, #border);
if (view.windowState == View.WINDOW_FULL_SCREEN) {
bw = sw;
bh = sh;
}
if (bw > 0 && bh > 0) {
var scale_x = bw.toFloat() / w;
var scale_y = bh.toFloat() / h;
var scale = scale_x < scale_y ? scale_x : scale_y;
if ((scale > 1 && style == "stretch") ||
(scale < 1 && style == "shrink")) {
display_scale = scale;
w = w * scale;
h = h * scale;
}
}
}
handler.style.set {
width: w + "px",
height: h + "px",
};
}
// https://sciter.com/event-handling/
// https://sciter.com/docs/content/sciter/Event.htm
var entered = false;
var keymap = {};
for (var (k, v) in Event) {
k = k + ""
if (k[0] == "V" && k[1] == "K") {
keymap[v] = k;
}
}
// VK_ENTER = VK_RETURN
// somehow, handler.onKey and view.onKey not working
function self.onKey(evt) {
last_key_time = getTime();
if (is_file_transfer || is_port_forward) return false;
if (!entered) return false;
if (!keyboard_enabled) return false;
switch (evt.type) {
case Event.KEY_DOWN:
handler.key_down_or_up(1, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
if (is_osx && evt.commandKey) {
handler.key_down_or_up(0, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
}
break;
case Event.KEY_UP:
handler.key_down_or_up(0, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey,
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
break;
case Event.KEY_CHAR:
// the keypress event is fired when the element receives character value. Event.keyCode is a UNICODE code point of the character
handler.key_down_or_up(2, "", evt.keyCode, evt.altKey,
evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey);
break;
default:
return false;
}
return true;
}
var wait_window_toolbar = false;
var last_mouse_mask;
var acc_wheel_delta_x = 0;
var acc_wheel_delta_y = 0;
var last_wheel_time = 0;
var inertia_velocity_x = 0;
var inertia_velocity_y = 0;
var acc_wheel_delta_x0 = 0;
var acc_wheel_delta_y0 = 0;
var total_wheel_time = 0;
var wheeling = false;
var dragging = false;
// https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum
function resetWheel() {
acc_wheel_delta_x = 0;
acc_wheel_delta_y = 0;
last_wheel_time = 0;
inertia_velocity_x = 0;
inertia_velocity_y = 0;
acc_wheel_delta_x0 = 0;
acc_wheel_delta_y0 = 0;
total_wheel_time = 0;
wheeling = false;
}
var INERTIA_ACCELERATION = 30;
// not good, precision not enough to simulate accelation effect,
// seems have to use pixel based rather line based delta
function accWheel(v, is_x) {
if (wheeling) return;
var abs_v = Math.abs(v);
var max_t = abs_v / INERTIA_ACCELERATION;
for (var t = 0.1; t < max_t; t += 0.1) {
var d = Math.round((abs_v - t * INERTIA_ACCELERATION / 2) * t).toInteger();
if (d >= 1) {
abs_v -= t * INERTIA_ACCELERATION;
if (v < 0) {
d = -d;
v = -abs_v;
} else {
v = abs_v;
}
handler.send_mouse(3, is_x ? d : 0, !is_x ? d : 0, false, false, false, false);
accWheel(v, is_x);
break;
}
}
}
function handler.onMouse(evt)
{
if (is_file_transfer || is_port_forward) return false;
if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) {
if (evt.y < 10) {
if (!wait_window_toolbar) {
wait_window_toolbar = true;
self.timer(300ms, function() {
if (!wait_window_toolbar) return;
if (view.windowState == View.WINDOW_FULL_SCREEN) {
$(header).style.set {
display: "block",
padding: (2 * workarea_offset) + "px 0 0 0",
};
}
wait_window_toolbar = false;
});
}
} else {
wait_window_toolbar = false;
}
}
var mask = 0;
var wheel_delta_x;
var wheel_delta_y;
switch(evt.type) {
case Event.MOUSE_DOWN:
mask = 1;
dragging = true;
break;
case Event.MOUSE_UP:
mask = 2;
dragging = false;
break;
case Event.MOUSE_MOVE:
if (cursor_img.style#display != "none" && keyboard_enabled) cursor_img.style#display = "none";
break;
case Event.MOUSE_WHEEL:
// mouseWheelDistance = 8 * [currentUserDefs floatForKey:@"com.apple.scrollwheel.scaling"];
// seems buggy, it always -1 or 1, even I change system scrolling speed.
// to-do: should we use client side prefrence or server side?
mask = 3;
{
var (dx, dy) = evt.wheelDeltas;
if (Math.abs(dx) > Math.abs(dy)) {
dy = 0;
} else {
dx = 0;
}
acc_wheel_delta_x += dx;
acc_wheel_delta_y += dy;
wheel_delta_x = acc_wheel_delta_x.toInteger();
wheel_delta_y = acc_wheel_delta_y.toInteger();
acc_wheel_delta_x -= wheel_delta_x;
acc_wheel_delta_y -= wheel_delta_y;
var now = getTime();
var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0;
if (dt > 0) {
var vx = dx / dt;
var vy = dy / dt;
if (vx != 0 || vy != 0) {
inertia_velocity_x = vx;
inertia_velocity_y = vy;
}
}
acc_wheel_delta_x0 += dx;
acc_wheel_delta_y0 += dy;
total_wheel_time += dt;
if (dx == 0 && dy == 0) {
wheeling = false;
if (dt < 0.1 && total_wheel_time > 0) {
var v2 = (acc_wheel_delta_y0 / total_wheel_time) * inertia_velocity_y;
if (v2 > 0) {
v2 = Math.sqrt(v2);
inertia_velocity_y = inertia_velocity_y < 0 ? -v2 : v2;
accWheel(inertia_velocity_y, false);
}
v2 = (acc_wheel_delta_x0 / total_wheel_time) * inertia_velocity_x;
if (v2 > 0) {
v2 = Math.sqrt(v2);
inertia_velocity_x = inertia_velocity_x < 0 ? -v2 : v2;
accWheel(inertia_velocity_x, true);
}
}
resetWheel();
} else {
wheeling = true;
}
last_wheel_time = now;
if (wheel_delta_x == 0 && wheel_delta_y == 0) return keyboard_enabled;
}
break;
case Event.MOUSE_DCLICK: // seq: down, up, dclick, up
mask = 1;
break;
case Event.MOUSE_ENTER:
entered = true;
stdout.println("enter");
if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) {
wait_window_toolbar = false;
$(header).style.set {
display: "none",
};
}
return keyboard_enabled;
case Event.MOUSE_LEAVE:
entered = false;
stdout.println("leave");
return keyboard_enabled;
default:
return false;
}
var x = evt.x;
var y = evt.y;
if (mask != 0) {
// to gain control of the mouse, user must move mouse
if (cur_x != x || cur_y != y) {
return keyboard_enabled;
}
// save bandwidth
x = 0;
y = 0;
} else {
cur_x = x;
cur_y = y;
}
if (mask != 3) {
resetWheel();
}
if (!keyboard_enabled) return false;
x = (x / display_scale).toInteger();
y = (y / display_scale).toInteger();
// insert down between two up, osx has this behavior for triple click
if (last_mouse_mask == 2 && mask == 2) {
handler.send_mouse((evt.buttons << 3) | 1, x + display_origin_x, y + display_origin_y, evt.altKey,
evt.ctrlKey, evt.shiftKey, evt.commandKey);
}
last_mouse_mask = mask;
// to-do: altKey, ctrlKey etc
handler.send_mouse((evt.buttons << 3) | mask,
mask == 3 ? wheel_delta_x : x + display_origin_x,
mask == 3 ? wheel_delta_y : y + display_origin_y,
evt.altKey,
evt.ctrlKey, evt.shiftKey, evt.commandKey);
return true;
};
var cur_hotx = 0;
var cur_hoty = 0;
var cur_img = null;
var cur_x = 0;
var cur_y = 0;
var cursors = {};
var image_binded;
handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
cur_hotx = hotx;
cur_hoty = hoty;
cursor_img.style.set {
width: width + "px",
height: height + "px",
};
var img = Image.fromBytes(colors);
if (img) {
image_binded = true;
cursors[id] = [img, hotx, hoty, width, height];
this.bindImage("in-memory:cursor", img);
self.timer(1ms, function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty); });
cur_img = img;
}
}
handler.setCursorId = function(id) {
var img = cursors[id];
if (img) {
image_binded = true;
cur_hotx = img[1];
cur_hoty = img[2];
cursor_img.style.set {
width: img[3] + "px",
height: img[4] + "px",
};
img = img[0];
this.bindImage("in-memory:cursor", img);
self.timer(1ms, function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty); });
cur_img = img;
}
}
handler.setCursorPosition = function(x, y) {
if (!image_binded) return;
cur_x = x - display_origin_x;
cur_y = y - display_origin_y;
var x = cur_x - cur_hotx;
var y = cur_y - cur_hoty;
x *= display_scale;
y *= display_scale;
cursor_img.style.set {
left: x + "px",
top: y + "px",
display: "block",
};
handler.style.cursor(null);
}
function self.ready() {
var w = 960;
var h = 640;
if (is_file_transfer || is_port_forward) {
var r = handler.get_size();
if (r[0] > 0) {
view.move(r[0], r[1], r[2], r[3]);
} else {
centerize(w, h);
}
} else {
centerize(w, h);
}
if (!is_port_forward) connecting();
if (is_file_transfer) initializeFileTransfer();
if (is_port_forward) initializePortForward();
}
var workarea_offset = 0;
var size_adapted;
handler.adaptSize = function() {
if (size_adapted) return;
size_adapted = true;
var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw);
var (fx, fy, fw, fh) = view.screenBox(#frame, #rectw);
workarea_offset = sy;
var r = handler.get_size();
if (r[2] > 0) {
if (r[2] >= fw && r[3] >= fh) {
view.windowState = View.WINDOW_FULL_SCREEN;
} else if (r[2] >= sw && r[3] >= sh) {
view.windowState = View.WINDOW_MAXIMIZED;
} else {
view.move(r[0], r[1], r[2], r[3]);
}
} else {
var w = handler.box(#width, #border)
if (sw == w) {
view.windowState = View.WINDOW_MAXIMIZED;
return;
}
var h = $(header).box(#height, #border);
// extra for border
var extra = 2;
centerize(w + extra, handler.box(#height, #border) + h + extra);
}
}
function self.closing() {
var (x, y, w, h) = view.box(#rectw, #border, #screen);
if (is_file_transfer) save_file_transfer_close_state();
if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h);
}
handler.setPermission = function(name, enabled) {
if (name == "keyboard") keyboard_enabled = enabled;
if (name == "audio") audio_enabled = enabled;
if (name == "clipboard") clipboard_enabled = enabled;
header.update();
}
handler.closeSuccess = function() {
// handler.msgbox("success", "Successful", "Ready to go.");
handler.msgbox("", "", "");
}