Add .gitignore and .ratignore files for various directories
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
6
guacamole/.gitignore
vendored
Normal file
6
guacamole/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
src/main/webapp/META-INF/
|
||||
src/main/webapp/generated/
|
||||
nb-configuration.xml
|
||||
customs.json
|
||||
target/
|
||||
*~
|
||||
1
guacamole/.ratignore
Normal file
1
guacamole/.ratignore
Normal file
@@ -0,0 +1 @@
|
||||
src/main/frontend/dist/**/*
|
||||
70
guacamole/doc/example/user-mapping.xml
Normal file
70
guacamole/doc/example/user-mapping.xml
Normal file
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<user-mapping>
|
||||
|
||||
<!-- Per-user authentication and config information -->
|
||||
<authorize username="USERNAME" password="PASSWORD">
|
||||
<protocol>vnc</protocol>
|
||||
<param name="hostname">localhost</param>
|
||||
<param name="port">5900</param>
|
||||
<param name="password">VNCPASS</param>
|
||||
</authorize>
|
||||
|
||||
<!-- Another user, but using md5 to hash the password
|
||||
(example below uses the md5 hash of "PASSWORD") -->
|
||||
<authorize
|
||||
username="USERNAME2"
|
||||
password="319f4d26e3c536b5dd871bb2c52e3178"
|
||||
encoding="md5">
|
||||
|
||||
<!-- First authorized connection -->
|
||||
<connection name="localhost">
|
||||
<protocol>vnc</protocol>
|
||||
<param name="hostname">localhost</param>
|
||||
<param name="port">5901</param>
|
||||
<param name="password">VNCPASS</param>
|
||||
</connection>
|
||||
|
||||
<!-- Second authorized connection -->
|
||||
<connection name="otherhost">
|
||||
<protocol>vnc</protocol>
|
||||
<param name="hostname">otherhost</param>
|
||||
<param name="port">5900</param>
|
||||
<param name="password">VNCPASS</param>
|
||||
</connection>
|
||||
|
||||
</authorize>
|
||||
|
||||
<!-- Another user, but using SHA-256 to hash the password -->
|
||||
<authorize
|
||||
username="USERNAME3"
|
||||
password="5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
|
||||
encoding="sha256">
|
||||
|
||||
<connection name="localhost">
|
||||
<protocol>vnc</protocol>
|
||||
<param name="hostname">localhost</param>
|
||||
<param name="port">5900</param>
|
||||
<param name="password">VNCPASS</param>
|
||||
</connection>
|
||||
|
||||
</authorize>
|
||||
|
||||
</user-mapping>
|
||||
379
guacamole/pom.xml
Normal file
379
guacamole/pom.xml
Normal file
@@ -0,0 +1,379 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<version>1.6.0</version>
|
||||
<name>guacamole</name>
|
||||
<url>http://guacamole.apache.org/</url>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-client</artifactId>
|
||||
<version>1.6.0</version>
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
|
||||
<description>
|
||||
The Guacamole web application, providing authentication and an HTML5
|
||||
remote desktop client.
|
||||
</description>
|
||||
|
||||
<!-- All applicable licenses -->
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache License, Version 2.0</name>
|
||||
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<!-- Git repository -->
|
||||
<scm>
|
||||
<url>https://github.com/apache/guacamole-client</url>
|
||||
<connection>scm:git:https://git.wip-us.apache.org/repos/asf/guacamole-client.git</connection>
|
||||
</scm>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<!-- Build AngularJS portion of application using NPM -->
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<version>1.11.3</version>
|
||||
<configuration>
|
||||
<workingDirectory>src/main/frontend</workingDirectory>
|
||||
<installDirectory>${project.build.directory}</installDirectory>
|
||||
|
||||
<!-- Newer Node.js requires the following to avoid an "ERR_OSSL_EVP_UNSUPPORTED"
|
||||
error when WebPack attempts to use its default hash (MD4) for content hashing -->
|
||||
<environmentVariables>
|
||||
<NODE_OPTIONS>--openssl-legacy-provider</NODE_OPTIONS>
|
||||
</environmentVariables>
|
||||
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>install-node-and-npm</id>
|
||||
<goals>
|
||||
<goal>install-node-and-npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<nodeVersion>v18.18.0</nodeVersion>
|
||||
<npmVersion>9.8.1</npmVersion>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>npm-ci</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<arguments>ci</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>npm-build</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<arguments>run build</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- Copy automatically-generated set of NPM module dependencies for
|
||||
later use by LICENSE generator -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-npm-dependency-list</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${dependency.list.directory}</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/frontend/dist</directory>
|
||||
<includes>
|
||||
<include>npm-dependencies.txt</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- Automatically generate LICENSE and NOTICE -->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>generate-license-files</id>
|
||||
<phase>generate-resources</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<configuration>
|
||||
|
||||
<webResources>
|
||||
|
||||
<!-- Add frontend folder to war package ... -->
|
||||
<resource>
|
||||
<directory>src/main/frontend/dist</directory>
|
||||
<excludes>
|
||||
<exclude>translations/*.json</exclude>
|
||||
<exclude>index.html</exclude>
|
||||
<exclude>verifyCachedVersion.js</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
|
||||
<!-- ... but filter index.html and translation strings -->
|
||||
<resource>
|
||||
<directory>src/main/frontend/dist</directory>
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>translations/*.json</include>
|
||||
<include>index.html</include>
|
||||
<include>verifyCachedVersion.js</include>
|
||||
</includes>
|
||||
</resource>
|
||||
|
||||
<!-- Include all licenses within META-INF -->
|
||||
<resource>
|
||||
<directory>${project.build.directory}/licenses</directory>
|
||||
<targetPath>META-INF</targetPath>
|
||||
</resource>
|
||||
|
||||
</webResources>
|
||||
|
||||
<!-- Add files from guacamole-common-js -->
|
||||
<overlays>
|
||||
<overlay>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-common-js</artifactId>
|
||||
<type>zip</type>
|
||||
</overlay>
|
||||
</overlays>
|
||||
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-cli</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>exploded</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Java servlet API -->
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
<version>2.5</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JSR 356 WebSocket API -->
|
||||
<dependency>
|
||||
<groupId>javax.websocket</groupId>
|
||||
<artifactId>javax.websocket-api</artifactId>
|
||||
<version>1.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jul-to-slf4j</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<exclusions>
|
||||
|
||||
<!-- Exclude optional dependency on JavaMail -->
|
||||
<exclusion>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>javax.mail</artifactId>
|
||||
</exclusion>
|
||||
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Guacamole Extension API -->
|
||||
<dependency>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-ext</artifactId>
|
||||
<version>1.6.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Guacamole JavaScript API -->
|
||||
<dependency>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-common-js</artifactId>
|
||||
<version>1.6.0</version>
|
||||
<type>zip</type>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Jetty 8 servlet API (websocket) -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-websocket</artifactId>
|
||||
<version>8.1.1.v20120215</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Jetty 9.0 servlet API (websocket) -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-parent</artifactId>
|
||||
<version>20</version>
|
||||
<scope>provided</scope>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-api</artifactId>
|
||||
<version>9.0.7.v20131107</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-servlet</artifactId>
|
||||
<version>9.0.7.v20131107</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Tomcat servlet API (websocket) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>tomcat-catalina</artifactId>
|
||||
<version>7.0.37</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>tomcat-coyote</artifactId>
|
||||
<version>7.0.37</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Guice - Dependency Injection -->
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.inject.extensions</groupId>
|
||||
<artifactId>guice-assistedinject</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.inject.extensions</groupId>
|
||||
<artifactId>guice-servlet</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Jersey - JAX-RS Implementation -->
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.containers</groupId>
|
||||
<artifactId>jersey-container-servlet-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.inject</groupId>
|
||||
<artifactId>jersey-hk2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.hk2</groupId>
|
||||
<artifactId>guice-bridge</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.media</groupId>
|
||||
<artifactId>jersey-media-json-jackson</artifactId>
|
||||
<exclusions>
|
||||
|
||||
<!-- Resolve version conflict (see below - transitive
|
||||
dependencies of jersey-media-json-jackson disagree on
|
||||
1.2.1 vs. 1.2.2) -->
|
||||
<exclusion>
|
||||
<groupId>jakarta.activation</groupId>
|
||||
<artifactId>jakarta.activation-api</artifactId>
|
||||
</exclusion>
|
||||
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- JSR-250 annotations -->
|
||||
<dependency>
|
||||
<groupId>javax.annotation</groupId>
|
||||
<artifactId>jsr250-api</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Guava Base Libraries -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Force use of version 1.2.2 (transitive dependencies of
|
||||
jersey-media-json-jackson disagree on 1.2.1 vs. 1.2.2) -->
|
||||
<dependency>
|
||||
<groupId>jakarta.activation</groupId>
|
||||
<artifactId>jakarta.activation-api</artifactId>
|
||||
<version>1.2.2</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Jed Richards
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,8 @@
|
||||
angular-module-shim (https://github.com/jedrichards/angular-module-shim)
|
||||
------------------------------------------------------------------------
|
||||
|
||||
Version: 0.0.4
|
||||
From: 'Jed Richards' (https://github.com/jedrichards)
|
||||
License(s):
|
||||
MIT (bundled/angular-module-shim-0.0.4/LICENSE)
|
||||
|
||||
95
guacamole/src/licenses/bundled/carlito/LICENSE
Normal file
95
guacamole/src/licenses/bundled/carlito/LICENSE
Normal file
@@ -0,0 +1,95 @@
|
||||
Copyright (c) 2010-2013 by tyPoland Lukasz Dziedzic with Reserved Font Name "Carlito".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1 as shown below.
|
||||
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
|
||||
PREAMBLE The goals of the Open Font License (OFL) are to stimulate
|
||||
worldwide development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to provide
|
||||
a free and open framework in which fonts may be shared and improved in
|
||||
partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves.
|
||||
The fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such.
|
||||
This may include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components
|
||||
as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting ? in part or in whole ?
|
||||
any of the components of the Original Version, by changing formats or
|
||||
by porting the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical writer
|
||||
or other person who contributed to the Font Software.
|
||||
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole, must
|
||||
be distributed entirely under this license, and must not be distributed
|
||||
under any other license. The requirement for fonts to remain under
|
||||
this license does not apply to any document created using the Font
|
||||
Software.
|
||||
|
||||
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are not met.
|
||||
|
||||
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||
DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
8
guacamole/src/licenses/bundled/carlito/README
Normal file
8
guacamole/src/licenses/bundled/carlito/README
Normal file
@@ -0,0 +1,8 @@
|
||||
Carlito (http://code.google.com/p/chromium/issues/detail?id=280557)
|
||||
-------------------------------------------------------------------
|
||||
|
||||
Version: N/A
|
||||
From: 'tyPoland Lukasz Dziedzic' (http://www.lukaszdziedzic.eu/)
|
||||
License(s):
|
||||
SIL Open Font (bundled/carlito/LICENSE)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (C) 2019 Glyptodon, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,9 @@
|
||||
Session Recording Player for Glyptodon Enterprise
|
||||
(https://github.com/glyptodon/glyptodon-enterprise-player)
|
||||
----------------------------------------------------------
|
||||
|
||||
Version: 1.1.0-1
|
||||
From: 'Glyptodon, Inc.' (https://glyptodon.com/)
|
||||
License(s):
|
||||
MIT (bundled/glyptodon-enterprise-player/LICENSE)
|
||||
|
||||
3
guacamole/src/main/frontend/.gitignore
vendored
Normal file
3
guacamole/src/main/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*~
|
||||
node_modules
|
||||
dist
|
||||
10612
guacamole/src/main/frontend/package-lock.json
generated
Normal file
10612
guacamole/src/main/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
guacamole/src/main/frontend/package.json
Normal file
43
guacamole/src/main/frontend/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "webpack --progress"
|
||||
},
|
||||
"dependencies": {
|
||||
"@simonwep/pickr": "^1.8.2",
|
||||
"angular": "^1.8.3",
|
||||
"angular-route": "^1.8.3",
|
||||
"angular-templatecache-webpack-plugin": "^1.0.1",
|
||||
"angular-translate": "^2.19.1",
|
||||
"angular-translate-interpolation-messageformat": "^2.19.1",
|
||||
"angular-translate-loader-static-files": "^2.19.1",
|
||||
"blob-polyfill": "^9.0.20240710",
|
||||
"csv": "^6.3.9",
|
||||
"d3-path": "^3.1.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"datalist-polyfill": "^1.25.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"fuzzysort": "^3.0.2",
|
||||
"jquery": "^3.7.1",
|
||||
"jstz": "^2.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"yaml": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.7",
|
||||
"@babel/preset-env": "^7.24.7",
|
||||
"babel-loader": "^8.3.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"closure-webpack-plugin": "^2.6.1",
|
||||
"copy-webpack-plugin": "^5.1.2",
|
||||
"css-loader": "^5.2.7",
|
||||
"css-minimizer-webpack-plugin": "^1.3.0",
|
||||
"exports-loader": "^1.1.1",
|
||||
"find-package-json": "^1.2.0",
|
||||
"google-closure-compiler": "20240317.0.0",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"mini-css-extract-plugin": "^1.6.2",
|
||||
"webpack": "^4.47.0",
|
||||
"webpack-cli": "^4.10.0"
|
||||
}
|
||||
}
|
||||
157
guacamole/src/main/frontend/plugins/dependency-list-plugin.js
Normal file
157
guacamole/src/main/frontend/plugins/dependency-list-plugin.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
const finder = require('find-package-json');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const validateOptions = require('schema-utils');
|
||||
|
||||
/**
|
||||
* The name of this plugin.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const PLUGIN_NAME = 'dependency-list-plugin';
|
||||
|
||||
/**
|
||||
* The schema of the configuration options object accepted by the constructor
|
||||
* of DependencyListPlugin.
|
||||
*
|
||||
* @see https://github.com/webpack/schema-utils/blob/v1.0.0/README.md#usage
|
||||
*/
|
||||
const PLUGIN_OPTIONS_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
/**
|
||||
* The name of the file that should contain the dependency list. By
|
||||
* default, this will be "npm-dependencies.txt".
|
||||
*/
|
||||
filename: { type: 'string' },
|
||||
|
||||
/**
|
||||
* The path in which the dependency list file should be saved. By
|
||||
* default, this will be the output path of the Webpack compiler.
|
||||
*/
|
||||
path: { type: 'string' }
|
||||
|
||||
},
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Webpack plugin that automatically lists each of the NPM dependencies
|
||||
* included within any bundles produced by the compile process.
|
||||
*/
|
||||
class DependencyListPlugin {
|
||||
|
||||
/**
|
||||
* Creates a new DependencyListPlugin configured with the given options.
|
||||
* The options given must conform to the options schema.
|
||||
*
|
||||
* @see PLUGIN_OPTIONS_SCHEMA
|
||||
*
|
||||
* @param {*} options
|
||||
* The configuration options to apply to the plugin.
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
validateOptions(PLUGIN_OPTIONS_SCHEMA, options, 'DependencyListPlugin');
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint for all Webpack plugins. This function will be invoked when
|
||||
* the plugin is being associated with the compile process.
|
||||
*
|
||||
* @param {Compiler} compiler
|
||||
* A reference to the Webpack compiler.
|
||||
*/
|
||||
apply(compiler) {
|
||||
|
||||
/**
|
||||
* Logger for this plugin.
|
||||
*
|
||||
* @type {Logger}
|
||||
*/
|
||||
const logger = compiler.getInfrastructureLogger(PLUGIN_NAME);
|
||||
|
||||
/**
|
||||
* The directory receiving the dependency list file.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const outputPath = this.options.path || compiler.options.output.path;
|
||||
|
||||
/**
|
||||
* The full path to the output file that should contain the list of
|
||||
* discovered NPM module dependencies.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const outputFile = path.join(
|
||||
outputPath,
|
||||
this.options.filename || 'npm-dependencies.txt'
|
||||
);
|
||||
|
||||
// Wait for compilation to fully complete
|
||||
compiler.hooks.done.tap(PLUGIN_NAME, (stats) => {
|
||||
|
||||
const moduleCoords = {};
|
||||
|
||||
// Map each file used within any bundle built by the compiler to
|
||||
// its corresponding NPM package, ignoring files that have no such
|
||||
// package
|
||||
stats.compilation.fileDependencies.forEach(file => {
|
||||
|
||||
// Locate NPM package corresponding to file dependency (there
|
||||
// may not be one)
|
||||
const moduleFinder = finder(file);
|
||||
const npmPackage = moduleFinder.next().value;
|
||||
|
||||
// Translate absolute path into more readable path relative to
|
||||
// root of compilation process
|
||||
const relativePath = path.relative(compiler.options.context, file);
|
||||
|
||||
if (npmPackage.name) {
|
||||
moduleCoords[npmPackage.name + ':' + npmPackage.version] = true;
|
||||
logger.info('File dependency "%s" mapped to NPM package "%s" (v%s)',
|
||||
relativePath, npmPackage.name, npmPackage.version);
|
||||
}
|
||||
else
|
||||
logger.info('Skipping file dependency "%s" (no NPM package)',
|
||||
relativePath);
|
||||
|
||||
});
|
||||
|
||||
// Create output path if it doesn't yet exist
|
||||
if (!fs.existsSync(outputPath))
|
||||
fs.mkdirSync(outputPath, { recursive: true, mode: 0o755 });
|
||||
|
||||
// Write all discovered NPM packages to configured output file
|
||||
const sortedCoords = Object.keys(moduleCoords).sort();
|
||||
fs.writeFileSync(outputFile, sortedCoords.join('\n') + '\n');
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = DependencyListPlugin;
|
||||
|
||||
56
guacamole/src/main/frontend/src/angular-module-shim.js
vendored
Normal file
56
guacamole/src/main/frontend/src/angular-module-shim.js
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014 Jed Richards
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
(function(angular) {
|
||||
|
||||
'use strict';
|
||||
|
||||
if ( !angular ) {
|
||||
throw new Error('angular-module-shim: Missing Angular');
|
||||
}
|
||||
|
||||
var origFn = angular.module;
|
||||
var hash = {};
|
||||
|
||||
angular.module = function(name,requires,configFn) {
|
||||
|
||||
var requires = requires || [];
|
||||
var registered = hash[name];
|
||||
var module;
|
||||
|
||||
if ( registered ) {
|
||||
module = origFn(name);
|
||||
module.requires.push.apply(module.requires,requires);
|
||||
// Register the config function if it exists.
|
||||
if (configFn) {
|
||||
module.config(configFn);
|
||||
}
|
||||
} else {
|
||||
hash[name] = true;
|
||||
module = origFn(name,requires,configFn);
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
})(window.angular);
|
||||
26
guacamole/src/main/frontend/src/app/auth/authModule.js
Normal file
26
guacamole/src/main/frontend/src/app/auth/authModule.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The module for authentication and management of tokens.
|
||||
*/
|
||||
angular.module('auth', [
|
||||
'rest',
|
||||
'storage'
|
||||
]);
|
||||
@@ -0,0 +1,550 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A service for authenticating a user against the REST API. Invoking the
|
||||
* authenticate() or login() functions of this service will automatically
|
||||
* affect the login dialog, if visible.
|
||||
*
|
||||
* This service broadcasts events on $rootScope depending on the status and
|
||||
* result of authentication operations:
|
||||
*
|
||||
* "guacLoginPending"
|
||||
* An authentication request is being submitted and we are awaiting the
|
||||
* result. The request may not yet have been submitted if the parameters
|
||||
* for that request are not ready. This event receives a promise that
|
||||
* resolves with the HTTP parameters that were ultimately submitted as its
|
||||
* sole parameter.
|
||||
*
|
||||
* "guacLogin"
|
||||
* Authentication was successful and a new token was created. This event
|
||||
* receives the authentication token as its sole parameter.
|
||||
*
|
||||
* "guacLogout"
|
||||
* An existing token is being destroyed. This event receives the
|
||||
* authentication token as its sole parameter. If the existing token for
|
||||
* the current session is being replaced without destroying that session,
|
||||
* this event is not fired.
|
||||
*
|
||||
* "guacLoginFailed"
|
||||
* An authentication request has failed for any reason. This event is
|
||||
* broadcast before any other events that are specific to the nature of
|
||||
* the failure, and may be used to detect login failures in lieu of those
|
||||
* events. This event receives two parameters: the HTTP parameters
|
||||
* submitted and the Error object received from the REST endpoint.
|
||||
*
|
||||
* "guacInsufficientCredentials"
|
||||
* An authentication request failed because additional credentials are
|
||||
* needed before the request can be processed. This event receives two
|
||||
* parameters: the HTTP parameters submitted and the Error object received
|
||||
* from the REST endpoint.
|
||||
*
|
||||
* "guacInvalidCredentials"
|
||||
* An authentication request failed because the credentials provided are
|
||||
* invalid. This event receives two parameters: the HTTP parameters
|
||||
* submitted and the Error object received from the REST endpoint.
|
||||
*/
|
||||
angular.module('auth').factory('authenticationService', ['$injector',
|
||||
function authenticationService($injector) {
|
||||
|
||||
// Required types
|
||||
var AuthenticationResult = $injector.get('AuthenticationResult');
|
||||
var Error = $injector.get('Error');
|
||||
|
||||
// Required services
|
||||
var $q = $injector.get('$q');
|
||||
var $rootScope = $injector.get('$rootScope');
|
||||
var localStorageService = $injector.get('localStorageService');
|
||||
var requestService = $injector.get('requestService');
|
||||
|
||||
var service = {};
|
||||
|
||||
/**
|
||||
* The most recent authentication result, or null if no authentication
|
||||
* result is cached.
|
||||
*
|
||||
* @type AuthenticationResult
|
||||
*/
|
||||
var cachedResult = null;
|
||||
|
||||
/**
|
||||
* The unique identifier of the local storage key which stores the latest
|
||||
* authentication token.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
var AUTH_TOKEN_STORAGE_KEY = 'GUAC_AUTH_TOKEN';
|
||||
|
||||
/**
|
||||
* Retrieves the authentication result cached in memory. If the user has not
|
||||
* yet authenticated, the user has logged out, or the last authentication
|
||||
* attempt failed, null is returned.
|
||||
*
|
||||
* NOTE: setAuthenticationResult() will be called upon page load, so the
|
||||
* cache should always be populated after the page has successfully loaded.
|
||||
*
|
||||
* @returns {AuthenticationResult}
|
||||
* The last successful authentication result, or null if the user is not
|
||||
* currently authenticated.
|
||||
*/
|
||||
var getAuthenticationResult = function getAuthenticationResult() {
|
||||
|
||||
// Use cached result, if any
|
||||
if (cachedResult)
|
||||
return cachedResult;
|
||||
|
||||
// Return explicit null if no auth data is currently stored
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores the given authentication result for future retrieval. The given
|
||||
* result MUST be the result of the most recent authentication attempt.
|
||||
*
|
||||
* @param {AuthenticationResult} data
|
||||
* The last successful authentication result, or null if the last
|
||||
* authentication attempt failed.
|
||||
*/
|
||||
var setAuthenticationResult = function setAuthenticationResult(data) {
|
||||
|
||||
// Clear the currently-stored result and auth token if the last
|
||||
// attempt failed
|
||||
if (!data) {
|
||||
cachedResult = null;
|
||||
localStorageService.removeItem(AUTH_TOKEN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
// Otherwise, store the authentication attempt directly.
|
||||
// Note that only the auth token is stored in persistent local storage.
|
||||
// To re-obtain an autentication result upon a fresh page load,
|
||||
// reauthenticate with the persistent token, which can be obtained by
|
||||
// calling getCurrentToken().
|
||||
else {
|
||||
|
||||
// Always store in cache
|
||||
cachedResult = data;
|
||||
|
||||
// Persist only the auth token past tab/window closure, and only
|
||||
// if not anonymous
|
||||
if (data.username !== AuthenticationResult.ANONYMOUS_USERNAME)
|
||||
localStorageService.setItem(
|
||||
AUTH_TOKEN_STORAGE_KEY, data.authToken);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the stored authentication result, if any. If no authentication
|
||||
* result is currently stored, this function has no effect.
|
||||
*/
|
||||
var clearAuthenticationResult = function clearAuthenticationResult() {
|
||||
setAuthenticationResult(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a request to authenticate a user using the token REST API endpoint
|
||||
* and given arbitrary parameters, returning a promise that succeeds only
|
||||
* if the authentication operation was successful. The resulting
|
||||
* authentication data can be retrieved later via getCurrentToken() or
|
||||
* getCurrentUsername(). Invoking this function will affect the UI,
|
||||
* including the login screen if visible.
|
||||
*
|
||||
* The provided parameters can be virtually any object, as each property
|
||||
* will be sent as an HTTP parameter in the authentication request.
|
||||
* Standard parameters include "username" for the user's username,
|
||||
* "password" for the user's associated password, and "token" for the
|
||||
* auth token to check/update.
|
||||
*
|
||||
* If a token is provided, it will be reused if possible.
|
||||
*
|
||||
* @param {Object|Promise} parameters
|
||||
* Arbitrary parameters to authenticate with. If a Promise is provided,
|
||||
* that Promise must resolve with the parameters to be submitted when
|
||||
* those parameters are available, and any error will be handled as if
|
||||
* from the authentication endpoint of the REST API itself.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise which succeeds only if the login operation was successful.
|
||||
*/
|
||||
service.authenticate = function authenticate(parameters) {
|
||||
|
||||
// Coerce received parameters object into a Promise, if it isn't
|
||||
// already a Promise
|
||||
parameters = $q.resolve(parameters);
|
||||
|
||||
// Notify that a fresh authentication request is underway
|
||||
$rootScope.$broadcast('guacLoginPending', parameters);
|
||||
|
||||
// Attempt authentication after auth parameters are available ...
|
||||
return parameters.then(function requestParametersReady(requestParams) {
|
||||
|
||||
// Strip any properties that are from AngularJS core, such as the
|
||||
// '$$state' property added by $q. Properties added by AngularJS
|
||||
// core will have a '$' prefix. The '$$state' property is
|
||||
// particularly problematic, as it is self-referential and explodes
|
||||
// the stack when fed to $.param().
|
||||
requestParams = _.omitBy(requestParams, (value, key) => key.startsWith('$'));
|
||||
|
||||
return requestService({
|
||||
method: 'POST',
|
||||
url: 'api/tokens',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
data: $.param(requestParams)
|
||||
})
|
||||
|
||||
// ... if authentication succeeds, handle received auth data ...
|
||||
.then(function authenticationSuccessful(data) {
|
||||
|
||||
var currentToken = service.getCurrentToken();
|
||||
|
||||
// If a new token was received, ensure the old token is invalidated,
|
||||
// if any, and notify listeners of the new token
|
||||
if (data.authToken !== currentToken) {
|
||||
|
||||
// If an old token existed, request that the token be revoked
|
||||
if (currentToken) {
|
||||
service.revokeToken(currentToken).catch(angular.noop);
|
||||
}
|
||||
|
||||
// Notify of login and new token
|
||||
setAuthenticationResult(new AuthenticationResult(data));
|
||||
$rootScope.$broadcast('guacLogin', data.authToken);
|
||||
|
||||
}
|
||||
|
||||
// Update cached authentication result, even if the token remains
|
||||
// the same
|
||||
else
|
||||
setAuthenticationResult(new AuthenticationResult(data));
|
||||
|
||||
// Authentication was successful
|
||||
return data;
|
||||
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
// ... if authentication fails, propogate failure to returned promise
|
||||
['catch'](requestService.createErrorCallback(function authenticationFailed(error) {
|
||||
|
||||
// Notify of generic login failure, for any event consumers that
|
||||
// wish to handle all types of failures at once
|
||||
$rootScope.$broadcast('guacLoginFailed', parameters, error);
|
||||
|
||||
// Request credentials if provided credentials were invalid
|
||||
if (error.type === Error.Type.INVALID_CREDENTIALS) {
|
||||
$rootScope.$broadcast('guacInvalidCredentials', parameters, error);
|
||||
clearAuthenticationResult();
|
||||
}
|
||||
|
||||
// Request more credentials if provided credentials were not enough
|
||||
else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS) {
|
||||
$rootScope.$broadcast('guacInsufficientCredentials', parameters, error);
|
||||
clearAuthenticationResult();
|
||||
}
|
||||
|
||||
// Abort rendering of page if an internal error occurs
|
||||
else if (error.type === Error.Type.INTERNAL_ERROR)
|
||||
$rootScope.$broadcast('guacFatalPageError', error);
|
||||
|
||||
// Authentication failed
|
||||
throw error;
|
||||
|
||||
}));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a request to update the current auth token, if any, using the
|
||||
* token REST API endpoint. If the optional parameters object is provided,
|
||||
* its properties will be included as parameters in the update request.
|
||||
* This function returns a promise that succeeds only if the authentication
|
||||
* operation was successful. The resulting authentication data can be
|
||||
* retrieved later via getCurrentToken() or getCurrentUsername().
|
||||
*
|
||||
* If there is no current auth token, this function behaves identically to
|
||||
* authenticate(), and makes a general authentication request.
|
||||
*
|
||||
* @param {Object} [parameters]
|
||||
* Arbitrary parameters to authenticate with, if any.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise which succeeds only if the login operation was successful.
|
||||
*/
|
||||
service.updateCurrentToken = function updateCurrentToken(parameters) {
|
||||
|
||||
// HTTP parameters for the authentication request
|
||||
var httpParameters = {};
|
||||
|
||||
// Add token parameter if current token is known
|
||||
var token = service.getCurrentToken();
|
||||
if (token)
|
||||
httpParameters.token = service.getCurrentToken();
|
||||
|
||||
// Add any additional parameters
|
||||
if (parameters)
|
||||
angular.extend(httpParameters, parameters);
|
||||
|
||||
// Make the request
|
||||
return service.authenticate(httpParameters);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether the session associated with a particular token is
|
||||
* still valid, without performing an operation that would result in that
|
||||
* session being marked as active. If no token is provided, the session of
|
||||
* the current user is checked.
|
||||
*
|
||||
* @param {string} [token]
|
||||
* The authentication token to pass with the "Guacamole-Token" header.
|
||||
* If omitted, and the user is logged in, the user's current
|
||||
* authentication token will be used.
|
||||
*
|
||||
* @returns {Promise.<!boolean>}
|
||||
* A promise that resolves with the boolean value "true" if the session
|
||||
* is valid, and resolves with the boolean value "false" otherwise,
|
||||
* including if an error prevents session validity from being
|
||||
* determined. The promise is never rejected.
|
||||
*/
|
||||
service.getValidity = function getValidity(token) {
|
||||
|
||||
// NOTE: Because this is a HEAD request, we will not receive a JSON
|
||||
// response body. We will only have a simple yes/no regarding whether
|
||||
// the auth token can be expected to be usable.
|
||||
return service.request({
|
||||
method: 'HEAD',
|
||||
url: 'api/session'
|
||||
}, token)
|
||||
|
||||
.then(function sessionIsValid() {
|
||||
return true;
|
||||
})
|
||||
|
||||
['catch'](function sessionIsNotValid() {
|
||||
return false;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a request to revoke an authentication token using the token REST
|
||||
* API endpoint, returning a promise that succeeds only if the token was
|
||||
* successfully revoked.
|
||||
*
|
||||
* @param {string} token
|
||||
* The authentication token to revoke.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise which succeeds only if the token was successfully revoked.
|
||||
*/
|
||||
service.revokeToken = function revokeToken(token) {
|
||||
return service.request({
|
||||
method: 'DELETE',
|
||||
url: 'api/session'
|
||||
}, token);
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a request to authenticate a user using the token REST API endpoint
|
||||
* with a username and password, ignoring any currently-stored token,
|
||||
* returning a promise that succeeds only if the login operation was
|
||||
* successful. The resulting authentication data can be retrieved later
|
||||
* via getCurrentToken() or getCurrentUsername(). Invoking this function
|
||||
* will affect the UI, including the login screen if visible.
|
||||
*
|
||||
* @param {String} username
|
||||
* The username to log in with.
|
||||
*
|
||||
* @param {String} password
|
||||
* The password to log in with.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise which succeeds only if the login operation was successful.
|
||||
*/
|
||||
service.login = function login(username, password) {
|
||||
return service.authenticate({
|
||||
username: username,
|
||||
password: password
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a request to logout a user using the token REST API endpoint,
|
||||
* returning a promise that succeeds only if the logout operation was
|
||||
* successful. Invoking this function will affect the UI, causing the
|
||||
* visible components of the application to be replaced with a status
|
||||
* message noting that the user has been logged out.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* A promise which succeeds only if the logout operation was
|
||||
* successful.
|
||||
*/
|
||||
service.logout = function logout() {
|
||||
|
||||
// Clear authentication data
|
||||
var token = service.getCurrentToken();
|
||||
clearAuthenticationResult();
|
||||
|
||||
// Notify listeners that a token is being destroyed
|
||||
$rootScope.$broadcast('guacLogout', token);
|
||||
|
||||
// Delete old token
|
||||
return service.revokeToken(token);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the current user has authenticated anonymously. An
|
||||
* anonymous user is denoted by the identifier reserved by the Guacamole
|
||||
* extension API for anonymous users (the empty string).
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if the current user has authenticated anonymously, false
|
||||
* otherwise.
|
||||
*/
|
||||
service.isAnonymous = function isAnonymous() {
|
||||
return service.getCurrentUsername() === '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the username of the current user. If the current user is not
|
||||
* logged in, this value may not be valid.
|
||||
*
|
||||
* @returns {String}
|
||||
* The username of the current user, or null if no authentication data
|
||||
* is present.
|
||||
*/
|
||||
service.getCurrentUsername = function getCurrentUsername() {
|
||||
|
||||
// Return username, if available
|
||||
var authData = getAuthenticationResult();
|
||||
if (authData)
|
||||
return authData.username;
|
||||
|
||||
// No auth data present
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the auth token associated with the current user. If the current
|
||||
* user is not logged in, this token may not be valid.
|
||||
*
|
||||
* @returns {String}
|
||||
* The auth token associated with the current user, or null if no
|
||||
* authentication data is present.
|
||||
*/
|
||||
service.getCurrentToken = function getCurrentToken() {
|
||||
|
||||
// Return cached auth token, if available
|
||||
var authData = getAuthenticationResult();
|
||||
if (authData)
|
||||
return authData.authToken;
|
||||
|
||||
// Fall back to the value from local storage if not found in cache
|
||||
return localStorageService.getItem(AUTH_TOKEN_STORAGE_KEY);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the identifier of the data source that authenticated the current
|
||||
* user. If the current user is not logged in, this value may not be valid.
|
||||
*
|
||||
* @returns {String}
|
||||
* The identifier of the data source that authenticated the current
|
||||
* user, or null if no authentication data is present.
|
||||
*/
|
||||
service.getDataSource = function getDataSource() {
|
||||
|
||||
// Return data source, if available
|
||||
var authData = getAuthenticationResult();
|
||||
if (authData)
|
||||
return authData.dataSource;
|
||||
|
||||
// No auth data present
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the identifiers of all data sources available to the current
|
||||
* user. If the current user is not logged in, this value may not be valid.
|
||||
*
|
||||
* @returns {String[]}
|
||||
* The identifiers of all data sources availble to the current user,
|
||||
* or an empty array if no authentication data is present.
|
||||
*/
|
||||
service.getAvailableDataSources = function getAvailableDataSources() {
|
||||
|
||||
// Return data sources, if available
|
||||
var authData = getAuthenticationResult();
|
||||
if (authData)
|
||||
return authData.availableDataSources;
|
||||
|
||||
// No auth data present
|
||||
return [];
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes an HTTP request leveraging the requestService(), automatically
|
||||
* including the given authentication token using the "Guacamole-Token"
|
||||
* header. If no token is provided, the user's current authentication token
|
||||
* is used instead. If the user is not logged in, the "Guacamole-Token"
|
||||
* header is simply omitted. The provided configuration object is not
|
||||
* modified by this function.
|
||||
*
|
||||
* @param {Object} object
|
||||
* A configuration object describing the HTTP request to be made by
|
||||
* requestService(). As described by requestService(), this object must
|
||||
* be a configuration object accepted by AngularJS' $http service.
|
||||
*
|
||||
* @param {string} [token]
|
||||
* The authentication token to pass with the "Guacamole-Token" header.
|
||||
* If omitted, and the user is logged in, the user's current
|
||||
* authentication token will be used.
|
||||
*
|
||||
* @returns {Promise.<Object>}
|
||||
* A promise that will resolve with the data from the HTTP response for
|
||||
* the underlying requestService() call if successful, or reject with
|
||||
* an @link{Error} describing the failure.
|
||||
*/
|
||||
service.request = function request(object, token) {
|
||||
|
||||
// Attempt to use current token if none is provided
|
||||
token = token || service.getCurrentToken();
|
||||
|
||||
// Add "Guacamole-Token" header if an authentication token is available
|
||||
if (token) {
|
||||
object = _.merge({
|
||||
headers : { 'Guacamole-Token' : token }
|
||||
}, object);
|
||||
}
|
||||
|
||||
return requestService(object);
|
||||
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service which defines the AuthenticationResult class.
|
||||
*/
|
||||
angular.module('auth').factory('AuthenticationResult', [function defineAuthenticationResult() {
|
||||
|
||||
/**
|
||||
* The object returned by REST API calls when representing the successful
|
||||
* result of an authentication attempt.
|
||||
*
|
||||
* @constructor
|
||||
* @param {AuthenticationResult|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* AuthenticationResult.
|
||||
*/
|
||||
var AuthenticationResult = function AuthenticationResult(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The unique token generated for the user that authenticated.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.authToken = template.authToken;
|
||||
|
||||
/**
|
||||
* The name which uniquely identifies the user that authenticated.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.username = template.username;
|
||||
|
||||
/**
|
||||
* The unique identifier of the data source which authenticated the
|
||||
* user.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.dataSource = template.dataSource;
|
||||
|
||||
/**
|
||||
* The identifiers of all data sources available to the user that
|
||||
* authenticated.
|
||||
*
|
||||
* @type String[]
|
||||
*/
|
||||
this.availableDataSources = template.availableDataSources;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The username reserved by the Guacamole extension API for users which have
|
||||
* authenticated anonymously.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
AuthenticationResult.ANONYMOUS_USERNAME = '';
|
||||
|
||||
return AuthenticationResult;
|
||||
|
||||
}]);
|
||||
34
guacamole/src/main/frontend/src/app/client/clientModule.js
Normal file
34
guacamole/src/main/frontend/src/app/client/clientModule.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The module for code used to connect to a connection or balancing group.
|
||||
*/
|
||||
angular.module('client', [
|
||||
'auth',
|
||||
'clipboard',
|
||||
'element',
|
||||
'history',
|
||||
'navigation',
|
||||
'notification',
|
||||
'osk',
|
||||
'rest',
|
||||
'textInput',
|
||||
'touch'
|
||||
]);
|
||||
@@ -0,0 +1,879 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The controller for the page used to connect to a connection or balancing group.
|
||||
*/
|
||||
angular.module('client').controller('clientController', ['$scope', '$routeParams', '$injector',
|
||||
function clientController($scope, $routeParams, $injector) {
|
||||
|
||||
// Required types
|
||||
const ConnectionGroup = $injector.get('ConnectionGroup');
|
||||
const ManagedClient = $injector.get('ManagedClient');
|
||||
const ManagedClientGroup = $injector.get('ManagedClientGroup');
|
||||
const ManagedClientState = $injector.get('ManagedClientState');
|
||||
const ManagedFilesystem = $injector.get('ManagedFilesystem');
|
||||
const Protocol = $injector.get('Protocol');
|
||||
const ScrollState = $injector.get('ScrollState');
|
||||
|
||||
// Required services
|
||||
const $location = $injector.get('$location');
|
||||
const authenticationService = $injector.get('authenticationService');
|
||||
const connectionGroupService = $injector.get('connectionGroupService');
|
||||
const clipboardService = $injector.get('clipboardService');
|
||||
const dataSourceService = $injector.get('dataSourceService');
|
||||
const guacClientManager = $injector.get('guacClientManager');
|
||||
const guacFullscreen = $injector.get('guacFullscreen');
|
||||
const iconService = $injector.get('iconService');
|
||||
const preferenceService = $injector.get('preferenceService');
|
||||
const requestService = $injector.get('requestService');
|
||||
const tunnelService = $injector.get('tunnelService');
|
||||
const userPageService = $injector.get('userPageService');
|
||||
|
||||
/**
|
||||
* The minimum number of pixels a drag gesture must move to result in the
|
||||
* menu being shown or hidden.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var MENU_DRAG_DELTA = 64;
|
||||
|
||||
/**
|
||||
* The maximum X location of the start of a drag gesture for that gesture
|
||||
* to potentially show the menu.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var MENU_DRAG_MARGIN = 64;
|
||||
|
||||
/**
|
||||
* When showing or hiding the menu via a drag gesture, the maximum number
|
||||
* of pixels the touch can move vertically and still affect the menu.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var MENU_DRAG_VERTICAL_TOLERANCE = 10;
|
||||
|
||||
/**
|
||||
* In order to open the guacamole menu, we need to hit ctrl-alt-shift. There are
|
||||
* several possible keysysms for each key.
|
||||
*/
|
||||
var SHIFT_KEYS = {0xFFE1 : true, 0xFFE2 : true},
|
||||
ALT_KEYS = {0xFFE9 : true, 0xFFEA : true, 0xFE03 : true,
|
||||
0xFFE7 : true, 0xFFE8 : true},
|
||||
CTRL_KEYS = {0xFFE3 : true, 0xFFE4 : true},
|
||||
MENU_KEYS = angular.extend({}, SHIFT_KEYS, ALT_KEYS, CTRL_KEYS);
|
||||
|
||||
/**
|
||||
* Keysym for detecting any END key presses, for the purpose of passing through
|
||||
* the Ctrl-Alt-Del sequence to a remote system.
|
||||
*/
|
||||
var END_KEYS = {0xFF57 : true, 0xFFB1 : true};
|
||||
|
||||
/**
|
||||
* Keysym for sending the DELETE key when the Ctrl-Alt-End hotkey
|
||||
* combo is pressed.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var DEL_KEY = 0xFFFF;
|
||||
|
||||
/**
|
||||
* Menu-specific properties.
|
||||
*/
|
||||
$scope.menu = {
|
||||
|
||||
/**
|
||||
* Whether the menu is currently shown.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
shown : false,
|
||||
|
||||
/**
|
||||
* The currently selected input method. This may be any of the values
|
||||
* defined within preferenceService.inputMethods.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
inputMethod : preferenceService.preferences.inputMethod,
|
||||
|
||||
/**
|
||||
* Whether translation of touch to mouse events should emulate an
|
||||
* absolute pointer device, or a relative pointer device.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
emulateAbsoluteMouse : preferenceService.preferences.emulateAbsoluteMouse,
|
||||
|
||||
/**
|
||||
* The current scroll state of the menu.
|
||||
*
|
||||
* @type ScrollState
|
||||
*/
|
||||
scrollState : new ScrollState(),
|
||||
|
||||
/**
|
||||
* The current desired values of all editable connection parameters as
|
||||
* a set of name/value pairs, including any changes made by the user.
|
||||
*
|
||||
* @type {Object.<String, String>}
|
||||
*/
|
||||
connectionParameters : {}
|
||||
|
||||
};
|
||||
|
||||
// Convenience method for closing the menu
|
||||
$scope.closeMenu = function closeMenu() {
|
||||
$scope.menu.shown = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies any changes to connection parameters made by the user within the
|
||||
* Guacamole menu to the given ManagedClient. If no client is supplied,
|
||||
* this function has no effect.
|
||||
*
|
||||
* @param {ManagedClient} client
|
||||
* The client to apply parameter changes to.
|
||||
*/
|
||||
$scope.applyParameterChanges = function applyParameterChanges(client) {
|
||||
angular.forEach($scope.menu.connectionParameters, function sendArgv(value, name) {
|
||||
if (client)
|
||||
ManagedClient.setArgument(client, name, value);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* The currently-focused client within the current ManagedClientGroup. If
|
||||
* there is no current group, no client is focused, or multiple clients are
|
||||
* focused, this will be null.
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
$scope.focusedClient = null;
|
||||
|
||||
/**
|
||||
* The set of clients that should be attached to the client UI. This will
|
||||
* be immediately initialized by a call to updateAttachedClients() below.
|
||||
*
|
||||
* @type ManagedClientGroup
|
||||
*/
|
||||
$scope.clientGroup = null;
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.getName
|
||||
*/
|
||||
$scope.getName = ManagedClientGroup.getName;
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.getTitle
|
||||
*/
|
||||
$scope.getTitle = ManagedClientGroup.getTitle;
|
||||
|
||||
/**
|
||||
* Arbitrary context that should be exposed to the guacGroupList directive
|
||||
* displaying the dropdown list of available connections within the
|
||||
* Guacamole menu.
|
||||
*/
|
||||
$scope.connectionListContext = {
|
||||
|
||||
/**
|
||||
* The set of clients desired within the current view. For each client
|
||||
* that should be present within the current view, that client's ID
|
||||
* will map to "true" here.
|
||||
*
|
||||
* @type {Object.<string, boolean>}
|
||||
*/
|
||||
attachedClients : {},
|
||||
|
||||
/**
|
||||
* Notifies that the client with the given ID has been added or
|
||||
* removed from the set of clients desired within the current view,
|
||||
* and the current view should be updated accordingly.
|
||||
*
|
||||
* @param {string} id
|
||||
* The ID of the client that was added or removed from the current
|
||||
* view.
|
||||
*/
|
||||
updateAttachedClients : function updateAttachedClients(id) {
|
||||
$scope.addRemoveClient(id, !$scope.connectionListContext.attachedClients[id]);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds or removes the client with the given ID from the set of clients
|
||||
* within the current view, updating the current URL accordingly.
|
||||
*
|
||||
* @param {string} id
|
||||
* The ID of the client to add or remove from the current view.
|
||||
*
|
||||
* @param {boolean} [remove=false]
|
||||
* Whether the specified client should be added (false) or removed
|
||||
* (true).
|
||||
*/
|
||||
$scope.addRemoveClient = function addRemoveClient(id, remove) {
|
||||
|
||||
// Deconstruct current path into corresponding client IDs
|
||||
const ids = ManagedClientGroup.getClientIdentifiers($routeParams.id);
|
||||
|
||||
// Add/remove ID as requested
|
||||
if (remove)
|
||||
_.pull(ids, id);
|
||||
else
|
||||
ids.push(id);
|
||||
|
||||
// Reconstruct path, updating attached clients via change in route
|
||||
$location.path('/client/' + ManagedClientGroup.getIdentifier(ids));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Reloads the contents of $scope.clientGroup to reflect the client IDs
|
||||
* currently listed in the URL.
|
||||
*/
|
||||
const reparseRoute = function reparseRoute() {
|
||||
|
||||
const previousClients = $scope.clientGroup ? $scope.clientGroup.clients.slice() : [];
|
||||
|
||||
// Replace existing group with new group
|
||||
setAttachedGroup(guacClientManager.getManagedClientGroup($routeParams.id));
|
||||
|
||||
// Store current set of attached clients for later use within the
|
||||
// Guacamole menu
|
||||
$scope.connectionListContext.attachedClients = {};
|
||||
$scope.clientGroup.clients.forEach((client) => {
|
||||
$scope.connectionListContext.attachedClients[client.id] = true;
|
||||
});
|
||||
|
||||
// Ensure menu is closed if updated view is not a modification of the
|
||||
// current view (has no clients in common). The menu should remain open
|
||||
// only while the current view is being modified, not when navigating
|
||||
// to an entirely different view.
|
||||
if (_.isEmpty(_.intersection(previousClients, $scope.clientGroup.clients)))
|
||||
$scope.menu.shown = false;
|
||||
|
||||
// Update newly-attached clients with current contents of clipboard
|
||||
clipboardService.resyncClipboard();
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces the ManagedClientGroup currently attached to the client
|
||||
* interface via $scope.clientGroup with the given ManagedClientGroup,
|
||||
* safely cleaning up after the previous group. If no ManagedClientGroup is
|
||||
* provided, the existing group is simply removed.
|
||||
*
|
||||
* @param {ManagedClientGroup} [managedClientGroup]
|
||||
* The ManagedClientGroup to attach to the interface, if any.
|
||||
*/
|
||||
const setAttachedGroup = function setAttachedGroup(managedClientGroup) {
|
||||
|
||||
// Do nothing if group is not actually changing
|
||||
if ($scope.clientGroup === managedClientGroup)
|
||||
return;
|
||||
|
||||
if ($scope.clientGroup) {
|
||||
|
||||
// Remove all disconnected clients from management (the user has
|
||||
// seen their status)
|
||||
_.filter($scope.clientGroup.clients, client => {
|
||||
|
||||
const connectionState = client.clientState.connectionState;
|
||||
return connectionState === ManagedClientState.ConnectionState.DISCONNECTED
|
||||
|| connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR
|
||||
|| connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR;
|
||||
|
||||
}).forEach(client => {
|
||||
guacClientManager.removeManagedClient(client.id);
|
||||
});
|
||||
|
||||
// Flag group as detached
|
||||
$scope.clientGroup.attached = false;
|
||||
|
||||
}
|
||||
|
||||
if (managedClientGroup) {
|
||||
$scope.clientGroup = managedClientGroup;
|
||||
$scope.clientGroup.attached = true;
|
||||
$scope.clientGroup.lastUsed = new Date().getTime();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Init sets of clients based on current URL ...
|
||||
reparseRoute();
|
||||
|
||||
// ... and re-initialize those sets if the URL has changed without
|
||||
// reloading the route
|
||||
$scope.$on('$routeUpdate', reparseRoute);
|
||||
|
||||
/**
|
||||
* The root connection groups of the connection hierarchy that should be
|
||||
* presented to the user for selecting a different connection, as a map of
|
||||
* data source identifier to the root connection group of that data
|
||||
* source. This will be null if the connection group hierarchy has not yet
|
||||
* been loaded or if the hierarchy is inapplicable due to only one
|
||||
* connection or balancing group being available.
|
||||
*
|
||||
* @type Object.<String, ConnectionGroup>
|
||||
*/
|
||||
$scope.rootConnectionGroups = null;
|
||||
|
||||
/**
|
||||
* Array of all connection properties that are filterable.
|
||||
*
|
||||
* @type String[]
|
||||
*/
|
||||
$scope.filteredConnectionProperties = [
|
||||
'name'
|
||||
];
|
||||
|
||||
/**
|
||||
* Array of all connection group properties that are filterable.
|
||||
*
|
||||
* @type String[]
|
||||
*/
|
||||
$scope.filteredConnectionGroupProperties = [
|
||||
'name'
|
||||
];
|
||||
|
||||
// Retrieve root groups and all descendants
|
||||
dataSourceService.apply(
|
||||
connectionGroupService.getConnectionGroupTree,
|
||||
authenticationService.getAvailableDataSources(),
|
||||
ConnectionGroup.ROOT_IDENTIFIER
|
||||
)
|
||||
.then(function rootGroupsRetrieved(rootConnectionGroups) {
|
||||
|
||||
// Store retrieved groups only if there are multiple connections or
|
||||
// balancing groups available
|
||||
var clientPages = userPageService.getClientPages(rootConnectionGroups);
|
||||
if (clientPages.length > 1)
|
||||
$scope.rootConnectionGroups = rootConnectionGroups;
|
||||
|
||||
}, requestService.WARN);
|
||||
|
||||
/**
|
||||
* Map of all available sharing profiles for the current connection by
|
||||
* their identifiers. If this information is not yet available, or no such
|
||||
* sharing profiles exist, this will be an empty object.
|
||||
*
|
||||
* @type Object.<String, SharingProfile>
|
||||
*/
|
||||
$scope.sharingProfiles = {};
|
||||
|
||||
/**
|
||||
* Map of all substituted key presses. If one key is pressed in place of another
|
||||
* the value of the substituted key is stored in an object with the keysym of
|
||||
* the original key.
|
||||
*
|
||||
* @type Object.<Number, Number>
|
||||
*/
|
||||
var substituteKeysPressed = {};
|
||||
|
||||
/**
|
||||
* Returns whether the shortcut for showing/hiding the Guacamole menu
|
||||
* (Ctrl+Alt+Shift) has been pressed.
|
||||
*
|
||||
* @param {Guacamole.Keyboard} keyboard
|
||||
* The Guacamole.Keyboard object tracking the local keyboard state.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true if Ctrl+Alt+Shift has been pressed, false otherwise.
|
||||
*/
|
||||
const isMenuShortcutPressed = function isMenuShortcutPressed(keyboard) {
|
||||
|
||||
// Ctrl+Alt+Shift has NOT been pressed if any key is currently held
|
||||
// down that isn't Ctrl, Alt, or Shift
|
||||
if (_.findKey(keyboard.pressed, (val, keysym) => !MENU_KEYS[keysym]))
|
||||
return false;
|
||||
|
||||
// Verify that one of each required key is held, regardless of
|
||||
// left/right location on the keyboard
|
||||
return !!(
|
||||
_.findKey(SHIFT_KEYS, (val, keysym) => keyboard.pressed[keysym])
|
||||
&& _.findKey(ALT_KEYS, (val, keysym) => keyboard.pressed[keysym])
|
||||
&& _.findKey(CTRL_KEYS, (val, keysym) => keyboard.pressed[keysym])
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
// Show menu if the user swipes from the left, hide menu when the user
|
||||
// swipes from the right, scroll menu while visible
|
||||
$scope.menuDrag = function menuDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) {
|
||||
|
||||
if ($scope.menu.shown) {
|
||||
|
||||
// Hide menu if swipe-from-right gesture is detected
|
||||
if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE
|
||||
&& startX - currentX >= MENU_DRAG_DELTA)
|
||||
$scope.menu.shown = false;
|
||||
|
||||
// Scroll menu by default
|
||||
else {
|
||||
$scope.menu.scrollState.left -= deltaX;
|
||||
$scope.menu.scrollState.top -= deltaY;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Show menu if swipe-from-left gesture is detected
|
||||
else if (startX <= MENU_DRAG_MARGIN) {
|
||||
if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE
|
||||
&& currentX - startX >= MENU_DRAG_DELTA)
|
||||
$scope.menu.shown = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
};
|
||||
|
||||
// Show/hide UI elements depending on input method
|
||||
$scope.$watch('menu.inputMethod', function setInputMethod(inputMethod) {
|
||||
|
||||
// Show input methods only if selected
|
||||
$scope.showOSK = (inputMethod === 'osk');
|
||||
$scope.showTextInput = (inputMethod === 'text');
|
||||
|
||||
});
|
||||
|
||||
// Update client state/behavior as visibility of the Guacamole menu changes
|
||||
$scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) {
|
||||
|
||||
// Re-update available connection parameters, if there is a focused
|
||||
// client (parameter information may not have been available at the
|
||||
// time focus changed)
|
||||
if (menuShown)
|
||||
$scope.menu.connectionParameters = $scope.focusedClient ?
|
||||
ManagedClient.getArgumentModel($scope.focusedClient) : {};
|
||||
|
||||
// Send any argument value data once menu is hidden
|
||||
else if (menuShownPreviousState)
|
||||
$scope.applyParameterChanges($scope.focusedClient);
|
||||
|
||||
/* Broadcast changes to the menu display state */
|
||||
$scope.$broadcast('guacMenuShown', menuShown);
|
||||
|
||||
});
|
||||
|
||||
// Toggle the menu when the guacClientToggleMenu event is received
|
||||
$scope.$on('guacToggleMenu',
|
||||
() => $scope.menu.shown = !$scope.menu.shown);
|
||||
|
||||
// Show the menu when the guacClientShowMenu event is received
|
||||
$scope.$on('guacShowMenu', () => $scope.menu.shown = true);
|
||||
|
||||
// Hide the menu when the guacClientHideMenu event is received
|
||||
$scope.$on('guacHideMenu', () => $scope.menu.shown = false);
|
||||
|
||||
// Automatically track and cache the currently-focused client
|
||||
$scope.$on('guacClientFocused', function focusedClientChanged(event, newFocusedClient) {
|
||||
|
||||
const oldFocusedClient = $scope.focusedClient;
|
||||
$scope.focusedClient = newFocusedClient;
|
||||
|
||||
// Apply any parameter changes when focus is changing
|
||||
if (oldFocusedClient)
|
||||
$scope.applyParameterChanges(oldFocusedClient);
|
||||
|
||||
// Update available connection parameters, if there is a focused
|
||||
// client
|
||||
$scope.menu.connectionParameters = newFocusedClient ?
|
||||
ManagedClient.getArgumentModel(newFocusedClient) : {};
|
||||
|
||||
});
|
||||
|
||||
// Automatically update connection parameters that have been modified
|
||||
// for the current focused client
|
||||
$scope.$on('guacClientArgumentsUpdated', function argumentsChanged(event, focusedClient) {
|
||||
|
||||
// Ignore any updated arguments not for the current focused client
|
||||
if ($scope.focusedClient && $scope.focusedClient === focusedClient)
|
||||
$scope.menu.connectionParameters = ManagedClient.getArgumentModel(focusedClient);
|
||||
|
||||
});
|
||||
|
||||
// Update page icon when thumbnail changes
|
||||
$scope.$watch('focusedClient.thumbnail.canvas', function thumbnailChanged(canvas) {
|
||||
iconService.setIcons(canvas);
|
||||
});
|
||||
|
||||
// Pull sharing profiles once the tunnel UUID is known
|
||||
$scope.$watch('focusedClient.tunnel.uuid', function retrieveSharingProfiles(uuid) {
|
||||
|
||||
// Only pull sharing profiles if tunnel UUID is actually available
|
||||
if (!uuid) {
|
||||
$scope.sharingProfiles = {};
|
||||
return;
|
||||
}
|
||||
|
||||
// Pull sharing profiles for the current connection
|
||||
tunnelService.getSharingProfiles(uuid)
|
||||
.then(function sharingProfilesRetrieved(sharingProfiles) {
|
||||
$scope.sharingProfiles = sharingProfiles;
|
||||
}, requestService.WARN);
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Produces a sharing link for the current connection using the given
|
||||
* sharing profile. The resulting sharing link, and any required login
|
||||
* information, will be displayed to the user within the Guacamole menu.
|
||||
*
|
||||
* @param {SharingProfile} sharingProfile
|
||||
* The sharing profile to use to generate the sharing link.
|
||||
*/
|
||||
$scope.share = function share(sharingProfile) {
|
||||
if ($scope.focusedClient)
|
||||
ManagedClient.createShareLink($scope.focusedClient, sharingProfile);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the current connection has any associated share links.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if the current connection has at least one associated share
|
||||
* link, false otherwise.
|
||||
*/
|
||||
$scope.isShared = function isShared() {
|
||||
return !!$scope.focusedClient && ManagedClient.isShared($scope.focusedClient);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the total number of share links associated with the current
|
||||
* connection.
|
||||
*
|
||||
* @returns {Number}
|
||||
* The total number of share links associated with the current
|
||||
* connection.
|
||||
*/
|
||||
$scope.getShareLinkCount = function getShareLinkCount() {
|
||||
|
||||
if (!$scope.focusedClient)
|
||||
return 0;
|
||||
|
||||
// Count total number of links within the ManagedClient's share link map
|
||||
var linkCount = 0;
|
||||
for (const dummy in $scope.focusedClient.shareLinks)
|
||||
linkCount++;
|
||||
|
||||
return linkCount;
|
||||
|
||||
};
|
||||
|
||||
// Opening the Guacamole menu after Ctrl+Alt+Shift, preventing those
|
||||
// keypresses from reaching any Guacamole client
|
||||
$scope.$on('guacBeforeKeydown', function incomingKeydown(event, keysym, keyboard) {
|
||||
|
||||
// Toggle menu if menu shortcut (Ctrl+Alt+Shift) is pressed
|
||||
if (isMenuShortcutPressed(keyboard)) {
|
||||
|
||||
// Don't send this key event through to the client, and release
|
||||
// all other keys involved in performing this shortcut
|
||||
event.preventDefault();
|
||||
keyboard.reset();
|
||||
|
||||
// Toggle the menu
|
||||
$scope.$apply(function() {
|
||||
$scope.menu.shown = !$scope.menu.shown;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Prevent all keydown events while menu is open
|
||||
else if ($scope.menu.shown)
|
||||
event.preventDefault();
|
||||
|
||||
});
|
||||
|
||||
// Prevent all keyup events while menu is open
|
||||
$scope.$on('guacBeforeKeyup', function incomingKeyup(event, keysym, keyboard) {
|
||||
if ($scope.menu.shown)
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Send Ctrl-Alt-Delete when Ctrl-Alt-End is pressed.
|
||||
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
|
||||
|
||||
// If one of the End keys is pressed, and we have a one keysym from each
|
||||
// of Ctrl and Alt groups, send Ctrl-Alt-Delete.
|
||||
if (END_KEYS[keysym]
|
||||
&& _.findKey(ALT_KEYS, (val, keysym) => keyboard.pressed[keysym])
|
||||
&& _.findKey(CTRL_KEYS, (val, keysym) => keyboard.pressed[keysym])
|
||||
) {
|
||||
|
||||
// Don't send this event through to the client.
|
||||
event.preventDefault();
|
||||
|
||||
// Record the substituted key press so that it can be
|
||||
// properly dealt with later.
|
||||
substituteKeysPressed[keysym] = DEL_KEY;
|
||||
|
||||
// Send through the delete key.
|
||||
$scope.$broadcast('guacSyntheticKeydown', DEL_KEY);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Update pressed keys as they are released
|
||||
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
|
||||
|
||||
// Deal with substitute key presses
|
||||
if (substituteKeysPressed[keysym]) {
|
||||
event.preventDefault();
|
||||
$scope.$broadcast('guacSyntheticKeyup', substituteKeysPressed[keysym]);
|
||||
delete substituteKeysPressed[keysym];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Update page title when client title changes
|
||||
$scope.$watch('getTitle(clientGroup)', function clientTitleChanged(title) {
|
||||
$scope.page.title = title;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns whether the current connection has been flagged as unstable due
|
||||
* to an apparent network disruption.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if the current connection has been flagged as unstable, false
|
||||
* otherwise.
|
||||
*/
|
||||
$scope.isConnectionUnstable = function isConnectionUnstable() {
|
||||
return _.findIndex($scope.clientGroup.clients, client => client.clientState.tunnelUnstable) !== -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Immediately disconnects all currently-focused clients, if any.
|
||||
*/
|
||||
$scope.disconnect = function disconnect() {
|
||||
|
||||
// Disconnect if client is available
|
||||
if ($scope.clientGroup) {
|
||||
$scope.clientGroup.clients.forEach(client => {
|
||||
if (client.clientProperties.focused)
|
||||
client.client.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
// Hide menu
|
||||
$scope.menu.shown = false;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Disconnects the given ManagedClient, removing it from the current
|
||||
* view.
|
||||
*
|
||||
* @param {ManagedClient} client
|
||||
* The client to disconnect.
|
||||
*/
|
||||
$scope.closeClientTile = function closeClientTile(client) {
|
||||
|
||||
$scope.addRemoveClient(client.id, true);
|
||||
guacClientManager.removeManagedClient(client.id);
|
||||
|
||||
// Ensure at least one client has focus (the only client with
|
||||
// focus may just have been removed)
|
||||
ManagedClientGroup.verifyFocus($scope.clientGroup);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Action which immediately disconnects the currently-connected client, if
|
||||
* any.
|
||||
*/
|
||||
var DISCONNECT_MENU_ACTION = {
|
||||
name : 'CLIENT.ACTION_DISCONNECT',
|
||||
className : 'danger disconnect',
|
||||
callback : $scope.disconnect
|
||||
};
|
||||
|
||||
/**
|
||||
* Action that toggles fullscreen mode within the
|
||||
* currently-connected client and then closes the menu.
|
||||
*/
|
||||
var FULLSCREEN_MENU_ACTION = {
|
||||
name : 'CLIENT.ACTION_FULLSCREEN',
|
||||
classname : 'fullscreen action',
|
||||
callback : function fullscreen() {
|
||||
|
||||
guacFullscreen.toggleFullscreenMode();
|
||||
$scope.menu.shown = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Set client-specific menu actions
|
||||
$scope.clientMenuActions = [ DISCONNECT_MENU_ACTION,FULLSCREEN_MENU_ACTION ];
|
||||
|
||||
/**
|
||||
* @borrows Protocol.getNamespace
|
||||
*/
|
||||
$scope.getProtocolNamespace = Protocol.getNamespace;
|
||||
|
||||
/**
|
||||
* The currently-visible filesystem within the filesystem menu, if the
|
||||
* filesystem menu is open. If no filesystem is currently visible, this
|
||||
* will be null.
|
||||
*
|
||||
* @type ManagedFilesystem
|
||||
*/
|
||||
$scope.filesystemMenuContents = null;
|
||||
|
||||
/**
|
||||
* Hides the filesystem menu.
|
||||
*/
|
||||
$scope.hideFilesystemMenu = function hideFilesystemMenu() {
|
||||
$scope.filesystemMenuContents = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the filesystem menu, displaying the contents of the given
|
||||
* filesystem within it.
|
||||
*
|
||||
* @param {ManagedFilesystem} filesystem
|
||||
* The filesystem to show within the filesystem menu.
|
||||
*/
|
||||
$scope.showFilesystemMenu = function showFilesystemMenu(filesystem) {
|
||||
$scope.filesystemMenuContents = filesystem;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the filesystem menu should be visible.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if the filesystem menu is shown, false otherwise.
|
||||
*/
|
||||
$scope.isFilesystemMenuShown = function isFilesystemMenuShown() {
|
||||
return !!$scope.filesystemMenuContents && $scope.menu.shown;
|
||||
};
|
||||
|
||||
// Automatically refresh display when filesystem menu is shown
|
||||
$scope.$watch('isFilesystemMenuShown()', function refreshFilesystem() {
|
||||
|
||||
// Refresh filesystem, if defined
|
||||
var filesystem = $scope.filesystemMenuContents;
|
||||
if (filesystem)
|
||||
ManagedFilesystem.refresh(filesystem, filesystem.currentDirectory);
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the full path to the given file as an ordered array of parent
|
||||
* directories.
|
||||
*
|
||||
* @param {ManagedFilesystem.File} file
|
||||
* The file whose full path should be retrieved.
|
||||
*
|
||||
* @returns {ManagedFilesystem.File[]}
|
||||
* An array of directories which make up the hierarchy containing the
|
||||
* given file, in order of increasing depth.
|
||||
*/
|
||||
$scope.getPath = function getPath(file) {
|
||||
|
||||
var path = [];
|
||||
|
||||
// Add all files to path in ascending order of depth
|
||||
while (file && file.parent) {
|
||||
path.unshift(file);
|
||||
file = file.parent;
|
||||
}
|
||||
|
||||
return path;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the current directory of the given filesystem to the given
|
||||
* directory.
|
||||
*
|
||||
* @param {ManagedFilesystem} filesystem
|
||||
* The filesystem whose current directory should be changed.
|
||||
*
|
||||
* @param {ManagedFilesystem.File} file
|
||||
* The directory to change to.
|
||||
*/
|
||||
$scope.changeDirectory = function changeDirectory(filesystem, file) {
|
||||
ManagedFilesystem.changeDirectory(filesystem, file);
|
||||
};
|
||||
|
||||
/**
|
||||
* Begins a file upload through the attached Guacamole client for
|
||||
* each file in the given FileList.
|
||||
*
|
||||
* @param {FileList} files
|
||||
* The files to upload.
|
||||
*/
|
||||
$scope.uploadFiles = function uploadFiles(files) {
|
||||
|
||||
// Upload each file
|
||||
for (var i = 0; i < files.length; i++)
|
||||
ManagedClient.uploadFile($scope.filesystemMenuContents.client, files[i], $scope.filesystemMenuContents);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether the attached client group has any associated file
|
||||
* transfers, regardless of those file transfers' state.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if there are any file transfers associated with the
|
||||
* attached client group, false otherise.
|
||||
*/
|
||||
$scope.hasTransfers = function hasTransfers() {
|
||||
|
||||
// There are no file transfers if there is no client group
|
||||
if (!$scope.clientGroup)
|
||||
return false;
|
||||
|
||||
return _.findIndex($scope.clientGroup.clients, ManagedClient.hasTransfers) !== -1;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the current user can share the current connection with
|
||||
* other users. A connection can be shared if and only if there is at least
|
||||
* one associated sharing profile.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if the current user can share the current connection with other
|
||||
* users, false otherwise.
|
||||
*/
|
||||
$scope.canShareConnection = function canShareConnection() {
|
||||
|
||||
// If there is at least one sharing profile, the connection can be shared
|
||||
for (var dummy in $scope.sharingProfiles)
|
||||
return true;
|
||||
|
||||
// Otherwise, sharing is not possible
|
||||
return false;
|
||||
|
||||
};
|
||||
|
||||
// Clean up when view destroyed
|
||||
$scope.$on('$destroy', function clientViewDestroyed() {
|
||||
setAttachedGroup(null);
|
||||
|
||||
// always unset fullscreen mode to not confuse user
|
||||
guacFullscreen.setFullscreenMode(false);
|
||||
});
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,675 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive for the guacamole client.
|
||||
*/
|
||||
angular.module('client').directive('guacClient', [function guacClient() {
|
||||
|
||||
const directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: 'app/client/templates/guacClient.html'
|
||||
};
|
||||
|
||||
directive.scope = {
|
||||
|
||||
/**
|
||||
* The client to display within this guacClient directive.
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
client : '=',
|
||||
|
||||
/**
|
||||
* Whether translation of touch to mouse events should emulate an
|
||||
* absolute pointer device, or a relative pointer device.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
emulateAbsoluteMouse : '='
|
||||
|
||||
};
|
||||
|
||||
directive.controller = ['$scope', '$injector', '$element',
|
||||
function guacClientController($scope, $injector, $element) {
|
||||
|
||||
// Required types
|
||||
const ManagedClient = $injector.get('ManagedClient');
|
||||
|
||||
// Required services
|
||||
const $rootScope = $injector.get('$rootScope');
|
||||
const $window = $injector.get('$window');
|
||||
|
||||
/**
|
||||
* Whether the local, hardware mouse cursor is in use.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
let localCursor = false;
|
||||
|
||||
/**
|
||||
* The current Guacamole client instance.
|
||||
*
|
||||
* @type Guacamole.Client
|
||||
*/
|
||||
let client = null;
|
||||
|
||||
/**
|
||||
* The display of the current Guacamole client instance.
|
||||
*
|
||||
* @type Guacamole.Display
|
||||
*/
|
||||
let display = null;
|
||||
|
||||
/**
|
||||
* The element associated with the display of the current
|
||||
* Guacamole client instance.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
let displayElement = null;
|
||||
|
||||
/**
|
||||
* The element which must contain the Guacamole display element.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
const displayContainer = $element.find('.display')[0];
|
||||
|
||||
/**
|
||||
* The main containing element for the entire directive.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
const main = $element[0];
|
||||
|
||||
/**
|
||||
* Guacamole mouse event object, wrapped around the main client
|
||||
* display.
|
||||
*
|
||||
* @type Guacamole.Mouse
|
||||
*/
|
||||
const mouse = new Guacamole.Mouse(displayContainer);
|
||||
|
||||
/**
|
||||
* Guacamole absolute mouse emulation object, wrapped around the
|
||||
* main client display.
|
||||
*
|
||||
* @type Guacamole.Mouse.Touchscreen
|
||||
*/
|
||||
const touchScreen = new Guacamole.Mouse.Touchscreen(displayContainer);
|
||||
|
||||
/**
|
||||
* Guacamole relative mouse emulation object, wrapped around the
|
||||
* main client display.
|
||||
*
|
||||
* @type Guacamole.Mouse.Touchpad
|
||||
*/
|
||||
const touchPad = new Guacamole.Mouse.Touchpad(displayContainer);
|
||||
|
||||
/**
|
||||
* Guacamole touch event handling object, wrapped around the main
|
||||
* client dislay.
|
||||
*
|
||||
* @type Guacamole.Touch
|
||||
*/
|
||||
const touch = new Guacamole.Touch(displayContainer);
|
||||
|
||||
/**
|
||||
* Updates the scale of the attached Guacamole.Client based on current window
|
||||
* size and "auto-fit" setting.
|
||||
*/
|
||||
const updateDisplayScale = function updateDisplayScale() {
|
||||
|
||||
if (!display) return;
|
||||
|
||||
// Calculate scale to fit screen
|
||||
$scope.client.clientProperties.minScale = Math.min(
|
||||
main.offsetWidth / Math.max(display.getWidth(), 1),
|
||||
main.offsetHeight / Math.max(display.getHeight(), 1)
|
||||
);
|
||||
|
||||
// Calculate appropriate maximum zoom level
|
||||
$scope.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3);
|
||||
|
||||
// Clamp zoom level, maintain auto-fit
|
||||
if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit)
|
||||
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
|
||||
|
||||
else if (display.getScale() > $scope.client.clientProperties.maxScale)
|
||||
$scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Scrolls the client view such that the mouse cursor is visible.
|
||||
*
|
||||
* @param {Guacamole.Mouse.State} mouseState The current mouse
|
||||
* state.
|
||||
*/
|
||||
const scrollToMouse = function scrollToMouse(mouseState) {
|
||||
|
||||
// Determine mouse position within view
|
||||
const mouse_view_x = mouseState.x + displayContainer.offsetLeft - main.scrollLeft;
|
||||
const mouse_view_y = mouseState.y + displayContainer.offsetTop - main.scrollTop;
|
||||
|
||||
// Determine viewport dimensions
|
||||
const view_width = main.offsetWidth;
|
||||
const view_height = main.offsetHeight;
|
||||
|
||||
// Determine scroll amounts based on mouse position relative to document
|
||||
|
||||
let scroll_amount_x;
|
||||
if (mouse_view_x > view_width)
|
||||
scroll_amount_x = mouse_view_x - view_width;
|
||||
else if (mouse_view_x < 0)
|
||||
scroll_amount_x = mouse_view_x;
|
||||
else
|
||||
scroll_amount_x = 0;
|
||||
|
||||
let scroll_amount_y;
|
||||
if (mouse_view_y > view_height)
|
||||
scroll_amount_y = mouse_view_y - view_height;
|
||||
else if (mouse_view_y < 0)
|
||||
scroll_amount_y = mouse_view_y;
|
||||
else
|
||||
scroll_amount_y = 0;
|
||||
|
||||
// Scroll (if necessary) to keep mouse on screen.
|
||||
main.scrollLeft += scroll_amount_x;
|
||||
main.scrollTop += scroll_amount_y;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the name of the angular event associated with the provided
|
||||
* mouse event.
|
||||
*
|
||||
* @param {Guacamole.Mouse.MouseEvent} event
|
||||
* The mouse event to determine an angular event name for.
|
||||
*
|
||||
* @returns
|
||||
* The name of the angular event associated with the provided
|
||||
* mouse event.
|
||||
*/
|
||||
const getMouseEventName = event => {
|
||||
switch (event.type) {
|
||||
case 'mousedown':
|
||||
return 'guacClientMouseDown';
|
||||
case 'mouseup':
|
||||
return 'guacClientMouseUp';
|
||||
default:
|
||||
return 'guacClientMouseMove';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a mouse event originating from the user's actual mouse.
|
||||
* This differs from handleEmulatedMouseEvent() in that the
|
||||
* software mouse cursor must be shown only if the user's browser
|
||||
* does not support explicitly setting the hardware mouse cursor.
|
||||
*
|
||||
* @param {Guacamole.Mouse.MouseEvent} event
|
||||
* The mouse event to handle.
|
||||
*/
|
||||
const handleMouseEvent = function handleMouseEvent(event) {
|
||||
|
||||
// Do not attempt to handle mouse state changes if the client
|
||||
// or display are not yet available
|
||||
if (!client || !display)
|
||||
return;
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
// Send mouse state, show cursor if necessary
|
||||
display.showCursor(!localCursor);
|
||||
client.sendMouseState(event.state, true);
|
||||
|
||||
// Broadcast the mouse event
|
||||
$rootScope.$broadcast(getMouseEventName(event), event, client);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a mouse event originating from one of Guacamole's mouse
|
||||
* emulation objects. This differs from handleMouseState() in that
|
||||
* the software mouse cursor must always be shown (as the emulated
|
||||
* mouse device will not have its own cursor).
|
||||
*
|
||||
* @param {Guacamole.Mouse.MouseEvent} event
|
||||
* The mouse event to handle.
|
||||
*/
|
||||
const handleEmulatedMouseEvent = function handleEmulatedMouseEvent(event) {
|
||||
|
||||
// Do not attempt to handle mouse state changes if the client
|
||||
// or display are not yet available
|
||||
if (!client || !display)
|
||||
return;
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
// Ensure software cursor is shown
|
||||
display.showCursor(true);
|
||||
|
||||
// Send mouse state, ensure cursor is visible
|
||||
scrollToMouse(event.state);
|
||||
client.sendMouseState(event.state, true);
|
||||
|
||||
// Broadcast the mouse event
|
||||
$rootScope.$broadcast(getMouseEventName(event), event, client);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the name of the angular event associated with the provided
|
||||
* touch event.
|
||||
*
|
||||
* @param {Guacamole.Touch.TouchEvent} event
|
||||
* The touch event to determine an angular event name for.
|
||||
*
|
||||
* @returns
|
||||
* The name of the angular event associated with the provided
|
||||
* touch event.
|
||||
*/
|
||||
const getTouchEventName = event => {
|
||||
switch (event.type) {
|
||||
case 'touchstart':
|
||||
return 'guacClientTouchStart';
|
||||
case 'touchend':
|
||||
return 'guacClientTouchEnd';
|
||||
default:
|
||||
return 'guacClientTouchMove';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a touch event originating from the user's device.
|
||||
*
|
||||
* @param {Guacamole.Touch.Event} touchEvent
|
||||
* The touch event.
|
||||
*/
|
||||
const handleTouchEvent = function handleTouchEvent(event) {
|
||||
|
||||
// Do not attempt to handle touch state changes if the client
|
||||
// or display are not yet available
|
||||
if (!client || !display)
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// Send touch state, hiding local cursor
|
||||
display.showCursor(false);
|
||||
client.sendTouchState(event.state, true);
|
||||
|
||||
// Broadcast the touch event
|
||||
$rootScope.$broadcast(getTouchEventName(event), event, client);
|
||||
|
||||
};
|
||||
|
||||
// Attach any given managed client
|
||||
$scope.$watch('client', function attachManagedClient(managedClient) {
|
||||
|
||||
// Remove any existing display
|
||||
displayContainer.innerHTML = "";
|
||||
|
||||
// Only proceed if a client is given
|
||||
if (!managedClient)
|
||||
return;
|
||||
|
||||
// Get Guacamole client instance
|
||||
client = managedClient.client;
|
||||
|
||||
// Attach possibly new display
|
||||
display = client.getDisplay();
|
||||
display.scale($scope.client.clientProperties.scale);
|
||||
|
||||
// Add display element
|
||||
displayElement = display.getElement();
|
||||
displayContainer.appendChild(displayElement);
|
||||
|
||||
// Do nothing when the display element is clicked on
|
||||
display.getElement().onclick = function(e) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
};
|
||||
|
||||
// Connect and update interface to match required size, deferring
|
||||
// connecting until a future element resize if the main element
|
||||
// size (desired display size) is not known and thus can't be sent
|
||||
// during the handshake
|
||||
$scope.mainElementResized();
|
||||
|
||||
});
|
||||
|
||||
// Update actual view scrollLeft when scroll properties change
|
||||
$scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
|
||||
main.scrollLeft = scrollLeft;
|
||||
$scope.client.clientProperties.scrollLeft = main.scrollLeft;
|
||||
});
|
||||
|
||||
// Update actual view scrollTop when scroll properties change
|
||||
$scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
|
||||
main.scrollTop = scrollTop;
|
||||
$scope.client.clientProperties.scrollTop = main.scrollTop;
|
||||
});
|
||||
|
||||
// Update scale when display is resized
|
||||
$scope.$watch('client.managedDisplay.size', function setDisplaySize() {
|
||||
$scope.$evalAsync(updateDisplayScale);
|
||||
});
|
||||
|
||||
// Keep local cursor up-to-date
|
||||
$scope.$watch('client.managedDisplay.cursor', function setCursor(cursor) {
|
||||
if (cursor)
|
||||
localCursor = mouse.setCursor(cursor.canvas, cursor.x, cursor.y);
|
||||
});
|
||||
|
||||
// Update touch event handling depending on remote multi-touch
|
||||
// support and mouse emulation mode
|
||||
$scope.$watchGroup([
|
||||
'client.multiTouchSupport',
|
||||
'emulateAbsoluteMouse'
|
||||
], function touchBehaviorChanged() {
|
||||
|
||||
// Clear existing event handling
|
||||
touch.offEach(['touchstart', 'touchmove', 'touchend'], handleTouchEvent);
|
||||
touchScreen.offEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
|
||||
touchPad.offEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
|
||||
|
||||
// Directly forward local touch events
|
||||
if ($scope.client.multiTouchSupport)
|
||||
touch.onEach(['touchstart', 'touchmove', 'touchend'], handleTouchEvent);
|
||||
|
||||
// Switch to touchscreen if mouse emulation is required and
|
||||
// absolute mouse emulation is preferred
|
||||
else if ($scope.emulateAbsoluteMouse)
|
||||
touchScreen.onEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
|
||||
|
||||
// Use touchpad for mouse emulation if absolute mouse emulation
|
||||
// is not preferred
|
||||
else
|
||||
touchPad.onEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
|
||||
|
||||
});
|
||||
|
||||
// Adjust scale if modified externally
|
||||
$scope.$watch('client.clientProperties.scale', function changeScale(scale) {
|
||||
|
||||
// Fix scale within limits
|
||||
scale = Math.max(scale, $scope.client.clientProperties.minScale);
|
||||
scale = Math.min(scale, $scope.client.clientProperties.maxScale);
|
||||
|
||||
// If at minimum zoom level, hide scroll bars
|
||||
if (scale === $scope.client.clientProperties.minScale)
|
||||
main.style.overflow = "hidden";
|
||||
|
||||
// If not at minimum zoom level, show scroll bars
|
||||
else
|
||||
main.style.overflow = "auto";
|
||||
|
||||
// Apply scale if client attached
|
||||
if (display)
|
||||
display.scale(scale);
|
||||
|
||||
if (scale !== $scope.client.clientProperties.scale)
|
||||
$scope.client.clientProperties.scale = scale;
|
||||
|
||||
});
|
||||
|
||||
// If autofit is set, the scale should be set to the minimum scale, filling the screen
|
||||
$scope.$watch('client.clientProperties.autoFit', function changeAutoFit(autoFit) {
|
||||
if(autoFit)
|
||||
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
|
||||
});
|
||||
|
||||
/**
|
||||
* Sends the current size of the main element (the display container)
|
||||
* to the Guacamole server, requesting that the remote display be
|
||||
* resized. If the Guacamole client is not yet connected, it will be
|
||||
* connected and the current size will sent through the initial
|
||||
* handshake. If the size of the main element is not yet known, this
|
||||
* function may need to be invoked multiple times until the size is
|
||||
* known and the client may be connected.
|
||||
*/
|
||||
$scope.mainElementResized = function mainElementResized() {
|
||||
|
||||
// Send new display size, if changed
|
||||
if (client && display && main.offsetWidth && main.offsetHeight) {
|
||||
|
||||
// Connect, if not already connected
|
||||
ManagedClient.connect($scope.client, main.offsetWidth, main.offsetHeight);
|
||||
|
||||
const pixelDensity = $window.devicePixelRatio || 1;
|
||||
const width = main.offsetWidth * pixelDensity;
|
||||
const height = main.offsetHeight * pixelDensity;
|
||||
|
||||
if (display.getWidth() !== width || display.getHeight() !== height)
|
||||
client.sendSize(width, height);
|
||||
|
||||
}
|
||||
|
||||
$scope.$evalAsync(updateDisplayScale);
|
||||
|
||||
};
|
||||
|
||||
// Scroll client display if absolute mouse is in use (the same drag
|
||||
// gesture is needed for moving the mouse pointer with relative mouse)
|
||||
$scope.clientDrag = function clientDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) {
|
||||
|
||||
if ($scope.emulateAbsoluteMouse) {
|
||||
$scope.client.clientProperties.scrollLeft -= deltaX;
|
||||
$scope.client.clientProperties.scrollTop -= deltaY;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* If a pinch gesture is in progress, the scale of the client display when
|
||||
* the pinch gesture began.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
let initialScale = null;
|
||||
|
||||
/**
|
||||
* If a pinch gesture is in progress, the X coordinate of the point on the
|
||||
* client display that was centered within the pinch at the time the
|
||||
* gesture began.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
let initialCenterX = 0;
|
||||
|
||||
/**
|
||||
* If a pinch gesture is in progress, the Y coordinate of the point on the
|
||||
* client display that was centered within the pinch at the time the
|
||||
* gesture began.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
let initialCenterY = 0;
|
||||
|
||||
// Zoom and pan client via pinch gestures
|
||||
$scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) {
|
||||
|
||||
// Do not handle pinch gestures if they would conflict with remote
|
||||
// handling of similar gestures
|
||||
if ($scope.client.multiTouchSupport > 1)
|
||||
return false;
|
||||
|
||||
// Do not handle pinch gestures while relative mouse is in use (2+
|
||||
// contact point gestures are used by relative mouse emulation to
|
||||
// support right click, middle click, and scrolling)
|
||||
if (!$scope.emulateAbsoluteMouse)
|
||||
return false;
|
||||
|
||||
// Stop gesture if not in progress
|
||||
if (!inProgress) {
|
||||
initialScale = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set initial scale if gesture has just started
|
||||
if (!initialScale) {
|
||||
initialScale = $scope.client.clientProperties.scale;
|
||||
initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale;
|
||||
initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale;
|
||||
}
|
||||
|
||||
// Determine new scale absolutely
|
||||
let currentScale = initialScale * currentLength / startLength;
|
||||
|
||||
// Fix scale within limits - scroll will be miscalculated otherwise
|
||||
currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale);
|
||||
currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale);
|
||||
|
||||
// Update scale based on pinch distance
|
||||
$scope.client.clientProperties.autoFit = false;
|
||||
$scope.client.clientProperties.scale = currentScale;
|
||||
|
||||
// Scroll display to keep original pinch location centered within current pinch
|
||||
$scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX;
|
||||
$scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY;
|
||||
|
||||
return false;
|
||||
|
||||
};
|
||||
|
||||
// Ensure focus is regained via mousedown before forwarding event
|
||||
mouse.on('mousedown', document.body.focus.bind(document.body));
|
||||
|
||||
// Forward all mouse events
|
||||
mouse.onEach(['mousedown', 'mousemove', 'mouseup'], handleMouseEvent);
|
||||
|
||||
// Hide software cursor when mouse leaves display
|
||||
mouse.on('mouseout', function() {
|
||||
if (!display) return;
|
||||
display.showCursor(false);
|
||||
});
|
||||
|
||||
// Update remote clipboard if local clipboard changes
|
||||
$scope.$on('guacClipboard', function onClipboard(event, data) {
|
||||
ManagedClient.setClipboard($scope.client, data);
|
||||
});
|
||||
|
||||
// Translate local keydown events to remote keydown events if keyboard is enabled
|
||||
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
|
||||
if ($scope.client.clientProperties.focused) {
|
||||
client.sendKeyEvent(1, keysym);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Translate local keyup events to remote keyup events if keyboard is enabled
|
||||
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
|
||||
if ($scope.client.clientProperties.focused) {
|
||||
client.sendKeyEvent(0, keysym);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Universally handle all synthetic keydown events
|
||||
$scope.$on('guacSyntheticKeydown', function syntheticKeydownListener(event, keysym) {
|
||||
if ($scope.client.clientProperties.focused)
|
||||
client.sendKeyEvent(1, keysym);
|
||||
});
|
||||
|
||||
// Universally handle all synthetic keyup events
|
||||
$scope.$on('guacSyntheticKeyup', function syntheticKeyupListener(event, keysym) {
|
||||
if ($scope.client.clientProperties.focused)
|
||||
client.sendKeyEvent(0, keysym);
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether a drag/drop operation is currently in progress (the user has
|
||||
* dragged a file over the Guacamole connection but has not yet
|
||||
* dropped it).
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
$scope.dropPending = false;
|
||||
|
||||
/**
|
||||
* Displays a visual indication that dropping the file currently
|
||||
* being dragged is possible. Further propagation and default behavior
|
||||
* of the given event is automatically prevented.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event related to the in-progress drag/drop operation.
|
||||
*/
|
||||
const notifyDragStart = function notifyDragStart(e) {
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$scope.$apply(() => {
|
||||
$scope.dropPending = true;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the visual indication that dropping the file currently
|
||||
* being dragged is possible. Further propagation and default behavior
|
||||
* of the given event is automatically prevented.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event related to the end of the former drag/drop operation.
|
||||
*/
|
||||
const notifyDragEnd = function notifyDragEnd(e) {
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$scope.$apply(() => {
|
||||
$scope.dropPending = false;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
main.addEventListener('dragenter', notifyDragStart, false);
|
||||
main.addEventListener('dragover', notifyDragStart, false);
|
||||
main.addEventListener('dragleave', notifyDragEnd, false);
|
||||
|
||||
// File drop event handler
|
||||
main.addEventListener('drop', function(e) {
|
||||
|
||||
notifyDragEnd(e);
|
||||
|
||||
// Ignore file drops if no attached client
|
||||
if (!$scope.client)
|
||||
return;
|
||||
|
||||
// Upload each file
|
||||
const files = e.dataTransfer.files;
|
||||
for (let i = 0; i < files.length; i++)
|
||||
ManagedClient.uploadFile($scope.client, files[i]);
|
||||
|
||||
}, false);
|
||||
|
||||
}];
|
||||
|
||||
return directive;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,409 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive for displaying a non-global notification describing the status
|
||||
* of a specific Guacamole client, including prompts for any information
|
||||
* necessary to continue the connection.
|
||||
*/
|
||||
angular.module('client').directive('guacClientNotification', [function guacClientNotification() {
|
||||
|
||||
const directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: 'app/client/templates/guacClientNotification.html'
|
||||
};
|
||||
|
||||
directive.scope = {
|
||||
|
||||
/**
|
||||
* The client whose status should be displayed.
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
client : '='
|
||||
|
||||
};
|
||||
|
||||
directive.controller = ['$scope', '$injector', '$element',
|
||||
function guacClientNotificationController($scope, $injector, $element) {
|
||||
|
||||
// Required types
|
||||
const ManagedClient = $injector.get('ManagedClient');
|
||||
const ManagedClientState = $injector.get('ManagedClientState');
|
||||
const Protocol = $injector.get('Protocol');
|
||||
|
||||
// Required services
|
||||
const $location = $injector.get('$location');
|
||||
const authenticationService = $injector.get('authenticationService');
|
||||
const guacClientManager = $injector.get('guacClientManager');
|
||||
const guacTranslate = $injector.get('guacTranslate');
|
||||
const requestService = $injector.get('requestService');
|
||||
const userPageService = $injector.get('userPageService');
|
||||
|
||||
/**
|
||||
* A Notification object describing the client status to display as a
|
||||
* dialog or prompt, as would be accepted by guacNotification.showStatus(),
|
||||
* or false if no status should be shown.
|
||||
*
|
||||
* @type {Notification|Object|Boolean}
|
||||
*/
|
||||
$scope.status = false;
|
||||
|
||||
/**
|
||||
* All error codes for which automatic reconnection is appropriate when a
|
||||
* client error occurs.
|
||||
*/
|
||||
const CLIENT_AUTO_RECONNECT = {
|
||||
0x0200: true,
|
||||
0x0202: true,
|
||||
0x0203: true,
|
||||
0x0207: true,
|
||||
0x0208: true,
|
||||
0x0301: true,
|
||||
0x0308: true
|
||||
};
|
||||
|
||||
/**
|
||||
* All error codes for which automatic reconnection is appropriate when a
|
||||
* tunnel error occurs.
|
||||
*/
|
||||
const TUNNEL_AUTO_RECONNECT = {
|
||||
0x0200: true,
|
||||
0x0202: true,
|
||||
0x0203: true,
|
||||
0x0207: true,
|
||||
0x0208: true,
|
||||
0x0308: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Action which logs out from Guacamole entirely.
|
||||
*/
|
||||
const LOGOUT_ACTION = {
|
||||
name : "CLIENT.ACTION_LOGOUT",
|
||||
className : "logout button",
|
||||
callback : function logoutCallback() {
|
||||
authenticationService.logout()
|
||||
['catch'](requestService.IGNORE);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Action which returns the user to the home screen. If the home page has
|
||||
* not yet been determined, this will be null.
|
||||
*/
|
||||
let NAVIGATE_HOME_ACTION = null;
|
||||
|
||||
// Assign home page action once user's home page has been determined
|
||||
userPageService.getHomePage()
|
||||
.then(function homePageRetrieved(homePage) {
|
||||
|
||||
// Define home action only if different from current location
|
||||
if ($location.path() !== homePage.url) {
|
||||
NAVIGATE_HOME_ACTION = {
|
||||
name : "CLIENT.ACTION_NAVIGATE_HOME",
|
||||
className : "home button",
|
||||
callback : function navigateHomeCallback() {
|
||||
$location.url(homePage.url);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}, requestService.WARN);
|
||||
|
||||
/**
|
||||
* Action which replaces the current client with a newly-connected client.
|
||||
*/
|
||||
const RECONNECT_ACTION = {
|
||||
name : "CLIENT.ACTION_RECONNECT",
|
||||
className : "reconnect button",
|
||||
callback : function reconnectCallback() {
|
||||
$scope.client = guacClientManager.replaceManagedClient($scope.client.id);
|
||||
$scope.status = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The reconnect countdown to display if an error or status warrants an
|
||||
* automatic, timed reconnect.
|
||||
*/
|
||||
const RECONNECT_COUNTDOWN = {
|
||||
text: "CLIENT.TEXT_RECONNECT_COUNTDOWN",
|
||||
callback: RECONNECT_ACTION.callback,
|
||||
remaining: 15
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a notification at the end of a Guacamole connection, whether
|
||||
* that connection is ending normally or due to an error. As the end of
|
||||
* a Guacamole connection may be due to changes in authentication status,
|
||||
* this will also implicitly peform a re-authentication attempt to check
|
||||
* for such changes, possibly resulting in auth-related events like
|
||||
* guacInvalidCredentials.
|
||||
*
|
||||
* @param {Notification|Boolean|Object} status
|
||||
* The status notification to show, as would be accepted by
|
||||
* guacNotification.showStatus().
|
||||
*/
|
||||
const notifyConnectionClosed = function notifyConnectionClosed(status) {
|
||||
|
||||
// Re-authenticate to verify auth status at end of connection
|
||||
authenticationService.updateCurrentToken($location.search())
|
||||
['catch'](requestService.IGNORE)
|
||||
|
||||
// Show the requested status once the authentication check has finished
|
||||
['finally'](function authenticationCheckComplete() {
|
||||
$scope.status = status;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Notifies the user that the connection state has changed.
|
||||
*
|
||||
* @param {String} connectionState
|
||||
* The current connection state, as defined by
|
||||
* ManagedClientState.ConnectionState.
|
||||
*/
|
||||
const notifyConnectionState = function notifyConnectionState(connectionState) {
|
||||
|
||||
// Hide any existing status
|
||||
$scope.status = false;
|
||||
|
||||
// Do not display status if status not known
|
||||
if (!connectionState)
|
||||
return;
|
||||
|
||||
// Build array of available actions
|
||||
let actions;
|
||||
if (NAVIGATE_HOME_ACTION)
|
||||
actions = [ NAVIGATE_HOME_ACTION, RECONNECT_ACTION, LOGOUT_ACTION ];
|
||||
else
|
||||
actions = [ RECONNECT_ACTION, LOGOUT_ACTION ];
|
||||
|
||||
// Get any associated status code
|
||||
const status = $scope.client.clientState.statusCode;
|
||||
|
||||
// Connecting
|
||||
if (connectionState === ManagedClientState.ConnectionState.CONNECTING
|
||||
|| connectionState === ManagedClientState.ConnectionState.WAITING) {
|
||||
$scope.status = {
|
||||
className : "connecting",
|
||||
title: "CLIENT.DIALOG_HEADER_CONNECTING",
|
||||
text: {
|
||||
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Client error
|
||||
else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) {
|
||||
|
||||
// Translation IDs for this error code
|
||||
const errorPrefix = "CLIENT.ERROR_CLIENT_";
|
||||
const errorId = errorPrefix + status.toString(16).toUpperCase();
|
||||
const defaultErrorId = errorPrefix + "DEFAULT";
|
||||
|
||||
// Determine whether the reconnect countdown applies
|
||||
const countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
|
||||
|
||||
// Use the guacTranslate service to determine if there is a translation for
|
||||
// this error code; if not, use the default
|
||||
guacTranslate(errorId, defaultErrorId).then(
|
||||
|
||||
// Show error status
|
||||
translationResult => notifyConnectionClosed({
|
||||
className : "error",
|
||||
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
|
||||
text : {
|
||||
key : translationResult.id
|
||||
},
|
||||
countdown : countdown,
|
||||
actions : actions
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// Tunnel error
|
||||
else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) {
|
||||
|
||||
// Translation IDs for this error code
|
||||
const errorPrefix = "CLIENT.ERROR_TUNNEL_";
|
||||
const errorId = errorPrefix + status.toString(16).toUpperCase();
|
||||
const defaultErrorId = errorPrefix + "DEFAULT";
|
||||
|
||||
// Determine whether the reconnect countdown applies
|
||||
const countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
|
||||
|
||||
// Use the guacTranslate service to determine if there is a translation for
|
||||
// this error code; if not, use the default
|
||||
guacTranslate(errorId, defaultErrorId).then(
|
||||
|
||||
// Show error status
|
||||
translationResult => notifyConnectionClosed({
|
||||
className : "error",
|
||||
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
|
||||
text : {
|
||||
key : translationResult.id
|
||||
},
|
||||
countdown : countdown,
|
||||
actions : actions
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// Disconnected
|
||||
else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) {
|
||||
notifyConnectionClosed({
|
||||
title : "CLIENT.DIALOG_HEADER_DISCONNECTED",
|
||||
text : {
|
||||
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
|
||||
},
|
||||
actions : actions
|
||||
});
|
||||
}
|
||||
|
||||
// Hide status for all other states
|
||||
else
|
||||
$scope.status = false;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompts the user to enter additional connection parameters. If the
|
||||
* protocol and associated parameters of the underlying connection are not
|
||||
* yet known, this function has no effect and should be re-invoked once
|
||||
* the parameters are known.
|
||||
*
|
||||
* @param {Object.<String, String>} requiredParameters
|
||||
* The set of all parameters requested by the server via "required"
|
||||
* instructions, where each object key is the name of a requested
|
||||
* parameter and each value is the current value entered by the user.
|
||||
*/
|
||||
const notifyParametersRequired = function notifyParametersRequired(requiredParameters) {
|
||||
|
||||
/**
|
||||
* Action which submits the current set of parameter values, requesting
|
||||
* that the connection continue.
|
||||
*/
|
||||
const SUBMIT_PARAMETERS = {
|
||||
name : "CLIENT.ACTION_CONTINUE",
|
||||
className : "button",
|
||||
callback : function submitParameters() {
|
||||
if ($scope.client) {
|
||||
const params = $scope.client.requiredParameters;
|
||||
$scope.client.requiredParameters = null;
|
||||
ManagedClient.sendArguments($scope.client, params);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Action which cancels submission of additional parameters and
|
||||
* disconnects from the current connection.
|
||||
*/
|
||||
const CANCEL_PARAMETER_SUBMISSION = {
|
||||
name : "CLIENT.ACTION_CANCEL",
|
||||
className : "button",
|
||||
callback : function cancelSubmission() {
|
||||
$scope.client.requiredParameters = null;
|
||||
$scope.client.client.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
// Attempt to prompt for parameters only if the parameters that apply
|
||||
// to the underlying connection are known
|
||||
if (!$scope.client.protocol || !$scope.client.forms)
|
||||
return;
|
||||
|
||||
// Prompt for parameters
|
||||
$scope.status = {
|
||||
className : "parameters-required",
|
||||
formNamespace : Protocol.getNamespace($scope.client.protocol),
|
||||
forms : $scope.client.forms,
|
||||
formModel : requiredParameters,
|
||||
formSubmitCallback : SUBMIT_PARAMETERS.callback,
|
||||
actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ]
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the given connection state allows for submission of
|
||||
* connection parameters via "argv" instructions.
|
||||
*
|
||||
* @param {String} connectionState
|
||||
* The connection state to test, as defined by
|
||||
* ManagedClientState.ConnectionState.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true if the given connection state allows submission of connection
|
||||
* parameters via "argv" instructions, false otherwise.
|
||||
*/
|
||||
const canSubmitParameters = function canSubmitParameters(connectionState) {
|
||||
return (connectionState === ManagedClientState.ConnectionState.WAITING ||
|
||||
connectionState === ManagedClientState.ConnectionState.CONNECTED);
|
||||
};
|
||||
|
||||
// Show status dialog when connection status changes
|
||||
$scope.$watchGroup([
|
||||
'client.clientState.connectionState',
|
||||
'client.requiredParameters',
|
||||
'client.protocol',
|
||||
'client.forms'
|
||||
], function clientStateChanged(newValues) {
|
||||
|
||||
const connectionState = newValues[0];
|
||||
const requiredParameters = newValues[1];
|
||||
|
||||
// Prompt for parameters only if parameters can actually be submitted
|
||||
if (requiredParameters && canSubmitParameters(connectionState))
|
||||
notifyParametersRequired(requiredParameters);
|
||||
|
||||
// Otherwise, just show general connection state
|
||||
else
|
||||
notifyConnectionState(connectionState);
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Prevents the default behavior of the given AngularJS event if a
|
||||
* notification is currently shown and the client is focused.
|
||||
*
|
||||
* @param {event} e
|
||||
* The AngularJS event to selectively prevent.
|
||||
*/
|
||||
const preventDefaultDuringNotification = function preventDefaultDuringNotification(e) {
|
||||
if ($scope.status && $scope.client.clientProperties.focused)
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// Block internal handling of key events (by the client) if a
|
||||
// notification is visible
|
||||
$scope.$on('guacBeforeKeydown', preventDefaultDuringNotification);
|
||||
$scope.$on('guacBeforeKeyup', preventDefaultDuringNotification);
|
||||
|
||||
}];
|
||||
|
||||
return directive;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A toolbar/panel which displays a list of active Guacamole connections. The
|
||||
* panel is fixed to the bottom-right corner of its container and can be
|
||||
* manually hidden/exposed by the user.
|
||||
*/
|
||||
angular.module('client').directive('guacClientPanel', ['$injector', function guacClientPanel($injector) {
|
||||
|
||||
// Required services
|
||||
const guacClientManager = $injector.get('guacClientManager');
|
||||
const sessionStorageFactory = $injector.get('sessionStorageFactory');
|
||||
|
||||
// Required types
|
||||
const ManagedClientGroup = $injector.get('ManagedClientGroup');
|
||||
const ManagedClientState = $injector.get('ManagedClientState');
|
||||
|
||||
/**
|
||||
* Getter/setter for the boolean flag controlling whether the client panel
|
||||
* is currently hidden. This flag is maintained in session-local storage to
|
||||
* allow the state of the panel to persist despite navigation within the
|
||||
* same tab. When hidden, the panel will be collapsed against the right
|
||||
* side of the container. By default, the panel is visible.
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
var panelHidden = sessionStorageFactory.create(false);
|
||||
|
||||
return {
|
||||
// Element only
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
|
||||
/**
|
||||
* The ManagedClientGroup instances associated with the active
|
||||
* connections to be displayed within this panel.
|
||||
*
|
||||
* @type ManagedClientGroup[]
|
||||
*/
|
||||
clientGroups : '='
|
||||
|
||||
},
|
||||
templateUrl: 'app/client/templates/guacClientPanel.html',
|
||||
controller: ['$scope', '$element', function guacClientPanelController($scope, $element) {
|
||||
|
||||
/**
|
||||
* The DOM element containing the scrollable portion of the client
|
||||
* panel.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var scrollableArea = $element.find('.client-panel-connection-list')[0];
|
||||
|
||||
/**
|
||||
* On-scope reference to session-local storage of the flag
|
||||
* controlling whether then panel is hidden.
|
||||
*/
|
||||
$scope.panelHidden = panelHidden;
|
||||
|
||||
/**
|
||||
* Returns whether this panel currently has any client groups
|
||||
* associated with it.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* true if at least one client group is associated with this
|
||||
* panel, false otherwise.
|
||||
*/
|
||||
$scope.hasClientGroups = function hasClientGroups() {
|
||||
return $scope.clientGroups && $scope.clientGroups.some(function(group) {
|
||||
// Check if there is any group that is not attached
|
||||
return !group.attached;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.getIdentifier
|
||||
*/
|
||||
$scope.getIdentifier = ManagedClientGroup.getIdentifier;
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.getTitle
|
||||
*/
|
||||
$scope.getTitle = ManagedClientGroup.getTitle;
|
||||
|
||||
/**
|
||||
* Returns whether the status of any client within the given client
|
||||
* group has changed in a way that requires the user's attention.
|
||||
* This may be due to an error, or due to a server-initiated
|
||||
* disconnect.
|
||||
*
|
||||
* @param {ManagedClientGroup} clientGroup
|
||||
* The client group to test.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if the given client requires the user's attention,
|
||||
* false otherwise.
|
||||
*/
|
||||
$scope.hasStatusUpdate = function hasStatusUpdate(clientGroup) {
|
||||
return _.findIndex(clientGroup.clients, (client) => {
|
||||
|
||||
// Test whether the client has encountered an error
|
||||
switch (client.clientState.connectionState) {
|
||||
case ManagedClientState.ConnectionState.CONNECTION_ERROR:
|
||||
case ManagedClientState.ConnectionState.TUNNEL_ERROR:
|
||||
case ManagedClientState.ConnectionState.DISCONNECTED:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}) !== -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initiates an orderly disconnect of all clients within the given
|
||||
* group. The clients are removed from management such that
|
||||
* attempting to connect to any of the same connections will result
|
||||
* in new connections being established, rather than displaying a
|
||||
* notification that the connection has ended.
|
||||
*
|
||||
* @param {ManagedClientGroup} clientGroup
|
||||
* The group of clients to disconnect.
|
||||
*/
|
||||
$scope.disconnect = function disconnect(clientGroup) {
|
||||
guacClientManager.removeManagedClientGroup(ManagedClientGroup.getIdentifier(clientGroup));
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles whether the client panel is currently hidden.
|
||||
*/
|
||||
$scope.togglePanel = function togglePanel() {
|
||||
panelHidden(!panelHidden());
|
||||
};
|
||||
|
||||
// Override vertical scrolling, scrolling horizontally instead
|
||||
scrollableArea.addEventListener('wheel', function reorientVerticalScroll(e) {
|
||||
|
||||
var deltaMultiplier = {
|
||||
/* DOM_DELTA_PIXEL */ 0x00: 1,
|
||||
/* DOM_DELTA_LINE */ 0x01: 15,
|
||||
/* DOM_DELTA_PAGE */ 0x02: scrollableArea.offsetWidth
|
||||
};
|
||||
|
||||
if (e.deltaY) {
|
||||
this.scrollLeft += e.deltaY * (deltaMultiplier[e.deltaMode] || deltaMultiplier(0x01));
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}]
|
||||
};
|
||||
}]);
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive that displays a status indicator showing the number of users
|
||||
* joined to a connection. The specific usernames of those users are visible in
|
||||
* a tooltip on mouseover, and small notifications are displayed as users
|
||||
* join/leave the connection.
|
||||
*/
|
||||
angular.module('client').directive('guacClientUserCount', [function guacClientUserCount() {
|
||||
|
||||
const directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: 'app/client/templates/guacClientUserCount.html'
|
||||
};
|
||||
|
||||
directive.scope = {
|
||||
|
||||
/**
|
||||
* The client whose current users should be displayed.
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
client : '='
|
||||
|
||||
};
|
||||
|
||||
directive.controller = ['$scope', '$injector', '$element',
|
||||
function guacClientUserCountController($scope, $injector, $element) {
|
||||
|
||||
// Required types
|
||||
var AuthenticationResult = $injector.get('AuthenticationResult');
|
||||
|
||||
// Required services
|
||||
var $translate = $injector.get('$translate');
|
||||
|
||||
/**
|
||||
* The maximum number of messages displayed by this directive at any
|
||||
* given time. Old messages will be discarded as necessary to ensure
|
||||
* the number of messages displayed never exceeds this value.
|
||||
*
|
||||
* @constant
|
||||
* @type number
|
||||
*/
|
||||
var MAX_MESSAGES = 3;
|
||||
|
||||
/**
|
||||
* The list that should contain any notifications regarding users
|
||||
* joining or leaving the connection.
|
||||
*
|
||||
* @type HTMLUListElement
|
||||
*/
|
||||
var messages = $element.find('.client-user-count-messages')[0];
|
||||
|
||||
/**
|
||||
* Map of the usernames of all users of the current connection to the
|
||||
* number of concurrent connections those users have to the current
|
||||
* connection.
|
||||
*
|
||||
* @type Object.<string, number>
|
||||
*/
|
||||
$scope.userCounts = {};
|
||||
|
||||
/**
|
||||
* Displays a message noting that a change related to a particular user
|
||||
* of this connection has occurred.
|
||||
*
|
||||
* @param {!string} str
|
||||
* The key of the translation string containing the message to
|
||||
* display. This translation key must accept "USERNAME" as the
|
||||
* name of the translation parameter containing the username of
|
||||
* the user in question.
|
||||
*
|
||||
* @param {!string} username
|
||||
* The username of the user in question.
|
||||
*/
|
||||
var notify = function notify(str, username) {
|
||||
$translate(str, { 'USERNAME' : username }).then(function translationReady(text) {
|
||||
|
||||
if (messages.childNodes.length === 3)
|
||||
messages.removeChild(messages.lastChild);
|
||||
|
||||
var message = document.createElement('li');
|
||||
message.className = 'client-user-count-message';
|
||||
message.textContent = text;
|
||||
messages.insertBefore(message, messages.firstChild);
|
||||
|
||||
// Automatically remove the notification after its "fadeout"
|
||||
// animation ends. NOTE: This will not fire if the element is
|
||||
// not visible at all.
|
||||
message.addEventListener('animationend', function animationEnded() {
|
||||
messages.removeChild(message);
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a message noting that a particular user has joined the
|
||||
* current connection.
|
||||
*
|
||||
* @param {!string} username
|
||||
* The username of the user that joined.
|
||||
*/
|
||||
var notifyUserJoined = function notifyUserJoined(username) {
|
||||
if ($scope.isAnonymous(username))
|
||||
notify('CLIENT.TEXT_ANONYMOUS_USER_JOINED', username);
|
||||
else
|
||||
notify('CLIENT.TEXT_USER_JOINED', username);
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a message noting that a particular user has left the
|
||||
* current connection.
|
||||
*
|
||||
* @param {!string} username
|
||||
* The username of the user that left.
|
||||
*/
|
||||
var notifyUserLeft = function notifyUserLeft(username) {
|
||||
if ($scope.isAnonymous(username))
|
||||
notify('CLIENT.TEXT_ANONYMOUS_USER_LEFT', username);
|
||||
else
|
||||
notify('CLIENT.TEXT_USER_LEFT', username);
|
||||
};
|
||||
|
||||
/**
|
||||
* The ManagedClient attached to this directive at the time the
|
||||
* notification update scope watch was last invoked. This is necessary
|
||||
* as $scope.$watchGroup() does not allow for the callback to know
|
||||
* whether the scope was previously uninitialized (it's "oldValues"
|
||||
* parameter receives a copy of the new values if there are no old
|
||||
* values).
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
var oldClient = null;
|
||||
|
||||
/**
|
||||
* Returns whether the given username represents an anonymous user.
|
||||
*
|
||||
* @param {!string} username
|
||||
* The username of the user to check.
|
||||
*
|
||||
* @returns {!boolean}
|
||||
* true if the given username represents an anonymous user, false
|
||||
* otherwise.
|
||||
*/
|
||||
$scope.isAnonymous = function isAnonymous(username) {
|
||||
return username === AuthenticationResult.ANONYMOUS_USERNAME;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the translation key of the translation string that should be
|
||||
* used to render the number of connections a user with the given
|
||||
* username has to the current connection. The appropriate string will
|
||||
* vary by whether the user is anonymous.
|
||||
*
|
||||
* @param {!string} username
|
||||
* The username of the user to check.
|
||||
*
|
||||
* @returns {!string}
|
||||
* The translation key of the translation string that should be
|
||||
* used to render the number of connections the user with the given
|
||||
* username has to the current connection.
|
||||
*/
|
||||
$scope.getUserCountTranslationKey = function getUserCountTranslationKey(username) {
|
||||
return $scope.isAnonymous(username) ? 'CLIENT.INFO_ANONYMOUS_USER_COUNT' : 'CLIENT.INFO_USER_COUNT';
|
||||
};
|
||||
|
||||
// Update visible notifications as users join/leave
|
||||
$scope.$watchGroup([ 'client', 'client.userCount' ], function usersChanged() {
|
||||
|
||||
// Resynchronize directive with state of any attached client when
|
||||
// the client changes, to ensure notifications are only shown for
|
||||
// future changes in users present
|
||||
if (oldClient !== $scope.client) {
|
||||
|
||||
$scope.userCounts = {};
|
||||
oldClient = $scope.client;
|
||||
|
||||
angular.forEach($scope.client.users, function initUsers(connections, username) {
|
||||
var count = Object.keys(connections).length;
|
||||
$scope.userCounts[username] = count;
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Display join/leave notifications for users who are currently
|
||||
// connected but whose connection counts have changed
|
||||
angular.forEach($scope.client.users, function addNewUsers(connections, username) {
|
||||
|
||||
var count = Object.keys(connections).length;
|
||||
var known = $scope.userCounts[username] || 0;
|
||||
|
||||
if (count > known)
|
||||
notifyUserJoined(username);
|
||||
else if (count < known)
|
||||
notifyUserLeft(username);
|
||||
|
||||
$scope.userCounts[username] = count;
|
||||
|
||||
});
|
||||
|
||||
// Display leave notifications for users who are no longer connected
|
||||
angular.forEach($scope.userCounts, function removeOldUsers(count, username) {
|
||||
if (!$scope.client.users[username]) {
|
||||
notifyUserLeft(username);
|
||||
delete $scope.userCounts[username];
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}];
|
||||
|
||||
return directive;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive for controlling the zoom level and scale-to-fit behavior of a
|
||||
* a single Guacamole client.
|
||||
*/
|
||||
angular.module('client').directive('guacClientZoom', [function guacClientZoom() {
|
||||
|
||||
const directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: 'app/client/templates/guacClientZoom.html'
|
||||
};
|
||||
|
||||
directive.scope = {
|
||||
|
||||
/**
|
||||
* The client to control the zoom/autofit of.
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
client : '='
|
||||
|
||||
};
|
||||
|
||||
directive.controller = ['$scope', '$injector', '$element',
|
||||
function guacClientZoomController($scope, $injector, $element) {
|
||||
|
||||
/**
|
||||
* Zooms in by 10%, automatically disabling autofit.
|
||||
*/
|
||||
$scope.zoomIn = function zoomIn() {
|
||||
$scope.client.clientProperties.autoFit = false;
|
||||
$scope.client.clientProperties.scale += 0.1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Zooms out by 10%, automatically disabling autofit.
|
||||
*/
|
||||
$scope.zoomOut = function zoomOut() {
|
||||
$scope.client.clientProperties.autoFit = false;
|
||||
$scope.client.clientProperties.scale -= 0.1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets the client autofit setting to false.
|
||||
*/
|
||||
$scope.clearAutoFit = function clearAutoFit() {
|
||||
$scope.client.clientProperties.autoFit = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Notifies that the autofit setting has been manually changed by the
|
||||
* user.
|
||||
*/
|
||||
$scope.autoFitChanged = function autoFitChanged() {
|
||||
|
||||
// Reset to 100% scale when autofit is first disabled
|
||||
if (!$scope.client.clientProperties.autoFit)
|
||||
$scope.client.clientProperties.scale = 1;
|
||||
|
||||
};
|
||||
|
||||
}];
|
||||
|
||||
return directive;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which displays the contents of a filesystem received through the
|
||||
* Guacamole client.
|
||||
*/
|
||||
angular.module('client').directive('guacFileBrowser', [function guacFileBrowser() {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
|
||||
/**
|
||||
* @type ManagedFilesystem
|
||||
*/
|
||||
filesystem : '='
|
||||
|
||||
},
|
||||
|
||||
templateUrl: 'app/client/templates/guacFileBrowser.html',
|
||||
controller: ['$scope', '$element', '$injector', function guacFileBrowserController($scope, $element, $injector) {
|
||||
|
||||
// Required types
|
||||
var ManagedFilesystem = $injector.get('ManagedFilesystem');
|
||||
|
||||
// Required services
|
||||
var $interpolate = $injector.get('$interpolate');
|
||||
var $templateRequest = $injector.get('$templateRequest');
|
||||
|
||||
/**
|
||||
* The jQuery-wrapped element representing the contents of the
|
||||
* current directory within the file browser.
|
||||
*
|
||||
* @type Element[]
|
||||
*/
|
||||
var currentDirectoryContents = $element.find('.current-directory-contents');
|
||||
|
||||
/**
|
||||
* Statically-cached template HTML used to render each file within
|
||||
* a directory. Once available, this will be used through
|
||||
* createFileElement() to generate the DOM elements which make up
|
||||
* a directory listing.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
var fileTemplate = null;
|
||||
|
||||
/**
|
||||
* Returns whether the given file is a normal file.
|
||||
*
|
||||
* @param {ManagedFilesystem.File} file
|
||||
* The file to test.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if the given file is a normal file, false otherwise.
|
||||
*/
|
||||
$scope.isNormalFile = function isNormalFile(file) {
|
||||
return file.type === ManagedFilesystem.File.Type.NORMAL;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the given file is a directory.
|
||||
*
|
||||
* @param {ManagedFilesystem.File} file
|
||||
* The file to test.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if the given file is a directory, false otherwise.
|
||||
*/
|
||||
$scope.isDirectory = function isDirectory(file) {
|
||||
return file.type === ManagedFilesystem.File.Type.DIRECTORY;
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the currently-displayed directory to the given
|
||||
* directory.
|
||||
*
|
||||
* @param {ManagedFilesystem.File} file
|
||||
* The directory to change to.
|
||||
*/
|
||||
$scope.changeDirectory = function changeDirectory(file) {
|
||||
ManagedFilesystem.changeDirectory($scope.filesystem, file);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initiates a download of the given file. The progress of the
|
||||
* download can be observed through guacFileTransferManager.
|
||||
*
|
||||
* @param {ManagedFilesystem.File} file
|
||||
* The file to download.
|
||||
*/
|
||||
$scope.downloadFile = function downloadFile(file) {
|
||||
ManagedFilesystem.downloadFile($scope.filesystem, file.streamName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively interpolates all text nodes within the DOM tree of
|
||||
* the given element. All other node types, attributes, etc. will
|
||||
* be left uninterpolated.
|
||||
*
|
||||
* @param {Element} element
|
||||
* The element at the root of the DOM tree to be interpolated.
|
||||
*
|
||||
* @param {Object} context
|
||||
* The evaluation context to use when evaluating expressions
|
||||
* embedded in text nodes within the provided element.
|
||||
*/
|
||||
var interpolateElement = function interpolateElement(element, context) {
|
||||
|
||||
// Interpolate the contents of text nodes directly
|
||||
if (element.nodeType === Node.TEXT_NODE)
|
||||
element.nodeValue = $interpolate(element.nodeValue)(context);
|
||||
|
||||
// Recursively interpolate the contents of all descendant text
|
||||
// nodes
|
||||
if (element.hasChildNodes()) {
|
||||
var children = element.childNodes;
|
||||
for (var i = 0; i < children.length; i++)
|
||||
interpolateElement(children[i], context);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new element representing the given file and properly
|
||||
* handling user events, bypassing the overhead incurred through
|
||||
* use of ngRepeat and related techniques.
|
||||
*
|
||||
* Note that this function depends on the availability of the
|
||||
* statically-cached fileTemplate.
|
||||
*
|
||||
* @param {ManagedFilesystem.File} file
|
||||
* The file to generate an element for.
|
||||
*
|
||||
* @returns {Element[]}
|
||||
* A jQuery-wrapped array containing a single DOM element
|
||||
* representing the given file.
|
||||
*/
|
||||
var createFileElement = function createFileElement(file) {
|
||||
|
||||
// Create from internal template
|
||||
var element = angular.element(fileTemplate);
|
||||
interpolateElement(element[0], file);
|
||||
|
||||
// Double-clicking on unknown file types will do nothing
|
||||
var fileAction = function doNothing() {};
|
||||
|
||||
// Change current directory when directories are clicked
|
||||
if ($scope.isDirectory(file)) {
|
||||
element.addClass('directory');
|
||||
fileAction = function changeDirectory() {
|
||||
$scope.changeDirectory(file);
|
||||
};
|
||||
}
|
||||
|
||||
// Initiate downloads when normal files are clicked
|
||||
else if ($scope.isNormalFile(file)) {
|
||||
element.addClass('normal-file');
|
||||
fileAction = function downloadFile() {
|
||||
$scope.downloadFile(file);
|
||||
};
|
||||
}
|
||||
|
||||
// Mark file as focused upon click
|
||||
element.on('click', function handleFileClick() {
|
||||
|
||||
// Fire file-specific action if already focused
|
||||
if (element.hasClass('focused')) {
|
||||
fileAction();
|
||||
element.removeClass('focused');
|
||||
}
|
||||
|
||||
// Otherwise mark as focused
|
||||
else {
|
||||
element.parent().children().removeClass('focused');
|
||||
element.addClass('focused');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Prevent text selection during navigation
|
||||
element.on('selectstart', function avoidSelect(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
return element;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts the given map of files, returning an array of those files
|
||||
* grouped by file type (directories first, followed by non-
|
||||
* directories) and sorted lexicographically.
|
||||
*
|
||||
* @param {Object.<String, ManagedFilesystem.File>} files
|
||||
* The map of files to sort.
|
||||
*
|
||||
* @returns {ManagedFilesystem.File[]}
|
||||
* An array of all files in the given map, sorted
|
||||
* lexicographically with directories first, followed by non-
|
||||
* directories.
|
||||
*/
|
||||
var sortFiles = function sortFiles(files) {
|
||||
|
||||
// Get all given files as an array
|
||||
var unsortedFiles = [];
|
||||
for (var name in files)
|
||||
unsortedFiles.push(files[name]);
|
||||
|
||||
// Sort files - directories first, followed by all other files
|
||||
// sorted by name
|
||||
return unsortedFiles.sort(function fileComparator(a, b) {
|
||||
|
||||
// Directories come before non-directories
|
||||
if ($scope.isDirectory(a) && !$scope.isDirectory(b))
|
||||
return -1;
|
||||
|
||||
// Non-directories come after directories
|
||||
if (!$scope.isDirectory(a) && $scope.isDirectory(b))
|
||||
return 1;
|
||||
|
||||
// All other combinations are sorted by name
|
||||
return a.name.localeCompare(b.name);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Watch directory contents once file template is available
|
||||
$templateRequest('app/client/templates/file.html').then(function fileTemplateRetrieved(html) {
|
||||
|
||||
// Store file template statically
|
||||
fileTemplate = html;
|
||||
|
||||
// Update the contents of the file browser whenever the current directory (or its contents) changes
|
||||
$scope.$watch('filesystem.currentDirectory.files', function currentDirectoryChanged(files) {
|
||||
|
||||
// Clear current content
|
||||
currentDirectoryContents.html('');
|
||||
|
||||
// Display all files within current directory, sorted
|
||||
angular.forEach(sortFiles(files), function displayFile(file) {
|
||||
currentDirectoryContents.append(createFileElement(file));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}, angular.noop); // end retrieve file template
|
||||
|
||||
// Refresh file browser when any upload completes
|
||||
$scope.$on('guacUploadComplete', function uploadComplete(event, filename) {
|
||||
|
||||
// Refresh filesystem, if it exists
|
||||
if ($scope.filesystem)
|
||||
ManagedFilesystem.refresh($scope.filesystem, $scope.filesystem.currentDirectory);
|
||||
|
||||
});
|
||||
|
||||
}]
|
||||
|
||||
};
|
||||
}]);
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Directive which displays an active file transfer, providing links for
|
||||
* downloads, if applicable.
|
||||
*/
|
||||
angular.module('client').directive('guacFileTransfer', [function guacFileTransfer() {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
|
||||
/**
|
||||
* The file transfer to display.
|
||||
*
|
||||
* @type ManagedFileUpload|ManagedFileDownload
|
||||
*/
|
||||
transfer : '='
|
||||
|
||||
},
|
||||
|
||||
templateUrl: 'app/client/templates/guacFileTransfer.html',
|
||||
controller: ['$scope', '$injector', function guacFileTransferController($scope, $injector) {
|
||||
|
||||
// Required services
|
||||
const guacTranslate = $injector.get('guacTranslate');
|
||||
|
||||
// Required types
|
||||
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
|
||||
|
||||
/**
|
||||
* Returns the unit string that is most appropriate for the
|
||||
* number of bytes transferred thus far - either 'gb', 'mb', 'kb',
|
||||
* or 'b'.
|
||||
*
|
||||
* @returns {String}
|
||||
* The unit string that is most appropriate for the number of
|
||||
* bytes transferred thus far.
|
||||
*/
|
||||
$scope.getProgressUnit = function getProgressUnit() {
|
||||
|
||||
var bytes = $scope.transfer.progress;
|
||||
|
||||
// Gigabytes
|
||||
if (bytes > 1000000000)
|
||||
return 'gb';
|
||||
|
||||
// Megabytes
|
||||
if (bytes > 1000000)
|
||||
return 'mb';
|
||||
|
||||
// Kilobytes
|
||||
if (bytes > 1000)
|
||||
return 'kb';
|
||||
|
||||
// Bytes
|
||||
return 'b';
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the amount of data transferred thus far, in the units
|
||||
* returned by getProgressUnit().
|
||||
*
|
||||
* @returns {Number}
|
||||
* The amount of data transferred thus far, in the units
|
||||
* returned by getProgressUnit().
|
||||
*/
|
||||
$scope.getProgressValue = function getProgressValue() {
|
||||
|
||||
var bytes = $scope.transfer.progress;
|
||||
if (!bytes)
|
||||
return bytes;
|
||||
|
||||
// Convert bytes to necessary units
|
||||
switch ($scope.getProgressUnit()) {
|
||||
|
||||
// Gigabytes
|
||||
case 'gb':
|
||||
return (bytes / 1000000000).toFixed(1);
|
||||
|
||||
// Megabytes
|
||||
case 'mb':
|
||||
return (bytes / 1000000).toFixed(1);
|
||||
|
||||
// Kilobytes
|
||||
case 'kb':
|
||||
return (bytes / 1000).toFixed(1);
|
||||
|
||||
// Bytes
|
||||
case 'b':
|
||||
default:
|
||||
return bytes;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the percentage of bytes transferred thus far, if the
|
||||
* overall length of the file is known.
|
||||
*
|
||||
* @returns {Number}
|
||||
* The percentage of bytes transferred thus far, if the
|
||||
* overall length of the file is known.
|
||||
*/
|
||||
$scope.getPercentDone = function getPercentDone() {
|
||||
return $scope.transfer.progress / $scope.transfer.length * 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether the associated file transfer is in progress.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if the file transfer is in progress, false othherwise.
|
||||
*/
|
||||
$scope.isInProgress = function isInProgress() {
|
||||
|
||||
// Not in progress if there is no transfer
|
||||
if (!$scope.transfer)
|
||||
return false;
|
||||
|
||||
// Determine in-progress status based on stream state
|
||||
switch ($scope.transfer.transferState.streamState) {
|
||||
|
||||
// IDLE or OPEN file transfers are active
|
||||
case ManagedFileTransferState.StreamState.IDLE:
|
||||
case ManagedFileTransferState.StreamState.OPEN:
|
||||
return true;
|
||||
|
||||
// All others are not active
|
||||
default:
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the file associated with this file transfer can
|
||||
* be saved locally via a call to save().
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if a call to save() will result in the file being
|
||||
* saved, false otherwise.
|
||||
*/
|
||||
$scope.isSavable = function isSavable() {
|
||||
return !!$scope.transfer.blob;
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the downloaded file, if any. If this transfer is an upload
|
||||
* or the download is not yet complete, this function has no
|
||||
* effect.
|
||||
*/
|
||||
$scope.save = function save() {
|
||||
|
||||
// Ignore if no blob exists
|
||||
if (!$scope.transfer.blob)
|
||||
return;
|
||||
|
||||
// Save file
|
||||
saveAs($scope.transfer.blob, $scope.transfer.filename);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether an error has occurred. If an error has occurred,
|
||||
* the transfer is no longer active, and the text of the error can
|
||||
* be read from getErrorText().
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if an error has occurred during transfer, false
|
||||
* otherwise.
|
||||
*/
|
||||
$scope.hasError = function hasError() {
|
||||
return $scope.transfer.transferState.streamState === ManagedFileTransferState.StreamState.ERROR;
|
||||
};
|
||||
|
||||
// The translated error message for the current status code
|
||||
$scope.translatedErrorMessage = '';
|
||||
|
||||
$scope.$watch('transfer.transferState.statusCode', function statusCodeChanged(statusCode) {
|
||||
|
||||
// Determine translation name of error
|
||||
const errorName = 'CLIENT.ERROR_UPLOAD_' + statusCode.toString(16).toUpperCase();
|
||||
|
||||
// Use translation string, or the default if no translation is found for this error code
|
||||
guacTranslate(errorName, 'CLIENT.ERROR_UPLOAD_DEFAULT').then(
|
||||
translationResult => $scope.translatedErrorMessage = translationResult.message
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
}] // end file transfer controller
|
||||
|
||||
};
|
||||
}]);
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Directive which displays all active file transfers.
|
||||
*/
|
||||
angular.module('client').directive('guacFileTransferManager', [function guacFileTransferManager() {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
|
||||
/**
|
||||
* The client group whose file transfers should be managed by this
|
||||
* directive.
|
||||
*
|
||||
* @type ManagedClientGroup
|
||||
*/
|
||||
clientGroup : '='
|
||||
|
||||
},
|
||||
|
||||
templateUrl: 'app/client/templates/guacFileTransferManager.html',
|
||||
controller: ['$scope', '$injector', function guacFileTransferManagerController($scope, $injector) {
|
||||
|
||||
// Required types
|
||||
const ManagedClient = $injector.get('ManagedClient');
|
||||
const ManagedClientGroup = $injector.get('ManagedClientGroup');
|
||||
const ManagedFileTransferState = $injector.get('ManagedFileTransferState');
|
||||
|
||||
/**
|
||||
* Determines whether the given file transfer state indicates an
|
||||
* in-progress transfer.
|
||||
*
|
||||
* @param {ManagedFileTransferState} transferState
|
||||
* The file transfer state to check.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if the given file transfer state indicates an in-
|
||||
* progress transfer, false otherwise.
|
||||
*/
|
||||
var isInProgress = function isInProgress(transferState) {
|
||||
switch (transferState.streamState) {
|
||||
|
||||
// IDLE or OPEN file transfers are active
|
||||
case ManagedFileTransferState.StreamState.IDLE:
|
||||
case ManagedFileTransferState.StreamState.OPEN:
|
||||
return true;
|
||||
|
||||
// All others are not active
|
||||
default:
|
||||
return false;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all file transfers which are not currently in-progress.
|
||||
*/
|
||||
$scope.clearCompletedTransfers = function clearCompletedTransfers() {
|
||||
|
||||
// Nothing to clear if no client group attached
|
||||
if (!$scope.clientGroup)
|
||||
return;
|
||||
|
||||
// Remove completed uploads
|
||||
$scope.clientGroup.clients.forEach(client => {
|
||||
client.uploads = client.uploads.filter(function isUploadInProgress(upload) {
|
||||
return isInProgress(upload.transferState);
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.hasMultipleClients
|
||||
*/
|
||||
$scope.hasMultipleClients = ManagedClientGroup.hasMultipleClients;
|
||||
|
||||
/**
|
||||
* @borrows ManagedClient.hasTransfers
|
||||
*/
|
||||
$scope.hasTransfers = ManagedClient.hasTransfers;
|
||||
|
||||
}]
|
||||
|
||||
};
|
||||
}]);
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive for displaying a Guacamole client as a non-interactive
|
||||
* thumbnail.
|
||||
*/
|
||||
angular.module('client').directive('guacThumbnail', [function guacThumbnail() {
|
||||
|
||||
return {
|
||||
// Element only
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
|
||||
/**
|
||||
* The client to display within this guacThumbnail directive.
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
client : '='
|
||||
|
||||
},
|
||||
templateUrl: 'app/client/templates/guacThumbnail.html',
|
||||
controller: ['$scope', '$injector', '$element', function guacThumbnailController($scope, $injector, $element) {
|
||||
|
||||
// Required services
|
||||
var $window = $injector.get('$window');
|
||||
|
||||
/**
|
||||
* The display of the current Guacamole client instance.
|
||||
*
|
||||
* @type Guacamole.Display
|
||||
*/
|
||||
var display = null;
|
||||
|
||||
/**
|
||||
* The element associated with the display of the current
|
||||
* Guacamole client instance.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var displayElement = null;
|
||||
|
||||
/**
|
||||
* The element which must contain the Guacamole display element.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var displayContainer = $element.find('.display')[0];
|
||||
|
||||
/**
|
||||
* The main containing element for the entire directive.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var main = $element[0];
|
||||
|
||||
/**
|
||||
* Updates the scale of the attached Guacamole.Client based on current window
|
||||
* size and "auto-fit" setting.
|
||||
*/
|
||||
$scope.updateDisplayScale = function updateDisplayScale() {
|
||||
|
||||
if (!display) return;
|
||||
|
||||
// Fit within available area
|
||||
display.scale(Math.min(
|
||||
main.offsetWidth / Math.max(display.getWidth(), 1),
|
||||
main.offsetHeight / Math.max(display.getHeight(), 1)
|
||||
));
|
||||
|
||||
};
|
||||
|
||||
// Attach any given managed client
|
||||
$scope.$watch('client', function attachManagedClient(managedClient) {
|
||||
|
||||
// Remove any existing display
|
||||
displayContainer.innerHTML = "";
|
||||
|
||||
// Only proceed if a client is given
|
||||
if (!managedClient)
|
||||
return;
|
||||
|
||||
// Get Guacamole client instance
|
||||
var client = managedClient.client;
|
||||
|
||||
// Attach possibly new display
|
||||
display = client.getDisplay();
|
||||
|
||||
// Add display element
|
||||
displayElement = display.getElement();
|
||||
displayContainer.appendChild(displayElement);
|
||||
|
||||
});
|
||||
|
||||
// Update scale when display is resized
|
||||
$scope.$watch('client.managedDisplay.size', function setDisplaySize(size) {
|
||||
$scope.$evalAsync($scope.updateDisplayScale);
|
||||
});
|
||||
|
||||
}]
|
||||
};
|
||||
}]);
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which displays one or more Guacamole clients in an evenly-tiled
|
||||
* view. The number of rows and columns used for the arrangement of tiles is
|
||||
* automatically determined by the number of clients present.
|
||||
*/
|
||||
angular.module('client').directive('guacTiledClients', [function guacTiledClients() {
|
||||
|
||||
const directive = {
|
||||
restrict: 'E',
|
||||
templateUrl: 'app/client/templates/guacTiledClients.html',
|
||||
};
|
||||
|
||||
directive.scope = {
|
||||
|
||||
/**
|
||||
* The function to invoke when the "close" button in the header of a
|
||||
* client tile is clicked. The ManagedClient that is closed will be
|
||||
* made available to the Angular expression defining the callback as
|
||||
* "$client".
|
||||
*
|
||||
* @type function
|
||||
*/
|
||||
onClose : '&',
|
||||
|
||||
/**
|
||||
* The group of Guacamole clients that should be displayed in an
|
||||
* evenly-tiled grid arrangement.
|
||||
*
|
||||
* @type ManagedClientGroup
|
||||
*/
|
||||
clientGroup : '=',
|
||||
|
||||
/**
|
||||
* Whether translation of touch to mouse events should emulate an
|
||||
* absolute pointer device, or a relative pointer device.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
emulateAbsoluteMouse : '='
|
||||
|
||||
};
|
||||
|
||||
directive.controller = ['$scope', '$injector', '$element',
|
||||
function guacTiledClientsController($scope, $injector, $element) {
|
||||
|
||||
// Required services
|
||||
const $rootScope = $injector.get('$rootScope');
|
||||
|
||||
// Required types
|
||||
const ManagedClient = $injector.get('ManagedClient');
|
||||
const ManagedClientGroup = $injector.get('ManagedClientGroup');
|
||||
|
||||
/**
|
||||
* Returns the currently-focused ManagedClient. If there is no such
|
||||
* client, or multiple clients are focused, null is returned.
|
||||
*
|
||||
* @returns {ManagedClient}
|
||||
* The currently-focused client, or null if there are no focused
|
||||
* clients or if multiple clients are focused.
|
||||
*/
|
||||
$scope.getFocusedClient = function getFocusedClient() {
|
||||
|
||||
const managedClientGroup = $scope.clientGroup;
|
||||
if (managedClientGroup) {
|
||||
const focusedClients = _.filter(managedClientGroup.clients, client => client.clientProperties.focused);
|
||||
if (focusedClients.length === 1)
|
||||
return focusedClients[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
// Notify whenever identify of currently-focused client changes
|
||||
$scope.$watch('getFocusedClient()', function focusedClientChanged(focusedClient) {
|
||||
$rootScope.$broadcast('guacClientFocused', focusedClient);
|
||||
});
|
||||
|
||||
// Notify whenever arguments of currently-focused client changes
|
||||
$scope.$watch('getFocusedClient().arguments', function focusedClientParametersChanged() {
|
||||
$rootScope.$broadcast('guacClientArgumentsUpdated', $scope.getFocusedClient());
|
||||
}, true);
|
||||
|
||||
// Notify whenever protocol of currently-focused client changes
|
||||
$scope.$watch('getFocusedClient().protocol', function focusedClientParametersChanged() {
|
||||
$rootScope.$broadcast('guacClientProtocolUpdated', $scope.getFocusedClient());
|
||||
}, true);
|
||||
|
||||
/**
|
||||
* Returns a callback for guacClick that assigns or updates keyboard
|
||||
* focus to the given client, allowing that client to receive and
|
||||
* handle keyboard events. Multiple clients may have keyboard focus
|
||||
* simultaneously.
|
||||
*
|
||||
* @param {ManagedClient} client
|
||||
* The client that should receive keyboard focus.
|
||||
*
|
||||
* @return {guacClick~callback}
|
||||
* The callback that guacClient should invoke when the given client
|
||||
* has been clicked.
|
||||
*/
|
||||
$scope.getFocusAssignmentCallback = function getFocusAssignmentCallback(client) {
|
||||
return (shift, ctrl) => {
|
||||
|
||||
// Clear focus of all other clients if not selecting multiple
|
||||
if (!shift && !ctrl) {
|
||||
$scope.clientGroup.clients.forEach(client => {
|
||||
client.clientProperties.focused = false;
|
||||
});
|
||||
}
|
||||
|
||||
client.clientProperties.focused = true;
|
||||
|
||||
// Fill in any gaps if performing rectangular multi-selection
|
||||
// via shift-click
|
||||
if (shift) {
|
||||
|
||||
let minRow = $scope.clientGroup.rows - 1;
|
||||
let minColumn = $scope.clientGroup.columns - 1;
|
||||
let maxRow = 0;
|
||||
let maxColumn = 0;
|
||||
|
||||
// Determine extents of selected area
|
||||
ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => {
|
||||
if (client.clientProperties.focused) {
|
||||
minRow = Math.min(minRow, row);
|
||||
minColumn = Math.min(minColumn, column);
|
||||
maxRow = Math.max(maxRow, row);
|
||||
maxColumn = Math.max(maxColumn, column);
|
||||
}
|
||||
});
|
||||
|
||||
ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => {
|
||||
client.clientProperties.focused =
|
||||
row >= minRow
|
||||
&& row <= maxRow
|
||||
&& column >= minColumn
|
||||
&& column <= maxColumn;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.hasMultipleClients
|
||||
*/
|
||||
$scope.hasMultipleClients = ManagedClientGroup.hasMultipleClients;
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.getClientGrid
|
||||
*/
|
||||
$scope.getClientGrid = ManagedClientGroup.getClientGrid;
|
||||
|
||||
/**
|
||||
* @borrows ManagedClient.isShared
|
||||
*/
|
||||
$scope.isShared = ManagedClient.isShared;
|
||||
|
||||
}];
|
||||
|
||||
return directive;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive for displaying a group of Guacamole clients as a non-interactive
|
||||
* thumbnail of tiled client displays.
|
||||
*/
|
||||
angular.module('client').directive('guacTiledThumbnails', [function guacTiledThumbnails() {
|
||||
|
||||
const directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: 'app/client/templates/guacTiledThumbnails.html'
|
||||
};
|
||||
|
||||
directive.scope = {
|
||||
|
||||
/**
|
||||
* The group of clients to display as a thumbnail of tiled client
|
||||
* displays.
|
||||
*
|
||||
* @type ManagedClientGroup
|
||||
*/
|
||||
clientGroup : '='
|
||||
|
||||
};
|
||||
|
||||
directive.controller = ['$scope', '$injector', '$element',
|
||||
function guacTiledThumbnailsController($scope, $injector, $element) {
|
||||
|
||||
// Required types
|
||||
const ManagedClientGroup = $injector.get('ManagedClientGroup');
|
||||
|
||||
/**
|
||||
* The overall height of the thumbnail view of the tiled grid of
|
||||
* clients within the client group, in pixels. This value is
|
||||
* intentionally based off a snapshot of the current browser size at
|
||||
* the time the directive comes into existence to ensure the contents
|
||||
* of the thumbnail are familiar in appearance and aspect ratio.
|
||||
*/
|
||||
$scope.height = Math.min(window.innerHeight, 128);
|
||||
|
||||
/**
|
||||
* The overall width of the thumbnail view of the tiled grid of
|
||||
* clients within the client group, in pixels. This value is
|
||||
* intentionally based off a snapshot of the current browser size at
|
||||
* the time the directive comes into existence to ensure the contents
|
||||
* of the thumbnail are familiar in appearance and aspect ratio.
|
||||
*/
|
||||
$scope.width = window.innerWidth / window.innerHeight * $scope.height;
|
||||
|
||||
/**
|
||||
* @borrows ManagedClientGroup.getClientGrid
|
||||
*/
|
||||
$scope.getClientGrid = ManagedClientGroup.getClientGrid;
|
||||
|
||||
}];
|
||||
|
||||
return directive;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which provides a fullscreen environment for its content.
|
||||
*/
|
||||
angular.module('client').directive('guacViewport', [function guacViewport() {
|
||||
|
||||
return {
|
||||
// Element only
|
||||
restrict: 'E',
|
||||
scope: {},
|
||||
transclude: true,
|
||||
templateUrl: 'app/client/templates/guacViewport.html',
|
||||
controller: ['$scope', '$injector', '$element',
|
||||
function guacViewportController($scope, $injector, $element) {
|
||||
|
||||
// Required services
|
||||
var $window = $injector.get('$window');
|
||||
|
||||
/**
|
||||
* The fullscreen container element.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var element = $element.find('.viewport')[0];
|
||||
|
||||
/**
|
||||
* The width of the browser viewport when fitVisibleArea() was last
|
||||
* invoked, in pixels, or null if fitVisibleArea() has not yet been
|
||||
* called.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var lastViewportWidth = null;
|
||||
|
||||
/**
|
||||
* The height of the browser viewport when fitVisibleArea() was
|
||||
* last invoked, in pixels, or null if fitVisibleArea() has not yet
|
||||
* been called.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var lastViewportHeight = null;
|
||||
|
||||
/**
|
||||
* Resizes the container element inside the guacViewport such that
|
||||
* it exactly fits within the visible area, even if the browser has
|
||||
* been scrolled.
|
||||
*/
|
||||
var fitVisibleArea = function fitVisibleArea() {
|
||||
|
||||
// Calculate viewport dimensions (this is NOT necessarily the
|
||||
// same as 100vw and 100vh, 100%, etc., particularly when the
|
||||
// on-screen keyboard of a mobile device pops open)
|
||||
var viewportWidth = $window.innerWidth;
|
||||
var viewportHeight = $window.innerHeight;
|
||||
|
||||
// Adjust element width to fit exactly within visible area
|
||||
if (viewportWidth !== lastViewportWidth) {
|
||||
element.style.width = viewportWidth + 'px';
|
||||
lastViewportWidth = viewportWidth;
|
||||
}
|
||||
|
||||
// Adjust element height to fit exactly within visible area
|
||||
if (viewportHeight !== lastViewportHeight) {
|
||||
element.style.height = viewportHeight + 'px';
|
||||
lastViewportHeight = viewportHeight;
|
||||
}
|
||||
|
||||
// Scroll element such that its upper-left corner is exactly
|
||||
// within the viewport upper-left corner, if not already there
|
||||
if (element.scrollLeft || element.scrollTop) {
|
||||
$window.scrollTo(
|
||||
$window.pageXOffset + element.scrollLeft,
|
||||
$window.pageYOffset + element.scrollTop
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Fit container within visible region when window scrolls
|
||||
$window.addEventListener('scroll', fitVisibleArea);
|
||||
|
||||
// Poll every 10ms, in case scroll event does not fire
|
||||
var pollArea = $window.setInterval(fitVisibleArea, 10);
|
||||
|
||||
// Clean up on destruction
|
||||
$scope.$on('$destroy', function destroyViewport() {
|
||||
$window.removeEventListener('scroll', fitVisibleArea);
|
||||
$window.clearInterval(pollArea);
|
||||
});
|
||||
|
||||
}]
|
||||
};
|
||||
}]);
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which converts between human-readable zoom
|
||||
* percentage and display scale.
|
||||
*/
|
||||
angular.module('client').directive('guacZoomCtrl', function guacZoomCtrl() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
priority: 101,
|
||||
link: function(scope, element, attrs, ngModel) {
|
||||
|
||||
// Evaluate the ngChange attribute when the model
|
||||
// changes.
|
||||
ngModel.$viewChangeListeners.push(function() {
|
||||
scope.$eval(attrs.ngChange);
|
||||
});
|
||||
|
||||
// When pushing to the menu, mutiply by 100.
|
||||
ngModel.$formatters.push(function(value) {
|
||||
return Math.round(value * 100);
|
||||
});
|
||||
|
||||
// When parsing value from menu, divide by 100.
|
||||
ngModel.$parsers.push(function(value) {
|
||||
return Math.round(value) / 100;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A service for checking browser audio support.
|
||||
*/
|
||||
angular.module('client').factory('guacAudio', [function guacAudio() {
|
||||
|
||||
/**
|
||||
* Object describing the UI's level of audio support.
|
||||
*/
|
||||
return new (function() {
|
||||
|
||||
/**
|
||||
* Array of all supported audio mimetypes.
|
||||
*
|
||||
* @type String[]
|
||||
*/
|
||||
this.supported = Guacamole.AudioPlayer.getSupportedTypes();
|
||||
|
||||
})();
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A service for managing several active Guacamole clients.
|
||||
*/
|
||||
angular.module('client').factory('guacClientManager', ['$injector',
|
||||
function guacClientManager($injector) {
|
||||
|
||||
// Required types
|
||||
const ManagedClient = $injector.get('ManagedClient');
|
||||
const ManagedClientGroup = $injector.get('ManagedClientGroup');
|
||||
|
||||
// Required services
|
||||
const $window = $injector.get('$window');
|
||||
const sessionStorageFactory = $injector.get('sessionStorageFactory');
|
||||
|
||||
var service = {};
|
||||
|
||||
/**
|
||||
* Getter/setter which retrieves or sets the map of all active managed
|
||||
* clients. Each key is the ID of the connection used by that client.
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
var storedManagedClients = sessionStorageFactory.create({}, function destroyClientStorage() {
|
||||
|
||||
// Disconnect all clients when storage is destroyed
|
||||
service.clear();
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a map of all active managed clients. Each key is the ID of the
|
||||
* connection used by that client.
|
||||
*
|
||||
* @returns {Object.<String, ManagedClient>}
|
||||
* A map of all active managed clients.
|
||||
*/
|
||||
service.getManagedClients = function getManagedClients() {
|
||||
return storedManagedClients();
|
||||
};
|
||||
|
||||
/**
|
||||
* Getter/setter which retrieves or sets the array of all active managed
|
||||
* client groups.
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
const storedManagedClientGroups = sessionStorageFactory.create([], function destroyClientGroupStorage() {
|
||||
|
||||
// Disconnect all clients when storage is destroyed
|
||||
service.clear();
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns an array of all managed client groups.
|
||||
*
|
||||
* @returns {ManagedClientGroup[]>}
|
||||
* An array of all active managed client groups.
|
||||
*/
|
||||
service.getManagedClientGroups = function getManagedClientGroups() {
|
||||
return storedManagedClientGroups();
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the ManagedClient with the given ID from all
|
||||
* ManagedClientGroups, automatically adjusting the tile size of the
|
||||
* clients that remain in each group. All client groups that are empty
|
||||
* after the client is removed will also be removed.
|
||||
*
|
||||
* @param {string} id
|
||||
* The ID of the ManagedClient to remove.
|
||||
*/
|
||||
const ungroupManagedClient = function ungroupManagedClient(id) {
|
||||
|
||||
const managedClientGroups = storedManagedClientGroups();
|
||||
|
||||
// Remove client from all groups
|
||||
managedClientGroups.forEach(group => {
|
||||
const removed = _.remove(group.clients, client => (client.id === id));
|
||||
if (removed.length) {
|
||||
|
||||
// Reset focus state if client is being removed from a group
|
||||
// that isn't currently attached (focus may otherwise be
|
||||
// retained and result in a newly added connection unexpectedly
|
||||
// sharing focus)
|
||||
if (!group.attached)
|
||||
removed.forEach(client => { client.clientProperties.focused = false; });
|
||||
|
||||
// Recalculate group grid if number of clients is changing
|
||||
ManagedClientGroup.recalculateTiles(group);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// Remove any groups that are now empty
|
||||
_.remove(managedClientGroups, group => !group.clients.length);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the existing ManagedClient associated with the connection having
|
||||
* the given ID, if any. If no such a ManagedClient already exists, this
|
||||
* function has no effect.
|
||||
*
|
||||
* @param {String} id
|
||||
* The ID of the connection whose ManagedClient should be removed.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if an existing client was removed, false otherwise.
|
||||
*/
|
||||
service.removeManagedClient = function removeManagedClient(id) {
|
||||
|
||||
var managedClients = storedManagedClients();
|
||||
|
||||
// Remove client if it exists
|
||||
if (id in managedClients) {
|
||||
|
||||
// Pull client out of any containing groups
|
||||
ungroupManagedClient(id);
|
||||
|
||||
// Disconnect and remove
|
||||
managedClients[id].client.disconnect();
|
||||
delete managedClients[id];
|
||||
|
||||
// A client was removed
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
// No client was removed
|
||||
return false;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new ManagedClient associated with the connection having the
|
||||
* given ID. If such a ManagedClient already exists, it is disconnected and
|
||||
* replaced.
|
||||
*
|
||||
* @param {String} id
|
||||
* The ID of the connection whose ManagedClient should be retrieved.
|
||||
*
|
||||
* @returns {ManagedClient}
|
||||
* The ManagedClient associated with the connection having the given
|
||||
* ID.
|
||||
*/
|
||||
service.replaceManagedClient = function replaceManagedClient(id) {
|
||||
|
||||
const managedClients = storedManagedClients();
|
||||
const managedClientGroups = storedManagedClientGroups();
|
||||
|
||||
// Remove client if it exists
|
||||
if (id in managedClients) {
|
||||
|
||||
const hadFocus = managedClients[id].clientProperties.focused;
|
||||
managedClients[id].client.disconnect();
|
||||
delete managedClients[id];
|
||||
|
||||
// Remove client from all groups
|
||||
managedClientGroups.forEach(group => {
|
||||
|
||||
const index = _.findIndex(group.clients, client => (client.id === id));
|
||||
if (index === -1)
|
||||
return;
|
||||
|
||||
group.clients[index] = managedClients[id] = ManagedClient.getInstance(id);
|
||||
managedClients[id].clientProperties.focused = hadFocus;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return managedClients[id];
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the ManagedClient associated with the connection having the
|
||||
* given ID. If no such ManagedClient exists, a new ManagedClient is
|
||||
* created.
|
||||
*
|
||||
* @param {String} id
|
||||
* The ID of the connection whose ManagedClient should be retrieved.
|
||||
*
|
||||
* @returns {ManagedClient}
|
||||
* The ManagedClient associated with the connection having the given
|
||||
* ID.
|
||||
*/
|
||||
service.getManagedClient = function getManagedClient(id) {
|
||||
|
||||
var managedClients = storedManagedClients();
|
||||
|
||||
// Ensure any existing client is removed from its containing group
|
||||
// prior to being returned
|
||||
ungroupManagedClient(id);
|
||||
|
||||
// Create new managed client if it doesn't already exist
|
||||
if (!(id in managedClients))
|
||||
managedClients[id] = ManagedClient.getInstance(id);
|
||||
|
||||
// Return existing client
|
||||
return managedClients[id];
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the ManagedClientGroup having the given ID. If no such
|
||||
* ManagedClientGroup exists, a new ManagedClientGroup is created by
|
||||
* extracting the relevant connections from the ID.
|
||||
*
|
||||
* @param {String} id
|
||||
* The ID of the ManagedClientGroup to retrieve or create.
|
||||
*
|
||||
* @returns {ManagedClientGroup}
|
||||
* The ManagedClientGroup having the given ID.
|
||||
*/
|
||||
service.getManagedClientGroup = function getManagedClientGroup(id) {
|
||||
|
||||
const managedClientGroups = storedManagedClientGroups();
|
||||
const existingGroup = _.find(managedClientGroups, (group) => {
|
||||
return id === ManagedClientGroup.getIdentifier(group);
|
||||
});
|
||||
|
||||
// Prefer to return the existing group if it exactly matches
|
||||
if (existingGroup)
|
||||
return existingGroup;
|
||||
|
||||
const clients = [];
|
||||
const clientIds = ManagedClientGroup.getClientIdentifiers(id);
|
||||
|
||||
// Separate active clients by whether they should be displayed within
|
||||
// the current view
|
||||
clientIds.forEach(function groupClients(id) {
|
||||
clients.push(service.getManagedClient(id));
|
||||
});
|
||||
|
||||
const group = new ManagedClientGroup({
|
||||
clients : clients
|
||||
});
|
||||
|
||||
// Focus the first client if there are no clients focused
|
||||
ManagedClientGroup.verifyFocus(group);
|
||||
|
||||
managedClientGroups.push(group);
|
||||
return group;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the existing ManagedClientGroup having the given ID, if any,
|
||||
* disconnecting and removing all ManagedClients associated with that
|
||||
* group. If no such a ManagedClientGroup currently exists, this function
|
||||
* has no effect.
|
||||
*
|
||||
* @param {String} id
|
||||
* The ID of the ManagedClientGroup to remove.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* true if a ManagedClientGroup was removed, false otherwise.
|
||||
*/
|
||||
service.removeManagedClientGroup = function removeManagedClientGroup(id) {
|
||||
|
||||
const managedClients = storedManagedClients();
|
||||
const managedClientGroups = storedManagedClientGroups();
|
||||
|
||||
// Remove all matching groups (there SHOULD only be one)
|
||||
const removed = _.remove(managedClientGroups, (group) => ManagedClientGroup.getIdentifier(group) === id);
|
||||
|
||||
// Disconnect all clients associated with the removed group(s)
|
||||
removed.forEach((group) => {
|
||||
group.clients.forEach((client) => {
|
||||
|
||||
const id = client.id;
|
||||
if (managedClients[id]) {
|
||||
managedClients[id].client.disconnect();
|
||||
delete managedClients[id];
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return !!removed.length;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Disconnects and removes all currently-connected clients and client
|
||||
* groups.
|
||||
*/
|
||||
service.clear = function clear() {
|
||||
|
||||
var managedClients = storedManagedClients();
|
||||
|
||||
// Disconnect each managed client
|
||||
for (var id in managedClients)
|
||||
managedClients[id].client.disconnect();
|
||||
|
||||
// Clear managed clients and client groups
|
||||
storedManagedClients({});
|
||||
storedManagedClientGroups([]);
|
||||
|
||||
};
|
||||
|
||||
// Disconnect all clients when window is unloaded
|
||||
$window.addEventListener('unload', service.clear);
|
||||
|
||||
return service;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A service for providing true fullscreen and keyboard lock support.
|
||||
* Keyboard lock is currently only supported by Chromium based browsers
|
||||
* (Edge >= V79, Chrome >= V68 and Opera >= V55)
|
||||
*/
|
||||
angular.module('client').factory('guacFullscreen', ['$injector',
|
||||
function guacFullscreen($injector) {
|
||||
|
||||
var service = {};
|
||||
|
||||
// check is browser in true fullscreen mode
|
||||
service.isInFullscreenMode = function isInFullscreenMode() {
|
||||
return document.fullscreenElement;
|
||||
}
|
||||
|
||||
// set fullscreen mode
|
||||
service.setFullscreenMode = function setFullscreenMode(state) {
|
||||
if (document.fullscreenEnabled) {
|
||||
if (state && !service.isInFullscreenMode())
|
||||
document.documentElement.requestFullscreen().then(navigator.keyboard.lock());
|
||||
else if (!state && service.isInFullscreenMode())
|
||||
document.exitFullscreen().then(navigator.keyboard.unlock());
|
||||
}
|
||||
}
|
||||
|
||||
// toggles current fullscreen mode (off if on, on if off)
|
||||
service.toggleFullscreenMode = function toggleFullscreenMode() {
|
||||
if (!service.isInFullscreenMode())
|
||||
service.setFullscreenMode(true);
|
||||
else
|
||||
service.setFullscreenMode(false);
|
||||
}
|
||||
|
||||
return service;
|
||||
|
||||
}]);
|
||||
135
guacamole/src/main/frontend/src/app/client/services/guacImage.js
Normal file
135
guacamole/src/main/frontend/src/app/client/services/guacImage.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A service for checking browser image support.
|
||||
*/
|
||||
angular.module('client').factory('guacImage', ['$injector', function guacImage($injector) {
|
||||
|
||||
// Required services
|
||||
var $q = $injector.get('$q');
|
||||
|
||||
var service = {};
|
||||
|
||||
/**
|
||||
* Map of possibly-supported image mimetypes to corresponding test images
|
||||
* encoded with base64. If the image is correctly decoded, it will be a
|
||||
* single pixel (1x1) image.
|
||||
*
|
||||
* @type Object.<String, String>
|
||||
*/
|
||||
var testImages = {
|
||||
|
||||
/**
|
||||
* Test JPEG image, encoded as base64.
|
||||
*/
|
||||
'image/jpeg' :
|
||||
'/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoH'
|
||||
+ 'BwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQME'
|
||||
+ 'BAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU'
|
||||
+ 'FBQUFBQUFBQUFBQUFBT/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAA'
|
||||
+ 'AAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAA'
|
||||
+ 'AAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AVMH/2Q==',
|
||||
|
||||
/**
|
||||
* Test PNG image, encoded as base64.
|
||||
*/
|
||||
'image/png' :
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvI'
|
||||
+ 'AAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==',
|
||||
|
||||
/**
|
||||
* Test WebP image, encoded as base64.
|
||||
*/
|
||||
'image/webp' : 'UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=='
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Deferred which tracks the progress and ultimate result of all pending
|
||||
* image format tests.
|
||||
*
|
||||
* @type Deferred
|
||||
*/
|
||||
var deferredSupportedMimetypes = $q.defer();
|
||||
|
||||
/**
|
||||
* Array of all promises associated with pending image tests. Each image
|
||||
* test promise MUST be guaranteed to resolve and MUST NOT be rejected.
|
||||
*
|
||||
* @type Promise[]
|
||||
*/
|
||||
var pendingTests = [];
|
||||
|
||||
/**
|
||||
* The array of supported image formats. This will be gradually populated
|
||||
* by the various image tests that occur in the background, and will not be
|
||||
* fully populated until all promises within pendingTests are resolved.
|
||||
*
|
||||
* @type String[]
|
||||
*/
|
||||
var supported = [];
|
||||
|
||||
/**
|
||||
* Return a promise which resolves with to an array of image mimetypes
|
||||
* supported by the browser, once those mimetypes are known. The returned
|
||||
* promise is guaranteed to resolve successfully.
|
||||
*
|
||||
* @returns {Promise.<String[]>}
|
||||
* A promise which resolves with an array of image mimetypes supported
|
||||
* by the browser.
|
||||
*/
|
||||
service.getSupportedMimetypes = function getSupportedMimetypes() {
|
||||
return deferredSupportedMimetypes.promise;
|
||||
};
|
||||
|
||||
// Test each possibly-supported image
|
||||
angular.forEach(testImages, function testImageSupport(data, mimetype) {
|
||||
|
||||
// Add promise for current image test
|
||||
var imageTest = $q.defer();
|
||||
pendingTests.push(imageTest.promise);
|
||||
|
||||
// Attempt to load image
|
||||
var image = new Image();
|
||||
image.src = 'data:' + mimetype + ';base64,' + data;
|
||||
|
||||
// Store as supported depending on whether load was successful
|
||||
image.onload = image.onerror = function imageTestComplete() {
|
||||
|
||||
// Image format is supported if successfully decoded
|
||||
if (image.width === 1 && image.height === 1)
|
||||
supported.push(mimetype);
|
||||
|
||||
// Test is complete
|
||||
imageTest.resolve();
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
// When all image tests are complete, resolve promise with list of
|
||||
// supported formats
|
||||
$q.all(pendingTests).then(function imageTestsCompleted() {
|
||||
deferredSupportedMimetypes.resolve(supported);
|
||||
});
|
||||
|
||||
return service;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A wrapper around the angular-translate $translate service that offers a
|
||||
* convenient way to fall back to a default translation if the requested
|
||||
* translation is not available.
|
||||
*/
|
||||
angular.module('client').factory('guacTranslate', ['$injector', function guacTranslate($injector) {
|
||||
|
||||
// Required services
|
||||
const $q = $injector.get('$q');
|
||||
const $translate = $injector.get('$translate');
|
||||
|
||||
// Required types
|
||||
const TranslationResult = $injector.get('TranslationResult');
|
||||
|
||||
/**
|
||||
* Returns a promise that will be resolved with a TranslationResult containg either the
|
||||
* requested ID and message (if translated), or the default ID and message if translated,
|
||||
* or the literal value of `defaultTranslationId` for both the ID and message if neither
|
||||
* is translated.
|
||||
*
|
||||
* @param {String} translationId
|
||||
* The requested translation ID, which may or may not be translated.
|
||||
*
|
||||
* @param {Sting} defaultTranslationId
|
||||
* The translation ID that will be used if no translation is found for `translationId`.
|
||||
*
|
||||
* @returns {Promise.<TranslationResult>}
|
||||
* A promise which resolves with a TranslationResult containing the results from
|
||||
* the translation attempt.
|
||||
*/
|
||||
var translateWithFallback = function translateWithFallback(translationId, defaultTranslationId) {
|
||||
const deferredTranslation = $q.defer();
|
||||
|
||||
// Attempt to translate the requested translation ID
|
||||
$translate(translationId).then(
|
||||
|
||||
// If the requested translation is available, use that
|
||||
translation => deferredTranslation.resolve(new TranslationResult({
|
||||
id: translationId, message: translation
|
||||
})),
|
||||
|
||||
// Otherwise, try the default translation ID
|
||||
() => $translate(defaultTranslationId).then(
|
||||
|
||||
// Default translation worked, so use that
|
||||
defaultTranslation =>
|
||||
deferredTranslation.resolve(new TranslationResult({
|
||||
id: defaultTranslationId, message: defaultTranslation
|
||||
})),
|
||||
|
||||
// Neither translation is available; as a fallback, return default ID for both
|
||||
() => deferredTranslation.resolve(new TranslationResult({
|
||||
id: defaultTranslationId, message: defaultTranslationId
|
||||
})),
|
||||
)
|
||||
);
|
||||
|
||||
return deferredTranslation.promise;
|
||||
};
|
||||
|
||||
return translateWithFallback;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A service for checking browser video support.
|
||||
*/
|
||||
angular.module('client').factory('guacVideo', [function guacVideo() {
|
||||
|
||||
/**
|
||||
* Object describing the UI's level of video support.
|
||||
*/
|
||||
return new (function() {
|
||||
|
||||
/**
|
||||
* Array of all supported video mimetypes.
|
||||
*/
|
||||
this.supported = Guacamole.VideoPlayer.getSupportedTypes();
|
||||
|
||||
})();
|
||||
|
||||
}]);
|
||||
137
guacamole/src/main/frontend/src/app/client/styles/client.css
Normal file
137
guacamole/src/main/frontend/src/app/client/styles/client.css
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
body.client {
|
||||
background: black;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#preload {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.client-view {
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
font-size: 0px;
|
||||
|
||||
}
|
||||
|
||||
.client-view-content {
|
||||
|
||||
/* IE10 */
|
||||
display: -ms-flexbox;
|
||||
-ms-flex-align: stretch;
|
||||
-ms-flex-direction: column;
|
||||
-ms-flex-pack: end;
|
||||
|
||||
/* Ancient Mozilla */
|
||||
display: -moz-box;
|
||||
-moz-box-align: stretch;
|
||||
-moz-box-orient: vertical;
|
||||
-moz-box-pack: end;
|
||||
|
||||
/* Ancient WebKit */
|
||||
display: -webkit-box;
|
||||
-webkit-box-align: stretch;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-pack: end;
|
||||
|
||||
/* Old WebKit */
|
||||
display: -webkit-flex;
|
||||
-webkit-align-items: stretch;
|
||||
-webkit-flex-direction: column;
|
||||
-webkit-flex-pack: end;
|
||||
|
||||
/* W3C */
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
flex-pack: end;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
font-size: 12pt;
|
||||
|
||||
}
|
||||
|
||||
.client-view .client-body {
|
||||
-ms-flex: 1 1 auto;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.client-view .client-bottom {
|
||||
-ms-flex: 0 0 auto;
|
||||
-moz-box-flex: 0;
|
||||
-webkit-box-flex: 0;
|
||||
-webkit-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.client-view .client-body guac-tiled-clients {
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
}
|
||||
|
||||
.client .menu .header h2 {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.client .user-menu .menu-contents li a.disconnect {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1em;
|
||||
background-position: 0.75em center;
|
||||
padding-left: 2.5em;
|
||||
background-image: url('images/x.svg');
|
||||
}
|
||||
|
||||
.client .drop-pending .display {
|
||||
background: #3161a9;
|
||||
}
|
||||
|
||||
.client .drop-pending .display > *{
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#guac-menu .header h2.connection-select-menu {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.connection-select-menu {
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.connection-select-menu .menu-dropdown {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.connection-select-menu .menu-dropdown .menu-contents {
|
||||
font-weight: normal;
|
||||
font-size: 0.8em;
|
||||
right: auto;
|
||||
left: 0;
|
||||
max-width: 100vw;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.connection-select-menu .menu-dropdown .menu-contents .filter input {
|
||||
border-bottom: 1px solid rgba(0,0,0,0.125);
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.connection-select-menu .menu-dropdown .menu-contents .filter {
|
||||
margin-bottom: 0.5em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.connection-select-menu .menu-dropdown .menu-contents .group-list .caption {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.connection-select-menu .menu-dropdown .menu-contents .caption .connection,
|
||||
.connection-select-menu .menu-dropdown .menu-contents .caption .connection-group {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#connection-warning {
|
||||
|
||||
position: absolute;
|
||||
right: 0.25em;
|
||||
bottom: 0.25em;
|
||||
z-index: 20;
|
||||
|
||||
width: 3in;
|
||||
max-width: 100%;
|
||||
min-height: 1em;
|
||||
|
||||
border-left: 2em solid #FA0;
|
||||
box-shadow: 1px 1px 2px rgba(0,0,0,0.25);
|
||||
background: #FFE;
|
||||
padding: 0.5em 0.75em;
|
||||
font-size: .8em;
|
||||
|
||||
}
|
||||
|
||||
#connection-warning::before {
|
||||
|
||||
content: ' ';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -2em;
|
||||
top: 0;
|
||||
|
||||
width: 1.25em;
|
||||
height: 100%;
|
||||
margin: 0 0.375em;
|
||||
|
||||
background: url('images/warning.svg');
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.software-cursor {
|
||||
cursor: url('images/mouse/blank.gif'),url('images/mouse/blank.cur'),default;
|
||||
overflow: hidden;
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.guac-error .software-cursor {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
div.main {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
font-size: 0px;
|
||||
}
|
||||
|
||||
div.displayOuter {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: table;
|
||||
}
|
||||
|
||||
div.displayMiddle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.display {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div.display * {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.display > * {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* Hide directory contents by default */
|
||||
|
||||
.file-browser .directory > .children {
|
||||
padding-left: 1em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-browser .list-item .caption {
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.file-browser .list-item.focused .caption {
|
||||
border: 1px dotted rgba(0, 0, 0, 0.5);
|
||||
background: rgba(204, 221, 170, 0.5);
|
||||
}
|
||||
|
||||
/* Directory / file icons */
|
||||
|
||||
.file-browser .normal-file > .caption .icon {
|
||||
background-image: url('images/file.svg');
|
||||
}
|
||||
|
||||
.file-browser .directory > .caption .icon {
|
||||
background-image: url('images/folder-closed.svg');
|
||||
}
|
||||
|
||||
.file-browser .directory.previous > .caption .icon {
|
||||
background-image: url('images/folder-up.svg');
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#file-transfer-dialog {
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20;
|
||||
|
||||
font-size: 0.8em;
|
||||
|
||||
width: 4in;
|
||||
max-width: 100%;
|
||||
max-height: 3in;
|
||||
|
||||
}
|
||||
|
||||
#file-transfer-dialog .transfer-manager {
|
||||
|
||||
/* IE10 */
|
||||
display: -ms-flexbox;
|
||||
-ms-flex-align: stretch;
|
||||
-ms-flex-direction: column;
|
||||
|
||||
/* Ancient Mozilla */
|
||||
display: -moz-box;
|
||||
-moz-box-align: stretch;
|
||||
-moz-box-orient: vertical;
|
||||
|
||||
/* Ancient WebKit */
|
||||
display: -webkit-box;
|
||||
-webkit-box-align: stretch;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
/* Old WebKit */
|
||||
display: -webkit-flex;
|
||||
-webkit-align-items: stretch;
|
||||
-webkit-flex-direction: column;
|
||||
|
||||
/* W3C */
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
max-width: inherit;
|
||||
max-height: inherit;
|
||||
|
||||
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25);
|
||||
|
||||
}
|
||||
|
||||
#file-transfer-dialog .transfer-manager .header {
|
||||
-ms-flex: 0 0 auto;
|
||||
-moz-box-flex: 0;
|
||||
-webkit-box-flex: 0;
|
||||
-webkit-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
#file-transfer-dialog .transfer-manager .transfer-manager-body {
|
||||
|
||||
-ms-flex: 1 1 auto;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
|
||||
overflow: auto;
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Shrink maximum height if viewport is too small for default 3in dialog.
|
||||
*/
|
||||
@media all and (max-height: 3in) {
|
||||
|
||||
#file-transfer-dialog {
|
||||
max-height: 1.5in;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* If viewport is too small for even the 1.5in dialog, fit all available space.
|
||||
*/
|
||||
@media all and (max-height: 1.5in) {
|
||||
|
||||
#file-transfer-dialog {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#file-transfer-dialog .transfer-manager {
|
||||
position: absolute;
|
||||
left: 0.5em;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
bottom: 0.5em;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#filesystem-menu .header h2 {
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#filesystem-menu .header {
|
||||
-ms-flex-align: center;
|
||||
-moz-box-align: center;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#filesystem-menu .menu-body {
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
#filesystem-menu .header.breadcrumbs {
|
||||
display: block;
|
||||
background: rgba(0,0,0,0.0125);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
box-shadow: none;
|
||||
margin-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
#filesystem-menu .header.breadcrumbs .breadcrumb {
|
||||
display: inline-block;
|
||||
padding: 0.5em;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#filesystem-menu .header.breadcrumbs .breadcrumb:hover {
|
||||
background-color: #CDA;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#filesystem-menu .header.breadcrumbs .breadcrumb.root {
|
||||
background-size: 1.5em 1.5em;
|
||||
-moz-background-size: 1.5em 1.5em;
|
||||
-webkit-background-size: 1.5em 1.5em;
|
||||
-khtml-background-size: 1.5em 1.5em;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-image: url('images/drive.svg');
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
170
guacamole/src/main/frontend/src/app/client/styles/guac-menu.css
Normal file
170
guacamole/src/main/frontend/src/app/client/styles/guac-menu.css
Normal file
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#guac-menu .content {
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
/* IE10 */
|
||||
display: -ms-flexbox;
|
||||
-ms-flex-align: stretch;
|
||||
-ms-flex-direction: column;
|
||||
|
||||
/* Ancient Mozilla */
|
||||
display: -moz-box;
|
||||
-moz-box-align: stretch;
|
||||
-moz-box-orient: vertical;
|
||||
|
||||
/* Ancient WebKit */
|
||||
display: -webkit-box;
|
||||
-webkit-box-align: stretch;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
/* Old WebKit */
|
||||
display: -webkit-flex;
|
||||
-webkit-align-items: stretch;
|
||||
-webkit-flex-direction: column;
|
||||
|
||||
/* W3C */
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
#guac-menu .content > * {
|
||||
|
||||
margin: 0;
|
||||
|
||||
-ms-flex: 0 0 auto;
|
||||
-moz-box-flex: 0;
|
||||
-webkit-box-flex: 0;
|
||||
-webkit-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
|
||||
}
|
||||
|
||||
#guac-menu .content > * + * {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#guac-menu .header h2 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#guac-menu #mouse-settings .choice {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#guac-menu #mouse-settings .choice .figure {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 75%;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
#guac-menu #keyboard-settings .caption {
|
||||
font-size: 0.9em;
|
||||
margin-left: 2em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
#guac-menu #mouse-settings .figure .caption {
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#guac-menu #mouse-settings .figure img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
#guac-menu #keyboard-settings .figure {
|
||||
float: right;
|
||||
max-width: 30%;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
#guac-menu #keyboard-settings .figure img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#guac-menu #zoom-settings {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#guac-menu #devices .device {
|
||||
|
||||
padding: 1em;
|
||||
border: 1px solid rgba(0, 0, 0, 0.125);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
|
||||
padding-left: 3.5em;
|
||||
background-size: 1.5em 1.5em;
|
||||
-moz-background-size: 1.5em 1.5em;
|
||||
-webkit-background-size: 1.5em 1.5em;
|
||||
-khtml-background-size: 1.5em 1.5em;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-position: 1em center;
|
||||
|
||||
}
|
||||
|
||||
#guac-menu #devices .device:hover {
|
||||
cursor: pointer;
|
||||
border-color: black;
|
||||
}
|
||||
|
||||
#guac-menu #devices .device.filesystem {
|
||||
background-image: url('images/drive.svg');
|
||||
}
|
||||
|
||||
#guac-menu #share-links {
|
||||
|
||||
padding: 1em;
|
||||
border: 1px solid rgba(0, 0, 0, 0.125);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
|
||||
font-size: 0.8em;
|
||||
|
||||
}
|
||||
|
||||
#guac-menu #share-links h3 {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#guac-menu #share-links th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#guac-menu #share-links a[href] {
|
||||
|
||||
display: block;
|
||||
padding: 0 1em;
|
||||
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.keyboard-container {
|
||||
|
||||
text-align: center;
|
||||
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
border-top: 1px solid black;
|
||||
background: #222;
|
||||
opacity: 0.85;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
}
|
||||
146
guacamole/src/main/frontend/src/app/client/styles/menu.css
Normal file
146
guacamole/src/main/frontend/src/app/client/styles/menu.css
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.menu {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
width: 480px;
|
||||
background: #EEE;
|
||||
box-shadow: inset -1px 0 2px white, 1px 0 2px black;
|
||||
z-index: 100;
|
||||
-webkit-transition: left 0.125s, opacity 0.125s;
|
||||
-moz-transition: left 0.125s, opacity 0.125s;
|
||||
-ms-transition: left 0.125s, opacity 0.125s;
|
||||
-o-transition: left 0.125s, opacity 0.125s;
|
||||
transition: left 0.125s, opacity 0.125s;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
|
||||
/* IE10 */
|
||||
display: -ms-flexbox;
|
||||
-ms-flex-align: stretch;
|
||||
-ms-flex-direction: column;
|
||||
|
||||
/* Ancient Mozilla */
|
||||
display: -moz-box;
|
||||
-moz-box-align: stretch;
|
||||
-moz-box-orient: vertical;
|
||||
|
||||
/* Ancient WebKit */
|
||||
display: -webkit-box;
|
||||
-webkit-box-align: stretch;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
/* Old WebKit */
|
||||
display: -webkit-flex;
|
||||
-webkit-align-items: stretch;
|
||||
-webkit-flex-direction: column;
|
||||
|
||||
/* W3C */
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
|
||||
.menu-content .header {
|
||||
|
||||
-ms-flex: 0 0 auto;
|
||||
-moz-box-flex: 0;
|
||||
-webkit-box-flex: 0;
|
||||
-webkit-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
|
||||
margin-bottom: 0;
|
||||
|
||||
}
|
||||
|
||||
.menu-body {
|
||||
|
||||
-ms-flex: 1 1 auto;
|
||||
-moz-box-flex: 1;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
|
||||
padding: 1em;
|
||||
overflow: auto;
|
||||
|
||||
/* IE10 */
|
||||
display: -ms-flexbox;
|
||||
-ms-flex-align: stretch;
|
||||
-ms-flex-direction: column;
|
||||
|
||||
/* Ancient Mozilla */
|
||||
display: -moz-box;
|
||||
-moz-box-align: stretch;
|
||||
-moz-box-orient: vertical;
|
||||
|
||||
/* Ancient WebKit */
|
||||
display: -webkit-box;
|
||||
-webkit-box-align: stretch;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
/* Old WebKit */
|
||||
display: -webkit-flex;
|
||||
-webkit-align-items: stretch;
|
||||
-webkit-flex-direction: column;
|
||||
|
||||
/* W3C */
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
.menu-body > * {
|
||||
-ms-flex: 0 0 auto;
|
||||
-moz-box-flex: 0;
|
||||
-webkit-box-flex: 0;
|
||||
-webkit-flex: 0 0 auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.menu-section h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.menu-section ~ .menu-section h3 {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.menu,
|
||||
.menu.closed {
|
||||
left: -480px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.menu.open {
|
||||
left: 0px;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.client-status-modal {
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: none;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
|
||||
}
|
||||
|
||||
.client-status-modal.shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.client-status-modal guac-modal {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.client-status-modal .notification {
|
||||
background: rgba(40, 40, 40, 0.75);
|
||||
color: white;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.client-status-modal .notification.error {
|
||||
background: rgba(112, 9, 8, 0.75)
|
||||
}
|
||||
|
||||
.client-status-modal .notification .title-bar {
|
||||
display: none
|
||||
}
|
||||
|
||||
.client-status-modal .notification .button {
|
||||
background: transparent;
|
||||
border: 2px solid white;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.client-status-modal .notification .button:hover {
|
||||
text-decoration: underline;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.client-status-modal .notification .button:active {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.client-status-modal .notification .parameters {
|
||||
width: 100%;
|
||||
max-width: 5in;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.client-status-modal .notification .parameters h3,
|
||||
.client-status-modal .notification .parameters .password-field .toggle-password {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.client-status-modal .notification .parameters input[type=email],
|
||||
.client-status-modal .notification .parameters input[type=number],
|
||||
.client-status-modal .notification .parameters input[type=password],
|
||||
.client-status-modal .notification .parameters input[type=text],
|
||||
.client-status-modal .notification .parameters textarea {
|
||||
background: transparent;
|
||||
border: 2px solid white;
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.share-menu {
|
||||
|
||||
/* IE10 */
|
||||
display: -ms-flexbox;
|
||||
-ms-flex-align: stretch;
|
||||
-ms-flex-direction: row;
|
||||
|
||||
/* Ancient Mozilla */
|
||||
display: -moz-box;
|
||||
-moz-box-align: stretch;
|
||||
-moz-box-orient: horizontal;
|
||||
|
||||
/* Ancient WebKit */
|
||||
display: -webkit-box;
|
||||
-webkit-box-align: stretch;
|
||||
-webkit-box-orient: horizontal;
|
||||
|
||||
/* Old WebKit */
|
||||
display: -webkit-flex;
|
||||
-webkit-align-items: stretch;
|
||||
-webkit-flex-direction: row;
|
||||
|
||||
/* W3C */
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: row;
|
||||
|
||||
}
|
||||
|
||||
.share-menu .menu-dropdown .menu-title {
|
||||
|
||||
padding-left: 2em;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1em;
|
||||
background-position: 0.5em center;
|
||||
background-image: url('images/share.svg');
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
div.thumbnail-main {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
font-size: 0px;
|
||||
}
|
||||
|
||||
.thumbnail-main .display {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Overall tiled grid layout.
|
||||
*/
|
||||
|
||||
.tiled-client-grid {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tiled-client-grid,
|
||||
.tiled-client-grid .tiled-client-row,
|
||||
.tiled-client-grid .tiled-client-cell,
|
||||
.tiled-client-grid .client-tile {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tiled-client-grid {
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tiled-client-grid .tiled-client-row {
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/*
|
||||
* Rendering of individual clients within tiles.
|
||||
*/
|
||||
|
||||
.tiled-client-grid .client-tile {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile .client-tile-header {
|
||||
|
||||
display: -webkit-box;
|
||||
|
||||
display: -webkit-flex;
|
||||
|
||||
display: -ms-flexbox;
|
||||
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
|
||||
margin: 0;
|
||||
background: #444;
|
||||
padding: 0 0.25em;
|
||||
font-size: 0.8em;
|
||||
color: white;
|
||||
z-index: 30;
|
||||
min-height: 1.5em;
|
||||
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile.focused .client-tile-header {
|
||||
background-color: #3161a9;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile .client-tile-header > * {
|
||||
-webkit-box-flex: 0;
|
||||
-webkit-flex: 0;
|
||||
-ms-flex: 0;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile .client-tile-header .client-tile-name {
|
||||
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
|
||||
padding: 0 0.5em;
|
||||
margin-bottom: -0.125em;
|
||||
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile .main {
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile-disconnect,
|
||||
.tiled-client-grid .client-tile-shared-indicator {
|
||||
max-height: 1em;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile-shared-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tiled-client-grid .shared .client-tile-shared-indicator {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count {
|
||||
|
||||
visibility: hidden;
|
||||
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
|
||||
border-radius: 0.25em;
|
||||
padding: 0.125em 0.75em;
|
||||
margin: 0.5em;
|
||||
|
||||
background: #055;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 0.8em;
|
||||
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count::before {
|
||||
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
|
||||
margin-bottom: -0.2em;
|
||||
padding-right: 0.25em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
background: center / contain no-repeat url('images/user-icons/guac-user-white.svg');
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count .client-user-count-users,
|
||||
.tiled-client-grid .client-user-count .client-user-count-messages {
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: 0.5em;
|
||||
list-style: none;
|
||||
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count .client-user-count-users,
|
||||
.tiled-client-grid .client-user-count .client-user-count-message {
|
||||
border-radius: 0.25em;
|
||||
background: black;
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count .client-user-count-message {
|
||||
white-space: nowrap;
|
||||
animation: 1s linear 3s fadeout;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile-header .client-user-count {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
background: black;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-tile-header .client-user-count::before {
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
|
||||
.tiled-client-grid .joined .client-user-count {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count .client-user-count-users {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count:hover .client-user-count-users {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count .client-user-count-user::after {
|
||||
content: ', ';
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count .client-user-count-user:last-child::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count .client-user-count-user {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count .client-user-count-user.anonymous {
|
||||
font-style: italic;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count .client-user-count-users {
|
||||
width: 256px;
|
||||
max-width: 75vw;
|
||||
white-space: normal;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.tiled-client-grid .client-user-count .client-user-count-users::before {
|
||||
|
||||
content: ' ';
|
||||
display: block;
|
||||
|
||||
position: absolute;
|
||||
right: 0.5em;
|
||||
top: -0.5em;
|
||||
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
background: black;
|
||||
border: 1px solid #333;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
transform: rotate(45deg);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.transfer-manager {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.transfer-manager .header h2 {
|
||||
font-size: 1em;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.transfer-manager .header {
|
||||
margin: 0;
|
||||
-ms-flex-align: center;
|
||||
-moz-box-align: center;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.transfer-manager h3 {
|
||||
margin: 0.25em;
|
||||
font-size: 1em;
|
||||
margin-bottom: 0;
|
||||
opacity: 0.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.transfer-manager .transfers {
|
||||
display: table;
|
||||
padding: 0.25em;
|
||||
width: 100%;
|
||||
}
|
||||
132
guacamole/src/main/frontend/src/app/client/styles/transfer.css
Normal file
132
guacamole/src/main/frontend/src/app/client/styles/transfer.css
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.transfer {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.transfer .transfer-status {
|
||||
display: table-cell;
|
||||
padding: 0.25em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.transfer .text {
|
||||
display: table-cell;
|
||||
text-align: right;
|
||||
padding: 0.25em
|
||||
}
|
||||
|
||||
.transfer .filename {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
padding: 0.125em;
|
||||
}
|
||||
|
||||
@keyframes transfer-progress {
|
||||
from {background-position: 0px 0px;}
|
||||
to {background-position: 64px 0px;}
|
||||
}
|
||||
|
||||
@-webkit-keyframes transfer-progress {
|
||||
from {background-position: 0px 0px;}
|
||||
to {background-position: 64px 0px;}
|
||||
}
|
||||
|
||||
.transfer .progress {
|
||||
|
||||
width: 100%;
|
||||
padding: 0.25em;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.25;
|
||||
|
||||
}
|
||||
|
||||
.transfer.in-progress .progress {
|
||||
|
||||
background-color: #EEE;
|
||||
background-image: url('images/progress.svg');
|
||||
|
||||
background-size: 16px 16px;
|
||||
-moz-background-size: 16px 16px;
|
||||
-webkit-background-size: 16px 16px;
|
||||
-khtml-background-size: 16px 16px;
|
||||
|
||||
animation-name: transfer-progress;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
|
||||
-webkit-animation-name: transfer-progress;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
|
||||
}
|
||||
|
||||
.transfer .progress .bar {
|
||||
display: none;
|
||||
background: #A3D655;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.transfer.in-progress .progress .bar {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.transfer.savable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.transfer.savable .filename {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.transfer.error {
|
||||
background: #FDD;
|
||||
}
|
||||
|
||||
.transfer.error .text,
|
||||
.transfer.error .progress .bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.transfer .error-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.transfer.error .error-text {
|
||||
display: block;
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.viewport {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
75
guacamole/src/main/frontend/src/app/client/styles/zoom.css
Normal file
75
guacamole/src/main/frontend/src/app/client/styles/zoom.css
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.client-zoom .client-zoom-out,
|
||||
.client-zoom .client-zoom-in,
|
||||
.client-zoom .client-zoom-state {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.client-zoom .client-zoom-out,
|
||||
.client-zoom .client-zoom-in {
|
||||
max-width: 3em;
|
||||
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2em;
|
||||
margin: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.client-zoom .client-zoom-out img,
|
||||
.client-zoom .client-zoom-in img {
|
||||
width: 100%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.client-zoom .client-zoom-out:hover,
|
||||
.client-zoom .client-zoom-in:hover {
|
||||
border: 1px solid rgba(0, 0, 0, 1);
|
||||
background: #CDA;
|
||||
}
|
||||
|
||||
.client-zoom .client-zoom-out:hover img,
|
||||
.client-zoom .client-zoom-in:hover img {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.client-zoom .client-zoom-state {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.client-zoom .client-zoom-autofit {
|
||||
text-align: left;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.client-zoom .client-zoom-state input {
|
||||
width: 2em;
|
||||
font-size: 1em;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-color: rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
.client-zoom .client-zoom-state input::-webkit-inner-spin-button,
|
||||
.client-zoom .client-zoom-state input::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
228
guacamole/src/main/frontend/src/app/client/templates/client.html
Normal file
228
guacamole/src/main/frontend/src/app/client/templates/client.html
Normal file
@@ -0,0 +1,228 @@
|
||||
|
||||
<guac-viewport>
|
||||
|
||||
<!-- Client view -->
|
||||
<div class="client-view">
|
||||
<div class="client-view-content">
|
||||
|
||||
<!-- Central portion of view -->
|
||||
<div class="client-body" guac-touch-drag="menuDrag">
|
||||
|
||||
<!-- All connections in current display -->
|
||||
<guac-tiled-clients
|
||||
on-close="closeClientTile($client)"
|
||||
client-group="clientGroup"
|
||||
emulate-absolute-mouse="menu.emulateAbsoluteMouse">
|
||||
</guac-tiled-clients>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Bottom portion of view -->
|
||||
<div class="client-bottom">
|
||||
|
||||
<!-- Text input -->
|
||||
<div class="text-input-container" ng-if="showTextInput">
|
||||
<guac-text-input></guac-text-input>
|
||||
</div>
|
||||
|
||||
<!-- On-screen keyboard -->
|
||||
<div class="keyboard-container" ng-if="showOSK">
|
||||
<guac-osk layout="'CLIENT.URL_OSK_LAYOUT' | translate"></guac-osk>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File transfers -->
|
||||
<div id="file-transfer-dialog" ng-show="hasTransfers()">
|
||||
<guac-file-transfer-manager client-group="clientGroup"></guac-file-transfer-manager>
|
||||
</div>
|
||||
|
||||
<!-- Connection stability warning -->
|
||||
<div id="connection-warning" ng-show="isConnectionUnstable()">
|
||||
{{'CLIENT.TEXT_CLIENT_STATUS_UNSTABLE' | translate}}
|
||||
</div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu" ng-class="{open: menu.shown}" id="guac-menu">
|
||||
<div class="menu-content" ng-if="menu.shown" guac-touch-drag="menuDrag">
|
||||
|
||||
<!-- Stationary header -->
|
||||
<div class="header">
|
||||
<h2 ng-hide="rootConnectionGroups">{{ getName(clientGroup) }}</h2>
|
||||
<h2 class="connection-select-menu" ng-show="rootConnectionGroups">
|
||||
<guac-menu menu-title="getName(clientGroup)" interactive="true">
|
||||
<div class="all-connections">
|
||||
<guac-group-list-filter connection-groups="rootConnectionGroups"
|
||||
filtered-connection-groups="filteredRootConnectionGroups"
|
||||
placeholder="'CLIENT.FIELD_PLACEHOLDER_FILTER' | translate"
|
||||
connection-properties="filteredConnectionProperties"
|
||||
connection-group-properties="filteredConnectionGroupProperties"></guac-group-list-filter>
|
||||
<guac-group-list
|
||||
connection-groups="filteredRootConnectionGroups"
|
||||
context="connectionListContext"
|
||||
templates="{
|
||||
'connection' : 'app/client/templates/connection.html',
|
||||
'connection-group' : 'app/client/templates/connectionGroup.html'
|
||||
}"
|
||||
page-size="10"></guac-group-list>
|
||||
</div>
|
||||
</guac-menu>
|
||||
</h2>
|
||||
<div class="share-menu" ng-show="canShareConnection()">
|
||||
<guac-menu menu-title="'CLIENT.ACTION_SHARE' | translate">
|
||||
<ul ng-repeat="sharingProfile in sharingProfiles">
|
||||
<li><a ng-click="share(sharingProfile)">{{sharingProfile.name}}</a></li>
|
||||
</ul>
|
||||
</guac-menu>
|
||||
</div>
|
||||
<guac-user-menu local-actions="clientMenuActions"></guac-user-menu>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable body -->
|
||||
<div class="menu-body" guac-touch-drag="visibleMenuDrag" guac-scroll="menu.scrollState">
|
||||
|
||||
<!-- Connection sharing -->
|
||||
<div class="menu-section" id="share-links" ng-show="isShared()">
|
||||
<div class="content">
|
||||
<h3>{{'CLIENT.INFO_CONNECTION_SHARED' | translate}}</h3>
|
||||
<p class="description"
|
||||
translate="CLIENT.HELP_SHARE_LINK"
|
||||
translate-values="{LINKS : getShareLinkCount()}"></p>
|
||||
<table>
|
||||
<tr ng-repeat="link in focusedClient.shareLinks | toArray | orderBy: value.name">
|
||||
<th>{{link.value.name}}</th>
|
||||
<td><a href="{{link.value.href}}" target="_blank">{{link.value.href}}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clipboard -->
|
||||
<div class="menu-section" id="clipboard-settings">
|
||||
<h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3>
|
||||
<div class="content">
|
||||
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
|
||||
<guac-clipboard></guac-clipboard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Devices -->
|
||||
<div class="menu-section" id="devices" ng-if="focusedClient.filesystems.length">
|
||||
<h3>{{'CLIENT.SECTION_HEADER_DEVICES' | translate}}</h3>
|
||||
<div class="content">
|
||||
<div class="device filesystem" ng-repeat="filesystem in focusedClient.filesystems" ng-click="showFilesystemMenu(filesystem)">
|
||||
{{filesystem.name}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connection parameters which may be modified while the connection is open -->
|
||||
<div class="menu-section connection-parameters" id="connection-settings" ng-if="focusedClient.protocol">
|
||||
<guac-form namespace="getProtocolNamespace(focusedClient.protocol)"
|
||||
content="focusedClient.forms"
|
||||
model="menu.connectionParameters"
|
||||
client="focusedClient"
|
||||
model-only="true"></guac-form>
|
||||
</div>
|
||||
|
||||
<!-- Input method -->
|
||||
<div class="menu-section" id="keyboard-settings">
|
||||
<h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3>
|
||||
<div class="content">
|
||||
|
||||
<!-- No IME -->
|
||||
<div class="choice">
|
||||
<label><input id="ime-none" name="input-method" ng-change="closeMenu()" ng-model="menu.inputMethod" type="radio" value="none"> {{'CLIENT.NAME_INPUT_METHOD_NONE' | translate}}</label>
|
||||
<p class="caption"><label for="ime-none">{{'CLIENT.HELP_INPUT_METHOD_NONE' | translate}}</label></p>
|
||||
</div>
|
||||
|
||||
<!-- Text input -->
|
||||
<div class="choice">
|
||||
<div class="figure"><label for="ime-text"><img src="images/settings/tablet-keys.svg" alt=""></label></div>
|
||||
<label><input id="ime-text" name="input-method" ng-change="closeMenu()" ng-model="menu.inputMethod" type="radio" value="text"> {{'CLIENT.NAME_INPUT_METHOD_TEXT' | translate}}</label>
|
||||
<p class="caption"><label for="ime-text">{{'CLIENT.HELP_INPUT_METHOD_TEXT' | translate}} </label></p>
|
||||
</div>
|
||||
|
||||
<!-- Guac OSK -->
|
||||
<div class="choice">
|
||||
<label><input id="ime-osk" name="input-method" ng-change="closeMenu()" ng-model="menu.inputMethod" type="radio" value="osk"> {{'CLIENT.NAME_INPUT_METHOD_OSK' | translate}}</label>
|
||||
<p class="caption"><label for="ime-osk">{{'CLIENT.HELP_INPUT_METHOD_OSK' | translate}}</label></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mouse mode -->
|
||||
<div class="menu-section" id="mouse-settings">
|
||||
<h3>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h3>
|
||||
<div class="content">
|
||||
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
|
||||
|
||||
<!-- Touchscreen -->
|
||||
<div class="choice">
|
||||
<input name="mouse-mode" ng-change="closeMenu()" ng-model="menu.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute">
|
||||
<div class="figure">
|
||||
<label for="absolute"><img src="images/settings/touchscreen.svg" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"></label>
|
||||
<p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Touchpad -->
|
||||
<div class="choice">
|
||||
<input name="mouse-mode" ng-change="closeMenu()" ng-model="menu.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative">
|
||||
<div class="figure">
|
||||
<label for="relative"><img src="images/settings/touchpad.svg" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"></label>
|
||||
<p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display options -->
|
||||
<div class="menu-section" id="display-settings" ng-if="focusedClient">
|
||||
<h3>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h3>
|
||||
<div class="content">
|
||||
<div id="zoom-settings">
|
||||
<guac-client-zoom client="focusedClient"></guac-client-zoom>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filesystem menu -->
|
||||
<div id="filesystem-menu" class="menu" ng-class="{open: isFilesystemMenuShown()}">
|
||||
<div class="menu-content">
|
||||
|
||||
<!-- Stationary header -->
|
||||
<div class="header">
|
||||
<h2>{{filesystemMenuContents.name}}</h2>
|
||||
<button class="upload button" guac-upload="uploadFiles">{{'CLIENT.ACTION_UPLOAD_FILES' | translate}}</button>
|
||||
<button class="back" ng-click="hideFilesystemMenu()">{{'CLIENT.ACTION_NAVIGATE_BACK' | translate}}</button>
|
||||
</div>
|
||||
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="header breadcrumbs"><div
|
||||
class="breadcrumb root"
|
||||
ng-click="changeDirectory(filesystemMenuContents, filesystemMenuContents.root)"></div><div
|
||||
class="breadcrumb"
|
||||
ng-repeat="file in getPath(filesystemMenuContents.currentDirectory)"
|
||||
ng-click="changeDirectory(filesystemMenuContents, file)">{{file.name}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable body -->
|
||||
<div class="menu-body">
|
||||
<guac-file-browser client="client" filesystem="filesystemMenuContents"></guac-file-browser>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</guac-viewport>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="connection-select-menu-connection connection">
|
||||
<input type="checkbox"
|
||||
ng-model="context.attachedClients[item.getClientIdentifier()]"
|
||||
ng-change="context.updateAttachedClients(item.getClientIdentifier())">
|
||||
<a ng-href="{{ item.getClientURL() }}">
|
||||
<div class="icon type" ng-class="item.protocol"></div>
|
||||
<span class="name">{{item.name}}</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="connection-select-menu-connection-group connection-group">
|
||||
<input type="checkbox"
|
||||
ng-show="item.balancing"
|
||||
ng-model="context.attachedClients[item.getClientIdentifier()]"
|
||||
ng-change="context.updateAttachedClients(item.getClientIdentifier())">
|
||||
<a ng-href="{{ item.getClientURL() }}">
|
||||
<div ng-show="item.balancing" class="icon type balancer"></div>
|
||||
<span class="name">{{item.name}}</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="file-browser-file list-item">
|
||||
|
||||
<!-- Filename and icon -->
|
||||
<div class="caption">
|
||||
<div class="icon"></div>
|
||||
{{::name}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="client-main main"
|
||||
ng-class="{ 'drop-pending': dropPending }"
|
||||
guac-resize="mainElementResized"
|
||||
guac-touch-drag="clientDrag"
|
||||
guac-touch-pinch="clientPinch">
|
||||
|
||||
<!-- Display -->
|
||||
<div class="displayOuter">
|
||||
|
||||
<div class="displayMiddle">
|
||||
<div class="display software-cursor">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="client-status-modal" ng-class="{ shown: status }">
|
||||
<guac-modal>
|
||||
<guac-notification notification="status"></guac-notification>
|
||||
</guac-modal>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="client-panel"
|
||||
ng-class="{ 'has-clients': hasClientGroups(), 'hidden' : panelHidden() }">
|
||||
|
||||
<!-- Toggle panel visibility -->
|
||||
<div class="client-panel-handle" ng-click="togglePanel()"></div>
|
||||
|
||||
<!-- List of connection thumbnails -->
|
||||
<ul class="client-panel-connection-list">
|
||||
<li ng-repeat="clientGroup in clientGroups | orderBy: '-lastUsed'"
|
||||
ng-if="!clientGroup.attached"
|
||||
ng-class="{ 'needs-attention' : hasStatusUpdate(clientGroup) }"
|
||||
class="client-panel-connection">
|
||||
|
||||
<!-- Close connection -->
|
||||
<button class="close-other-connection" ng-click="disconnect(clientGroup)">
|
||||
<img ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
|
||||
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
|
||||
src="images/x.svg">
|
||||
</button>
|
||||
|
||||
<!-- Thumbnail -->
|
||||
<a href="#/client/{{ getIdentifier(clientGroup) }}">
|
||||
<guac-tiled-thumbnails client-group="clientGroup"></guac-tiled-thumbnails>
|
||||
<div class="name">{{ getTitle(clientGroup) }}</div>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="client-user-count" title="{{ instance }}">
|
||||
<span class="client-user-count-value">{{ client.userCount }}</span>
|
||||
<ul class="client-user-count-messages"></ul>
|
||||
<ul class="client-user-count-users">
|
||||
<li class="client-user-count-user"
|
||||
ng-repeat="user in userCounts | toArray | orderBy: key"
|
||||
ng-class="{ anonymous : isAnonymous(user.key) }"
|
||||
translate="{{ getUserCountTranslationKey(user.key) }}"
|
||||
translate-values="{ USERNAME : user.key, COUNT : user.value }"></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="client-zoom">
|
||||
<div class="client-zoom-editor">
|
||||
<div ng-click="zoomOut()" class="client-zoom-out"><img src="images/settings/zoom-out.svg" alt="-"></div>
|
||||
<div class="client-zoom-state">
|
||||
<input type="number" guac-zoom-ctrl
|
||||
ng-model="client.clientProperties.scale"
|
||||
ng-model-options="{ updateOn: 'blur submit' }"
|
||||
ng-change="zoomSet()">%
|
||||
</div>
|
||||
<div ng-click="zoomIn()" class="client-zoom-in"><img src="images/settings/zoom-in.svg" alt="+"></div>
|
||||
</div>
|
||||
<div class="client-zoom-autofit">
|
||||
<label><input ng-model="client.clientProperties.autoFit"
|
||||
ng-change="changeAutoFit()"
|
||||
ng-disabled="autoFitDisabled()" type="checkbox" id="auto-fit">
|
||||
{{'CLIENT.TEXT_ZOOM_AUTO_FIT' | translate}}</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="file-browser">
|
||||
|
||||
<!-- Current directory contents -->
|
||||
<div class="current-directory-contents"></div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="transfer" ng-class="{'in-progress': isInProgress(), 'savable': isSavable(), 'error': hasError()}" ng-click="save()">
|
||||
|
||||
<!-- Overall status of transfer -->
|
||||
<div class="transfer-status">
|
||||
|
||||
<!-- Filename and progress bar -->
|
||||
<div class="filename">
|
||||
<div class="progress"><div ng-style="{'width': getPercentDone() + '%'}" class="bar"></div></div>
|
||||
{{transfer.filename}}
|
||||
</div>
|
||||
|
||||
<!-- Error text -->
|
||||
<p class="error-text">{{translatedErrorMessage}}</p>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Progress/status text -->
|
||||
<div class="text"
|
||||
translate="CLIENT.TEXT_FILE_TRANSFER_PROGRESS"
|
||||
translate-values="{PROGRESS: getProgressValue(), UNIT: getProgressUnit()}"></div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="transfer-manager">
|
||||
|
||||
<!-- File transfer manager header -->
|
||||
<div class="header">
|
||||
<h2>{{'CLIENT.SECTION_HEADER_FILE_TRANSFERS' | translate}}</h2>
|
||||
<button ng-click="clearCompletedTransfers()">{{'CLIENT.ACTION_CLEAR_COMPLETED_TRANSFERS' | translate}}</button>
|
||||
</div>
|
||||
|
||||
<!-- Sent/received files -->
|
||||
<div class="transfer-manager-body">
|
||||
<div class="transfer-manager-body-section" ng-repeat="client in clientGroup.clients" ng-show="hasTransfers(client)">
|
||||
<h3 ng-show="hasMultipleClients(clientGroup)">{{ client.name }}</h3>
|
||||
<div class="transfers">
|
||||
<guac-file-transfer
|
||||
transfer="upload"
|
||||
ng-repeat="upload in client.uploads">
|
||||
</guac-file-transfer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="thumbnail-main" guac-resize="updateDisplayScale">
|
||||
|
||||
<!-- Display -->
|
||||
<div class="displayOuter">
|
||||
<div class="displayMiddle">
|
||||
<div class="display">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
<div class="tiled-client-grid">
|
||||
<div class="tiled-client-row" ng-repeat="clientRow in getClientGrid(clientGroup)">
|
||||
<div class="tiled-client-cell" ng-repeat="client in clientRow">
|
||||
|
||||
<div class="client-tile" ng-if="client"
|
||||
ng-class="{
|
||||
'focused' : client.clientProperties.focused,
|
||||
'shared' : isShared(client),
|
||||
'joined' : client.userCount
|
||||
}"
|
||||
guac-click="getFocusAssignmentCallback(client)">
|
||||
<h3 class="client-tile-header" ng-if="hasMultipleClients(clientGroup)">
|
||||
<img class="client-tile-shared-indicator" src="images/share-white.svg">
|
||||
<span class="client-tile-name">{{ client.title }}</span>
|
||||
<guac-client-user-count client="client"></guac-client-user-count>
|
||||
<img ng-click="onClose({ '$client' : client })"
|
||||
class="client-tile-disconnect"
|
||||
ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
|
||||
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
|
||||
src="images/x.svg">
|
||||
</h3>
|
||||
<guac-client client="client" emulate-absolute-mouse="emulateAbsoluteMouse"></guac-client>
|
||||
|
||||
<!-- Client-specific status/error dialog -->
|
||||
<guac-client-notification client="client"></guac-client-notification>
|
||||
|
||||
<guac-client-user-count client="client" ng-if="!hasMultipleClients(clientGroup)"></guac-client-user-count>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="tiled-client-grid" ng-style="{
|
||||
'width' : width + 'px',
|
||||
'height' : height + 'px',
|
||||
}">
|
||||
<div class="tiled-client-row" ng-repeat="clientRow in getClientGrid(clientGroup)">
|
||||
<div class="tiled-client-cell" ng-repeat="client in clientRow">
|
||||
|
||||
<div class="client-tile" ng-if="client">
|
||||
<guac-thumbnail client="client"></guac-thumbnail>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,2 @@
|
||||
<div class="viewport" ng-transclude>
|
||||
</div>
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A service for generating new guacClient properties objects.
|
||||
*/
|
||||
angular.module('client').factory('ClientProperties', ['$injector', function defineClientProperties($injector) {
|
||||
|
||||
/**
|
||||
* Object used for interacting with a guacClient directive.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ClientProperties|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ClientProperties.
|
||||
*/
|
||||
var ClientProperties = function ClientProperties(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* Whether the display should be scaled automatically to fit within the
|
||||
* available space.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
this.autoFit = template.autoFit || true;
|
||||
|
||||
/**
|
||||
* The current scale. If autoFit is true, the effect of setting this
|
||||
* value is undefined.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.scale = template.scale || 1;
|
||||
|
||||
/**
|
||||
* The minimum scale value.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.minScale = template.minScale || 1;
|
||||
|
||||
/**
|
||||
* The maximum scale value.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.maxScale = template.maxScale || 3;
|
||||
|
||||
/**
|
||||
* Whether this client should receive keyboard events.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
this.focused = template.focused || false;
|
||||
|
||||
/**
|
||||
* The relative Y coordinate of the scroll offset of the display within
|
||||
* the client element.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.scrollTop = template.scrollTop || 0;
|
||||
|
||||
/**
|
||||
* The relative X coordinate of the scroll offset of the display within
|
||||
* the client element.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.scrollLeft = template.scrollLeft || 0;
|
||||
|
||||
};
|
||||
|
||||
return ClientProperties;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ManagedArgument class used by ManagedClient.
|
||||
*/
|
||||
angular.module('client').factory('ManagedArgument', ['$q', function defineManagedArgument($q) {
|
||||
|
||||
/**
|
||||
* Object which represents an argument (connection parameter) which may be
|
||||
* changed by the user while the connection is open.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedArgument|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedArgument.
|
||||
*/
|
||||
var ManagedArgument = function ManagedArgument(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The name of the connection parameter.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.name = template.name;
|
||||
|
||||
/**
|
||||
* The current value of the connection parameter.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.value = template.value;
|
||||
|
||||
/**
|
||||
* A valid, open output stream which may be used to apply a new value
|
||||
* to the connection parameter.
|
||||
*
|
||||
* @type {Guacamole.OutputStream}
|
||||
*/
|
||||
this.stream = template.stream;
|
||||
|
||||
/**
|
||||
* True if this argument has been modified in the webapp, but yet to
|
||||
* be confirmed by guacd, or false in any other case. A pending
|
||||
* argument cannot be modified again, and must be recreated before
|
||||
* editing is enabled again.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.pending = false;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests editable access to a given connection parameter, returning a
|
||||
* promise which is resolved with a ManagedArgument instance that provides
|
||||
* such access if the parameter is indeed editable.
|
||||
*
|
||||
* @param {ManagedClient} managedClient
|
||||
* The ManagedClient instance associated with the connection for which
|
||||
* an editable version of the connection parameter is being retrieved.
|
||||
*
|
||||
* @param {String} name
|
||||
* The name of the connection parameter.
|
||||
*
|
||||
* @param {String} value
|
||||
* The current value of the connection parameter, as received from a
|
||||
* prior, inbound "argv" stream.
|
||||
*
|
||||
* @returns {Promise.<ManagedArgument>}
|
||||
* A promise which is resolved with the new ManagedArgument instance
|
||||
* once the requested parameter has been verified as editable.
|
||||
*/
|
||||
ManagedArgument.getInstance = function getInstance(managedClient, name, value) {
|
||||
|
||||
var deferred = $q.defer();
|
||||
|
||||
// Create internal, fully-populated instance of ManagedArgument, to be
|
||||
// returned only once mutability of the associated connection parameter
|
||||
// has been verified
|
||||
var managedArgument = new ManagedArgument({
|
||||
name : name,
|
||||
value : value,
|
||||
stream : managedClient.client.createArgumentValueStream('text/plain', name)
|
||||
});
|
||||
|
||||
// The connection parameter is editable only if a successful "ack" is
|
||||
// received
|
||||
managedArgument.stream.onack = function ackReceived(status) {
|
||||
if (status.isError())
|
||||
deferred.reject(status);
|
||||
else
|
||||
deferred.resolve(managedArgument);
|
||||
};
|
||||
|
||||
return deferred.promise;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the given editable argument (connection parameter) to the given
|
||||
* value, updating the behavior of the associated connection in real-time.
|
||||
* If successful, the ManagedArgument provided cannot be used for future
|
||||
* calls to setValue() and will be read-only until replaced with a new
|
||||
* instance. This function only has an effect if the new parameter value
|
||||
* is different from the current value.
|
||||
*
|
||||
* @param {ManagedArgument} managedArgument
|
||||
* The ManagedArgument instance associated with the connection
|
||||
* parameter being modified.
|
||||
*
|
||||
* @param {String} value
|
||||
* The new value to assign to the connection parameter.
|
||||
*/
|
||||
ManagedArgument.setValue = function setValue(managedArgument, value) {
|
||||
|
||||
// Stream new value only if value has changed and a change is not
|
||||
// already pending
|
||||
if (!managedArgument.pending && value !== managedArgument.value) {
|
||||
|
||||
var writer = new Guacamole.StringWriter(managedArgument.stream);
|
||||
writer.sendText(value);
|
||||
writer.sendEnd();
|
||||
|
||||
// ManagedArgument instance is no longer usable
|
||||
managedArgument.pending = true;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return ManagedArgument;
|
||||
|
||||
}]);
|
||||
1147
guacamole/src/main/frontend/src/app/client/types/ManagedClient.js
Normal file
1147
guacamole/src/main/frontend/src/app/client/types/ManagedClient.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ManagedClientGroup class used by the guacClientManager service.
|
||||
*/
|
||||
angular.module('client').factory('ManagedClientGroup', ['$injector', function defineManagedClientGroup($injector) {
|
||||
|
||||
/**
|
||||
* Object which serves as a grouping of ManagedClients. Each
|
||||
* ManagedClientGroup may be attached, detached, and reattached dynamically
|
||||
* from different client views, with its contents automatically displayed
|
||||
* in a tiled arrangment if needed.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedClientGroup|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedClientGroup.
|
||||
*/
|
||||
const ManagedClientGroup = function ManagedClientGroup(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The time that this group was last brought to the foreground of
|
||||
* the current tab, as the number of milliseconds elapsed since
|
||||
* midnight of January 1, 1970 UTC. If the group has not yet been
|
||||
* viewed, this will be 0.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.lastUsed = template.lastUsed || 0;
|
||||
|
||||
/**
|
||||
* Whether this ManagedClientGroup is currently attached to the client
|
||||
* interface (true) or is running in the background (false).
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.attached = template.attached || false;
|
||||
|
||||
/**
|
||||
* The clients that should be displayed within the client interface
|
||||
* when this group is attached.
|
||||
*
|
||||
* @type {ManagedClient[]}
|
||||
* @default []
|
||||
*/
|
||||
this.clients = template.clients || [];
|
||||
|
||||
/**
|
||||
* The number of rows that should be used when arranging the clients
|
||||
* within this group in a grid. By default, this value is automatically
|
||||
* calculated from the number of clients.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
this.rows = template.rows || ManagedClientGroup.getRows(this);
|
||||
|
||||
/**
|
||||
* The number of columns that should be used when arranging the clients
|
||||
* within this group in a grid. By default, this value is automatically
|
||||
* calculated from the number of clients.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
this.columns = template.columns || ManagedClientGroup.getColumns(this);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the number of rows and columns stored within the given
|
||||
* ManagedClientGroup such that the clients within the group are evenly
|
||||
* distributed. This function should be called whenever the size of a
|
||||
* group changes.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup that should be updated.
|
||||
*/
|
||||
ManagedClientGroup.recalculateTiles = function recalculateTiles(group) {
|
||||
|
||||
const recalculated = new ManagedClientGroup({
|
||||
clients : group.clients
|
||||
});
|
||||
|
||||
group.rows = recalculated.rows;
|
||||
group.columns = recalculated.columns;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the unique ID representing the given ManagedClientGroup or set
|
||||
* of client IDs. The ID of a ManagedClientGroup consists simply of the
|
||||
* IDs of all its ManagedClients, separated by periods.
|
||||
*
|
||||
* @param {ManagedClientGroup|string[]} group
|
||||
* The ManagedClientGroup or array of client IDs to determine the
|
||||
* ManagedClientGroup ID of.
|
||||
*
|
||||
* @returns {string}
|
||||
* The unique ID representing the given ManagedClientGroup, or the
|
||||
* unique ID that would represent a ManagedClientGroup containing the
|
||||
* clients with the given IDs.
|
||||
*/
|
||||
ManagedClientGroup.getIdentifier = function getIdentifier(group) {
|
||||
|
||||
if (!_.isArray(group))
|
||||
group = _.map(group.clients, client => client.id);
|
||||
|
||||
return group.join('.');
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of client identifiers for all clients contained within
|
||||
* the given ManagedClientGroup. Order of the identifiers is preserved
|
||||
* with respect to the order of the clients within the group.
|
||||
*
|
||||
* @param {ManagedClientGroup|string} group
|
||||
* The ManagedClientGroup to retrieve the client identifiers from,
|
||||
* or its ID.
|
||||
*
|
||||
* @returns {string[]}
|
||||
* The client identifiers of all clients contained within the given
|
||||
* ManagedClientGroup.
|
||||
*/
|
||||
ManagedClientGroup.getClientIdentifiers = function getClientIdentifiers(group) {
|
||||
|
||||
if (_.isString(group))
|
||||
return group.split(/\./);
|
||||
|
||||
return group.clients.map(client => client.id);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the number of columns that should be used to evenly arrange
|
||||
* all provided clients in a tiled grid.
|
||||
*
|
||||
* @returns {Number}
|
||||
* The number of columns that should be used for the grid of
|
||||
* clients.
|
||||
*/
|
||||
ManagedClientGroup.getColumns = function getColumns(group) {
|
||||
|
||||
if (!group.clients.length)
|
||||
return 0;
|
||||
|
||||
return Math.ceil(Math.sqrt(group.clients.length));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the number of rows that should be used to evenly arrange all
|
||||
* provided clients in a tiled grid.
|
||||
*
|
||||
* @returns {Number}
|
||||
* The number of rows that should be used for the grid of clients.
|
||||
*/
|
||||
ManagedClientGroup.getRows = function getRows(group) {
|
||||
|
||||
if (!group.clients.length)
|
||||
return 0;
|
||||
|
||||
return Math.ceil(group.clients.length / ManagedClientGroup.getColumns(group));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the title which should be displayed as the page title if the
|
||||
* given client group is attached to the interface.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup to determine the title of.
|
||||
*
|
||||
* @returns {string}
|
||||
* The title of the given ManagedClientGroup.
|
||||
*/
|
||||
ManagedClientGroup.getTitle = function getTitle(group) {
|
||||
|
||||
// Use client-specific title if only one client
|
||||
if (group.clients.length === 1)
|
||||
return group.clients[0].title;
|
||||
|
||||
// With multiple clients, somehow combining multiple page titles would
|
||||
// be confusing. Instead, use the combined names.
|
||||
return ManagedClientGroup.getName(group);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the combined names of all clients within the given
|
||||
* ManagedClientGroup, as determined by the names of the associated
|
||||
* connections or connection groups.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup to determine the name of.
|
||||
*
|
||||
* @returns {string}
|
||||
* The combined names of all clients within the given
|
||||
* ManagedClientGroup.
|
||||
*/
|
||||
ManagedClientGroup.getName = function getName(group) {
|
||||
|
||||
// Generate a name from ONLY the focused clients, unless there are no
|
||||
// focused clients
|
||||
let relevantClients = _.filter(group.clients, client => client.clientProperties.focused);
|
||||
if (!relevantClients.length)
|
||||
relevantClients = group.clients;
|
||||
|
||||
return _.filter(relevantClients, (client => !!client.name)).map(client => client.name).join(', ') || '...';
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* A callback that is invoked for a ManagedClient within a ManagedClientGroup.
|
||||
*
|
||||
* @callback ManagedClientGroup~clientCallback
|
||||
* @param {ManagedClient} client
|
||||
* The relevant ManagedClient.
|
||||
*
|
||||
* @param {number} row
|
||||
* The row number of the client within the tiled grid, where 0 is the
|
||||
* first row.
|
||||
*
|
||||
* @param {number} column
|
||||
* The column number of the client within the tiled grid, where 0 is
|
||||
* the first column.
|
||||
*
|
||||
* @param {number} index
|
||||
* The index of the client within the relevant
|
||||
* {@link ManagedClientGroup#clients} array.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Loops through each of the clients associated with the given
|
||||
* ManagedClientGroup, invoking the given callback for each client.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup to loop through.
|
||||
*
|
||||
* @param {ManagedClientGroup~clientCallback} callback
|
||||
* The callback to invoke for each of the clients within the given
|
||||
* ManagedClientGroup.
|
||||
*/
|
||||
ManagedClientGroup.forEach = function forEach(group, callback) {
|
||||
let current = 0;
|
||||
for (let row = 0; row < group.rows; row++) {
|
||||
for (let column = 0; column < group.columns; column++) {
|
||||
|
||||
callback(group.clients[current], row, column, current);
|
||||
current++;
|
||||
|
||||
if (current >= group.clients.length)
|
||||
return;
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the given ManagedClientGroup contains more than one
|
||||
* client.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup to test.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true if two or more clients are currently present in the given
|
||||
* group, false otherwise.
|
||||
*/
|
||||
ManagedClientGroup.hasMultipleClients = function hasMultipleClients(group) {
|
||||
return group && group.clients.length > 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a two-dimensional array of all ManagedClients within the given
|
||||
* group, arranged in the grid defined by {@link ManagedClientGroup#rows}
|
||||
* and {@link ManagedClientGroup#columns}. If any grid cell lacks a
|
||||
* corresponding client (because the number of clients does not divide
|
||||
* evenly into a grid), that cell will be null.
|
||||
*
|
||||
* For the sake of AngularJS scope watches, the results of calling this
|
||||
* function are cached and will always favor modifying an existing array
|
||||
* over creating a new array, even for nested arrays.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The ManagedClientGroup defining the tiled grid arrangement of
|
||||
* ManagedClients.
|
||||
*
|
||||
* @returns {ManagedClient[][]}
|
||||
* A two-dimensional array of all ManagedClients within the given
|
||||
* group.
|
||||
*/
|
||||
ManagedClientGroup.getClientGrid = function getClientGrid(group) {
|
||||
|
||||
let index = 0;
|
||||
|
||||
// Operate on cached copy of grid
|
||||
const clientGrid = group._grid || (group._grid = []);
|
||||
|
||||
// Delete any rows in excess of the required size
|
||||
clientGrid.splice(group.rows);
|
||||
|
||||
for (let row = 0; row < group.rows; row++) {
|
||||
|
||||
// Prefer to use existing column arrays, deleting any columns in
|
||||
// excess of the required size
|
||||
const currentRow = clientGrid[row] || (clientGrid[row] = []);
|
||||
currentRow.splice(group.columns);
|
||||
|
||||
for (let column = 0; column < group.columns; column++) {
|
||||
currentRow[column] = group.clients[index++] || null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return clientGrid;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies that focus is assigned to at least one client in the given
|
||||
* group. If no client has focus, focus is assigned to the first client in
|
||||
* the group.
|
||||
*
|
||||
* @param {ManagedClientGroup} group
|
||||
* The group to verify.
|
||||
*/
|
||||
ManagedClientGroup.verifyFocus = function verifyFocus(group) {
|
||||
|
||||
// Focus the first client if there are no clients focused
|
||||
if (group.clients.length >= 1 && _.findIndex(group.clients, client => client.clientProperties.focused) === -1) {
|
||||
group.clients[0].clientProperties.focused = true;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return ManagedClientGroup;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ManagedClient class used by the guacClientManager service.
|
||||
*/
|
||||
angular.module('client').factory('ManagedClientState', [function defineManagedClientState() {
|
||||
|
||||
/**
|
||||
* Object which represents the state of a Guacamole client and its tunnel,
|
||||
* including any error conditions.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedClientState|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedClientState.
|
||||
*/
|
||||
var ManagedClientState = function ManagedClientState(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The current connection state. Valid values are described by
|
||||
* ManagedClientState.ConnectionState.
|
||||
*
|
||||
* @type String
|
||||
* @default ManagedClientState.ConnectionState.IDLE
|
||||
*/
|
||||
this.connectionState = template.connectionState || ManagedClientState.ConnectionState.IDLE;
|
||||
|
||||
/**
|
||||
* Whether the network connection used by the tunnel seems unstable. If
|
||||
* the network connection is unstable, the remote desktop connection
|
||||
* may perform poorly or disconnect.
|
||||
*
|
||||
* @type Boolean
|
||||
* @default false
|
||||
*/
|
||||
this.tunnelUnstable = template.tunnelUnstable || false;
|
||||
|
||||
/**
|
||||
* The status code of the current error condition, if connectionState
|
||||
* is CLIENT_ERROR or TUNNEL_ERROR. For all other connectionState
|
||||
* values, this will be @link{Guacamole.Status.Code.SUCCESS}.
|
||||
*
|
||||
* @type Number
|
||||
* @default Guacamole.Status.Code.SUCCESS
|
||||
*/
|
||||
this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Valid connection state strings. Each state string is associated with a
|
||||
* specific state of a Guacamole connection.
|
||||
*/
|
||||
ManagedClientState.ConnectionState = {
|
||||
|
||||
/**
|
||||
* The Guacamole connection has not yet been attempted.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
IDLE : "IDLE",
|
||||
|
||||
/**
|
||||
* The Guacamole connection is being established.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
CONNECTING : "CONNECTING",
|
||||
|
||||
/**
|
||||
* The Guacamole connection has been successfully established, and the
|
||||
* client is now waiting for receipt of initial graphical data.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
WAITING : "WAITING",
|
||||
|
||||
/**
|
||||
* The Guacamole connection has been successfully established, and
|
||||
* initial graphical data has been received.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
CONNECTED : "CONNECTED",
|
||||
|
||||
/**
|
||||
* The Guacamole connection has terminated successfully. No errors are
|
||||
* indicated.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
DISCONNECTED : "DISCONNECTED",
|
||||
|
||||
/**
|
||||
* The Guacamole connection has terminated due to an error reported by
|
||||
* the client. The associated error code is stored in statusCode.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
CLIENT_ERROR : "CLIENT_ERROR",
|
||||
|
||||
/**
|
||||
* The Guacamole connection has terminated due to an error reported by
|
||||
* the tunnel. The associated error code is stored in statusCode.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
TUNNEL_ERROR : "TUNNEL_ERROR"
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the current client state and, if given, the associated status code.
|
||||
* If an error is already represented, this function has no effect. If the
|
||||
* client state was previously marked as unstable, that flag is implicitly
|
||||
* cleared.
|
||||
*
|
||||
* @param {ManagedClientState} clientState
|
||||
* The ManagedClientState to update.
|
||||
*
|
||||
* @param {String} connectionState
|
||||
* The connection state to assign to the given ManagedClientState, as
|
||||
* listed within ManagedClientState.ConnectionState.
|
||||
*
|
||||
* @param {Number} [statusCode]
|
||||
* The status code to assign to the given ManagedClientState, if any,
|
||||
* as listed within Guacamole.Status.Code. If no status code is
|
||||
* specified, the status code of the ManagedClientState is not touched.
|
||||
*/
|
||||
ManagedClientState.setConnectionState = function(clientState, connectionState, statusCode) {
|
||||
|
||||
// Do not set state after an error is registered
|
||||
if (clientState.connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR
|
||||
|| clientState.connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR)
|
||||
return;
|
||||
|
||||
// Update connection state
|
||||
clientState.connectionState = connectionState;
|
||||
clientState.tunnelUnstable = false;
|
||||
|
||||
// Set status code, if given
|
||||
if (statusCode)
|
||||
clientState.statusCode = statusCode;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the given client state, setting whether the underlying tunnel
|
||||
* is currently unstable. An unstable tunnel is not necessarily
|
||||
* disconnected, but appears to be misbehaving and may be disconnected.
|
||||
*
|
||||
* @param {ManagedClientState} clientState
|
||||
* The ManagedClientState to update.
|
||||
*
|
||||
* @param {Boolean} unstable
|
||||
* Whether the underlying tunnel of the connection currently appears
|
||||
* unstable.
|
||||
*/
|
||||
ManagedClientState.setTunnelUnstable = function setTunnelUnstable(clientState, unstable) {
|
||||
clientState.tunnelUnstable = unstable;
|
||||
};
|
||||
|
||||
return ManagedClientState;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ManagedClientThumbnail class used by ManagedClient.
|
||||
*/
|
||||
angular.module('client').factory('ManagedClientThumbnail', [function defineManagedClientThumbnail() {
|
||||
|
||||
/**
|
||||
* Object which represents a thumbnail of the Guacamole client display,
|
||||
* along with the time that the thumbnail was generated.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedClientThumbnail|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedClientThumbnail.
|
||||
*/
|
||||
var ManagedClientThumbnail = function ManagedClientThumbnail(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The time that this thumbnail was generated, as the number of
|
||||
* milliseconds elapsed since midnight of January 1, 1970 UTC.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.timestamp = template.timestamp;
|
||||
|
||||
/**
|
||||
* The thumbnail of the Guacamole client display.
|
||||
*
|
||||
* @type HTMLCanvasElement
|
||||
*/
|
||||
this.canvas = template.canvas;
|
||||
|
||||
};
|
||||
|
||||
return ManagedClientThumbnail;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ManagedDisplay class used by the guacClientManager service.
|
||||
*/
|
||||
angular.module('client').factory('ManagedDisplay', ['$rootScope',
|
||||
function defineManagedDisplay($rootScope) {
|
||||
|
||||
/**
|
||||
* Object which serves as a surrogate interface, encapsulating a Guacamole
|
||||
* display while it is active, allowing it to be detached and reattached
|
||||
* from different client views.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedDisplay|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedDisplay.
|
||||
*/
|
||||
var ManagedDisplay = function ManagedDisplay(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The underlying Guacamole display.
|
||||
*
|
||||
* @type Guacamole.Display
|
||||
*/
|
||||
this.display = template.display;
|
||||
|
||||
/**
|
||||
* The current size of the Guacamole display.
|
||||
*
|
||||
* @type ManagedDisplay.Dimensions
|
||||
*/
|
||||
this.size = new ManagedDisplay.Dimensions(template.size);
|
||||
|
||||
/**
|
||||
* The current mouse cursor, if any.
|
||||
*
|
||||
* @type ManagedDisplay.Cursor
|
||||
*/
|
||||
this.cursor = template.cursor;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Object which represents the size of the Guacamole display.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedDisplay.Dimensions|Object} template
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedDisplay.Dimensions.
|
||||
*/
|
||||
ManagedDisplay.Dimensions = function Dimensions(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The current width of the Guacamole display, in pixels.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.width = template.width || 0;
|
||||
|
||||
/**
|
||||
* The current width of the Guacamole display, in pixels.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.height = template.height || 0;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Object which represents a mouse cursor used by the Guacamole display.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedDisplay.Cursor|Object} template
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedDisplay.Cursor.
|
||||
*/
|
||||
ManagedDisplay.Cursor = function Cursor(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The actual mouse cursor image.
|
||||
*
|
||||
* @type HTMLCanvasElement
|
||||
*/
|
||||
this.canvas = template.canvas;
|
||||
|
||||
/**
|
||||
* The X coordinate of the cursor hotspot.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.x = template.x;
|
||||
|
||||
/**
|
||||
* The Y coordinate of the cursor hotspot.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.y = template.y;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new ManagedDisplay which represents the current state of the
|
||||
* given Guacamole display.
|
||||
*
|
||||
* @param {Guacamole.Display} display
|
||||
* The Guacamole display to represent. Changes to this display will
|
||||
* affect this ManagedDisplay.
|
||||
*
|
||||
* @returns {ManagedDisplay}
|
||||
* A new ManagedDisplay which represents the current state of the
|
||||
* given Guacamole display.
|
||||
*/
|
||||
ManagedDisplay.getInstance = function getInstance(display) {
|
||||
|
||||
var managedDisplay = new ManagedDisplay({
|
||||
display : display
|
||||
});
|
||||
|
||||
// Store changes to display size
|
||||
display.onresize = function setClientSize() {
|
||||
$rootScope.$apply(function updateClientSize() {
|
||||
managedDisplay.size = new ManagedDisplay.Dimensions({
|
||||
width : display.getWidth(),
|
||||
height : display.getHeight()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Store changes to display cursor
|
||||
display.oncursor = function setClientCursor(canvas, x, y) {
|
||||
$rootScope.$apply(function updateClientCursor() {
|
||||
managedDisplay.cursor = new ManagedDisplay.Cursor({
|
||||
canvas : canvas,
|
||||
x : x,
|
||||
y : y
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return managedDisplay;
|
||||
|
||||
};
|
||||
|
||||
return ManagedDisplay;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ManagedFileTransferState class used by the guacClientManager
|
||||
* service.
|
||||
*/
|
||||
angular.module('client').factory('ManagedFileTransferState', [function defineManagedFileTransferState() {
|
||||
|
||||
/**
|
||||
* Object which represents the state of a Guacamole stream, including any
|
||||
* error conditions.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedFileTransferState|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedFileTransferState.
|
||||
*/
|
||||
var ManagedFileTransferState = function ManagedFileTransferState(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The current stream state. Valid values are described by
|
||||
* ManagedFileTransferState.StreamState.
|
||||
*
|
||||
* @type String
|
||||
* @default ManagedFileTransferState.StreamState.IDLE
|
||||
*/
|
||||
this.streamState = template.streamState || ManagedFileTransferState.StreamState.IDLE;
|
||||
|
||||
/**
|
||||
* The status code of the current error condition, if streamState
|
||||
* is ERROR. For all other streamState values, this will be
|
||||
* @link{Guacamole.Status.Code.SUCCESS}.
|
||||
*
|
||||
* @type Number
|
||||
* @default Guacamole.Status.Code.SUCCESS
|
||||
*/
|
||||
this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Valid stream state strings. Each state string is associated with a
|
||||
* specific state of a Guacamole stream.
|
||||
*/
|
||||
ManagedFileTransferState.StreamState = {
|
||||
|
||||
/**
|
||||
* The stream has not yet been opened.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
IDLE : "IDLE",
|
||||
|
||||
/**
|
||||
* The stream has been successfully established. Data can be sent or
|
||||
* received.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
OPEN : "OPEN",
|
||||
|
||||
/**
|
||||
* The stream has terminated successfully. No errors are indicated.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
CLOSED : "CLOSED",
|
||||
|
||||
/**
|
||||
* The stream has terminated due to an error. The associated error code
|
||||
* is stored in statusCode.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
ERROR : "ERROR"
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the current transfer state and, if given, the associated status
|
||||
* code. If an error is already represented, this function has no effect.
|
||||
*
|
||||
* @param {ManagedFileTransferState} transferState
|
||||
* The ManagedFileTransferState to update.
|
||||
*
|
||||
* @param {String} streamState
|
||||
* The stream state to assign to the given ManagedFileTransferState, as
|
||||
* listed within ManagedFileTransferState.StreamState.
|
||||
*
|
||||
* @param {Number} [statusCode]
|
||||
* The status code to assign to the given ManagedFileTransferState, if
|
||||
* any, as listed within Guacamole.Status.Code. If no status code is
|
||||
* specified, the status code of the ManagedFileTransferState is not
|
||||
* touched.
|
||||
*/
|
||||
ManagedFileTransferState.setStreamState = function setStreamState(transferState, streamState, statusCode) {
|
||||
|
||||
// Do not set state after an error is registered
|
||||
if (transferState.streamState === ManagedFileTransferState.StreamState.ERROR)
|
||||
return;
|
||||
|
||||
// Update stream state
|
||||
transferState.streamState = streamState;
|
||||
|
||||
// Set status code, if given
|
||||
if (statusCode)
|
||||
transferState.statusCode = statusCode;
|
||||
|
||||
};
|
||||
|
||||
return ManagedFileTransferState;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ManagedFileUpload class used by the guacClientManager service.
|
||||
*/
|
||||
angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector',
|
||||
function defineManagedFileUpload($rootScope, $injector) {
|
||||
|
||||
// Required types
|
||||
var Error = $injector.get('Error');
|
||||
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
|
||||
|
||||
// Required services
|
||||
var requestService = $injector.get('requestService');
|
||||
var tunnelService = $injector.get('tunnelService');
|
||||
|
||||
/**
|
||||
* Object which serves as a surrogate interface, encapsulating a Guacamole
|
||||
* file upload while it is active, allowing it to be detached and
|
||||
* reattached from different client views.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedFileUpload|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedFileUpload.
|
||||
*/
|
||||
var ManagedFileUpload = function ManagedFileUpload(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The current state of the file transfer stream.
|
||||
*
|
||||
* @type ManagedFileTransferState
|
||||
*/
|
||||
this.transferState = template.transferState || new ManagedFileTransferState();
|
||||
|
||||
/**
|
||||
* The mimetype of the file being transferred.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.mimetype = template.mimetype;
|
||||
|
||||
/**
|
||||
* The filename of the file being transferred.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.filename = template.filename;
|
||||
|
||||
/**
|
||||
* The number of bytes transferred so far.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.progress = template.progress;
|
||||
|
||||
/**
|
||||
* The total number of bytes in the file.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
this.length = template.length;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new ManagedFileUpload which uploads the given file to the
|
||||
* server through the given Guacamole client.
|
||||
*
|
||||
* @param {ManagedClient} managedClient
|
||||
* The ManagedClient through which the file is to be uploaded.
|
||||
*
|
||||
* @param {File} file
|
||||
* The file to upload.
|
||||
*
|
||||
* @param {Object} [object]
|
||||
* The object to upload the file to, if any, such as a filesystem
|
||||
* object.
|
||||
*
|
||||
* @param {String} [streamName]
|
||||
* The name of the stream to upload the file to. If an object is given,
|
||||
* this must be specified.
|
||||
*
|
||||
* @return {ManagedFileUpload}
|
||||
* A new ManagedFileUpload object which can be used to track the
|
||||
* progress of the upload.
|
||||
*/
|
||||
ManagedFileUpload.getInstance = function getInstance(managedClient, file, object, streamName) {
|
||||
|
||||
var managedFileUpload = new ManagedFileUpload();
|
||||
|
||||
// Pull Guacamole.Tunnel and Guacamole.Client from given ManagedClient
|
||||
var client = managedClient.client;
|
||||
var tunnel = managedClient.tunnel;
|
||||
|
||||
// Open file for writing
|
||||
var stream;
|
||||
if (!object)
|
||||
stream = client.createFileStream(file.type, file.name);
|
||||
|
||||
// If object/streamName specified, upload to that instead of a file
|
||||
// stream
|
||||
else
|
||||
stream = object.createOutputStream(file.type, streamName);
|
||||
|
||||
// Notify that the file transfer is pending
|
||||
$rootScope.$evalAsync(function uploadStreamOpen() {
|
||||
|
||||
// Init managed upload
|
||||
managedFileUpload.filename = file.name;
|
||||
managedFileUpload.mimetype = file.type;
|
||||
managedFileUpload.progress = 0;
|
||||
managedFileUpload.length = file.size;
|
||||
|
||||
// Notify that stream is open
|
||||
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
|
||||
ManagedFileTransferState.StreamState.OPEN);
|
||||
|
||||
});
|
||||
|
||||
// Upload file once stream is acknowledged
|
||||
stream.onack = function beginUpload(status) {
|
||||
|
||||
// Notify of any errors from the Guacamole server
|
||||
if (status.isError()) {
|
||||
$rootScope.$apply(function uploadStreamError() {
|
||||
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
|
||||
ManagedFileTransferState.StreamState.ERROR,
|
||||
status.code);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Begin upload
|
||||
tunnelService.uploadToStream(tunnel.uuid, stream, file, function uploadContinuing(length) {
|
||||
$rootScope.$apply(function uploadStreamProgress() {
|
||||
managedFileUpload.progress = length;
|
||||
});
|
||||
})
|
||||
|
||||
// Notify if upload succeeds
|
||||
.then(function uploadSuccessful() {
|
||||
|
||||
// Upload complete
|
||||
managedFileUpload.progress = file.size;
|
||||
|
||||
// Close the stream
|
||||
stream.sendEnd();
|
||||
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
|
||||
ManagedFileTransferState.StreamState.CLOSED);
|
||||
|
||||
// Notify of upload completion
|
||||
$rootScope.$broadcast('guacUploadComplete', file.name);
|
||||
},
|
||||
|
||||
// Notify if upload fails
|
||||
requestService.createErrorCallback(function uploadFailed(error) {
|
||||
|
||||
// Use provide status code if the error is coming from the stream
|
||||
if (error.type === Error.Type.STREAM_ERROR)
|
||||
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
|
||||
ManagedFileTransferState.StreamState.ERROR,
|
||||
error.statusCode);
|
||||
|
||||
// Fail with internal error for all other causes
|
||||
else
|
||||
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
|
||||
ManagedFileTransferState.StreamState.ERROR,
|
||||
Guacamole.Status.Code.INTERNAL_ERROR);
|
||||
|
||||
// Close the stream
|
||||
stream.sendEnd();
|
||||
|
||||
}));
|
||||
|
||||
// Ignore all further acks
|
||||
stream.onack = null;
|
||||
|
||||
|
||||
};
|
||||
|
||||
return managedFileUpload;
|
||||
|
||||
};
|
||||
|
||||
return ManagedFileUpload;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ManagedFilesystem class used by ManagedClient to represent
|
||||
* available remote filesystems.
|
||||
*/
|
||||
angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector',
|
||||
function defineManagedFilesystem($rootScope, $injector) {
|
||||
|
||||
// Required types
|
||||
var tunnelService = $injector.get('tunnelService');
|
||||
|
||||
/**
|
||||
* Object which serves as a surrogate interface, encapsulating a Guacamole
|
||||
* filesystem object while it is active, allowing it to be detached and
|
||||
* reattached from different client views.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedFilesystem|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedFilesystem.
|
||||
*/
|
||||
var ManagedFilesystem = function ManagedFilesystem(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The client that originally received the "filesystem" instruction
|
||||
* that resulted in the creation of this ManagedFilesystem.
|
||||
*
|
||||
* @type ManagedClient
|
||||
*/
|
||||
this.client = template.client;
|
||||
|
||||
/**
|
||||
* The Guacamole filesystem object, as received via a "filesystem"
|
||||
* instruction.
|
||||
*
|
||||
* @type Guacamole.Object
|
||||
*/
|
||||
this.object = template.object;
|
||||
|
||||
/**
|
||||
* The declared, human-readable name of the filesystem
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.name = template.name;
|
||||
|
||||
/**
|
||||
* The root directory of the filesystem.
|
||||
*
|
||||
* @type ManagedFilesystem.File
|
||||
*/
|
||||
this.root = template.root;
|
||||
|
||||
/**
|
||||
* The current directory being viewed or manipulated within the
|
||||
* filesystem.
|
||||
*
|
||||
* @type ManagedFilesystem.File
|
||||
*/
|
||||
this.currentDirectory = template.currentDirectory || template.root;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Refreshes the contents of the given file, if that file is a directory.
|
||||
* Only the immediate children of the file are refreshed. Files further
|
||||
* down the directory tree are not refreshed.
|
||||
*
|
||||
* @param {ManagedFilesystem} filesystem
|
||||
* The filesystem associated with the file being refreshed.
|
||||
*
|
||||
* @param {ManagedFilesystem.File} file
|
||||
* The file being refreshed.
|
||||
*/
|
||||
ManagedFilesystem.refresh = function updateDirectory(filesystem, file) {
|
||||
|
||||
// Do not attempt to refresh the contents of directories
|
||||
if (file.mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE)
|
||||
return;
|
||||
|
||||
// Request contents of given file
|
||||
filesystem.object.requestInputStream(file.streamName, function handleStream(stream, mimetype) {
|
||||
|
||||
// Ignore stream if mimetype is wrong
|
||||
if (mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) {
|
||||
stream.sendAck('Unexpected mimetype', Guacamole.Status.Code.UNSUPPORTED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Signal server that data is ready to be received
|
||||
stream.sendAck('Ready', Guacamole.Status.Code.SUCCESS);
|
||||
|
||||
// Read stream as JSON
|
||||
var reader = new Guacamole.JSONReader(stream);
|
||||
|
||||
// Acknowledge received JSON blobs
|
||||
reader.onprogress = function onprogress() {
|
||||
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
|
||||
};
|
||||
|
||||
// Reset contents of directory
|
||||
reader.onend = function jsonReady() {
|
||||
$rootScope.$evalAsync(function updateFileContents() {
|
||||
|
||||
// Empty contents
|
||||
file.files = {};
|
||||
|
||||
// Determine the expected filename prefix of each stream
|
||||
var expectedPrefix = file.streamName;
|
||||
if (expectedPrefix.charAt(expectedPrefix.length - 1) !== '/')
|
||||
expectedPrefix += '/';
|
||||
|
||||
// For each received stream name
|
||||
var mimetypes = reader.getJSON();
|
||||
for (var name in mimetypes) {
|
||||
|
||||
// Assert prefix is correct
|
||||
if (name.substring(0, expectedPrefix.length) !== expectedPrefix)
|
||||
continue;
|
||||
|
||||
// Extract filename from stream name
|
||||
var filename = name.substring(expectedPrefix.length);
|
||||
|
||||
// Deduce type from mimetype
|
||||
var type = ManagedFilesystem.File.Type.NORMAL;
|
||||
if (mimetypes[name] === Guacamole.Object.STREAM_INDEX_MIMETYPE)
|
||||
type = ManagedFilesystem.File.Type.DIRECTORY;
|
||||
|
||||
// Add file entry
|
||||
file.files[filename] = new ManagedFilesystem.File({
|
||||
mimetype : mimetypes[name],
|
||||
streamName : name,
|
||||
type : type,
|
||||
parent : file,
|
||||
name : filename
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new ManagedFilesystem instance from the given Guacamole.Object
|
||||
* and human-readable name. Upon creation, a request to populate the
|
||||
* contents of the root directory will be automatically dispatched.
|
||||
*
|
||||
* @param {ManagedClient} client
|
||||
* The client that originally received the "filesystem" instruction
|
||||
* that resulted in the creation of this ManagedFilesystem.
|
||||
*
|
||||
* @param {Guacamole.Object} object
|
||||
* The Guacamole.Object defining the filesystem.
|
||||
*
|
||||
* @param {String} name
|
||||
* A human-readable name for the filesystem.
|
||||
*
|
||||
* @returns {ManagedFilesystem}
|
||||
* The newly-created ManagedFilesystem.
|
||||
*/
|
||||
ManagedFilesystem.getInstance = function getInstance(client, object, name) {
|
||||
|
||||
// Init new filesystem object
|
||||
var managedFilesystem = new ManagedFilesystem({
|
||||
client : client,
|
||||
object : object,
|
||||
name : name,
|
||||
root : new ManagedFilesystem.File({
|
||||
mimetype : Guacamole.Object.STREAM_INDEX_MIMETYPE,
|
||||
streamName : Guacamole.Object.ROOT_STREAM,
|
||||
type : ManagedFilesystem.File.Type.DIRECTORY
|
||||
})
|
||||
});
|
||||
|
||||
// Retrieve contents of root
|
||||
ManagedFilesystem.refresh(managedFilesystem, managedFilesystem.root);
|
||||
|
||||
return managedFilesystem;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Downloads the given file from the server using the given Guacamole
|
||||
* client and filesystem. The browser will automatically start the
|
||||
* download upon completion of this function.
|
||||
*
|
||||
* @param {ManagedFilesystem} managedFilesystem
|
||||
* The ManagedFilesystem from which the file is to be downloaded. Any
|
||||
* path information provided must be relative to this filesystem.
|
||||
*
|
||||
* @param {String} path
|
||||
* The full, absolute path of the file to download.
|
||||
*/
|
||||
ManagedFilesystem.downloadFile = function downloadFile(managedFilesystem, path) {
|
||||
|
||||
// Request download
|
||||
managedFilesystem.object.requestInputStream(path, function downloadStreamReceived(stream, mimetype) {
|
||||
|
||||
// Parse filename from string
|
||||
var filename = path.match(/(.*[\\/])?(.*)/)[2];
|
||||
|
||||
// Start download
|
||||
tunnelService.downloadStream(managedFilesystem.client.tunnel.uuid, stream, mimetype, filename);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the current directory of the given filesystem, automatically
|
||||
* refreshing the contents of that directory.
|
||||
*
|
||||
* @param {ManagedFilesystem} filesystem
|
||||
* The filesystem whose current directory should be changed.
|
||||
*
|
||||
* @param {ManagedFilesystem.File} file
|
||||
* The directory to change to.
|
||||
*/
|
||||
ManagedFilesystem.changeDirectory = function changeDirectory(filesystem, file) {
|
||||
|
||||
// Refresh contents
|
||||
ManagedFilesystem.refresh(filesystem, file);
|
||||
|
||||
// Set current directory
|
||||
filesystem.currentDirectory = file;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* A file within a ManagedFilesystem. Each ManagedFilesystem.File provides
|
||||
* sufficient information for retrieval or replacement of the file's
|
||||
* contents, as well as the file's name and type.
|
||||
*
|
||||
* @param {ManagedFilesystem|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedFilesystem.File.
|
||||
*/
|
||||
ManagedFilesystem.File = function File(template) {
|
||||
|
||||
/**
|
||||
* The mimetype of the data contained within this file.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.mimetype = template.mimetype;
|
||||
|
||||
/**
|
||||
* The name of the stream representing this files contents within its
|
||||
* associated filesystem object.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.streamName = template.streamName;
|
||||
|
||||
/**
|
||||
* The type of this file. All legal file type strings are defined
|
||||
* within ManagedFilesystem.File.Type.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.type = template.type;
|
||||
|
||||
/**
|
||||
* The name of this file.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.name = template.name;
|
||||
|
||||
/**
|
||||
* The parent directory of this file. In the case of the root
|
||||
* directory, this will be null.
|
||||
*
|
||||
* @type ManagedFilesystem.File
|
||||
*/
|
||||
this.parent = template.parent;
|
||||
|
||||
/**
|
||||
* Map of all known files containined within this file by name. This is
|
||||
* only applicable to directories.
|
||||
*
|
||||
* @type Object.<String, ManagedFilesystem.File>
|
||||
*/
|
||||
this.files = template.files || {};
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* All legal type strings for a ManagedFilesystem.File.
|
||||
*
|
||||
* @type Object.<String, String>
|
||||
*/
|
||||
ManagedFilesystem.File.Type = {
|
||||
|
||||
/**
|
||||
* A normal file. As ManagedFilesystem does not currently represent any
|
||||
* other non-directory types of files, like symbolic links, this type
|
||||
* string may be used for any non-directory file.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
NORMAL : 'NORMAL',
|
||||
|
||||
/**
|
||||
* A directory.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
DIRECTORY : 'DIRECTORY'
|
||||
|
||||
};
|
||||
|
||||
return ManagedFilesystem;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ManagedShareLink class used by ManagedClient to represent
|
||||
* generated connection sharing links.
|
||||
*/
|
||||
angular.module('client').factory('ManagedShareLink', ['$injector',
|
||||
function defineManagedShareLink($injector) {
|
||||
|
||||
// Required types
|
||||
var UserCredentials = $injector.get('UserCredentials');
|
||||
|
||||
/**
|
||||
* Object which represents a link which can be used to gain access to an
|
||||
* active Guacamole connection.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ManagedShareLink|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ManagedShareLink.
|
||||
*/
|
||||
var ManagedShareLink = function ManagedShareLink(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The human-readable display name of this share link.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.name = template.name;
|
||||
|
||||
/**
|
||||
* The actual URL of the link which can be used to access the shared
|
||||
* connection.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.href = template.href;
|
||||
|
||||
/**
|
||||
* The sharing profile which was used to generate the share link.
|
||||
*
|
||||
* @type SharingProfile
|
||||
*/
|
||||
this.sharingProfile = template.sharingProfile;
|
||||
|
||||
/**
|
||||
* The credentials from which the share link was derived.
|
||||
*
|
||||
* @type UserCredentials
|
||||
*/
|
||||
this.sharingCredentials = template.sharingCredentials;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new ManagedShareLink from a set of UserCredentials and the
|
||||
* SharingProfile which was used to generate those UserCredentials.
|
||||
*
|
||||
* @param {SharingProfile} sharingProfile
|
||||
* The SharingProfile which was used, via the REST API, to generate the
|
||||
* given UserCredentials.
|
||||
*
|
||||
* @param {UserCredentials} sharingCredentials
|
||||
* The UserCredentials object returned by the REST API in response to a
|
||||
* request to share a connection using the given SharingProfile.
|
||||
*
|
||||
* @return {ManagedShareLink}
|
||||
* A new ManagedShareLink object can be used to access the connection
|
||||
* shared via the given SharingProfile and resulting UserCredentials.
|
||||
*/
|
||||
ManagedShareLink.getInstance = function getInstance(sharingProfile, sharingCredentials) {
|
||||
|
||||
// Generate new share link using the given profile and credentials
|
||||
return new ManagedShareLink({
|
||||
'name' : sharingProfile.name,
|
||||
'href' : UserCredentials.getLink(sharingCredentials),
|
||||
'sharingProfile' : sharingProfile,
|
||||
'sharingCredentials' : sharingCredentials
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
return ManagedShareLink;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the TranslationResult class used by the guacTranslate service. This class contains
|
||||
* both the translated message and the translation ID that generated the message, in the case
|
||||
* where it's unknown whether a translation is defined or not.
|
||||
*/
|
||||
angular.module('client').factory('TranslationResult', [function defineTranslationResult() {
|
||||
|
||||
/**
|
||||
* Object which represents the result of a translation as returned from
|
||||
* the guacTranslate service.
|
||||
*
|
||||
* @constructor
|
||||
* @param {TranslationResult|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* TranslationResult.
|
||||
*/
|
||||
const TranslationResult = function TranslationResult(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The translation ID.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.id = template.id;
|
||||
|
||||
/**
|
||||
* The translated message.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.message = template.message;
|
||||
|
||||
};
|
||||
|
||||
return TranslationResult;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The module for code used to manipulate/observe the clipboard.
|
||||
*/
|
||||
angular.module('clipboard', []);
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive provides an editor for the clipboard content maintained by
|
||||
* clipboardService. Changes to the clipboard by clipboardService will
|
||||
* automatically be reflected in the editor, and changes in the editor will
|
||||
* automatically be reflected in the clipboard by clipboardService.
|
||||
*/
|
||||
angular.module('clipboard').directive('guacClipboard', ['$injector',
|
||||
function guacClipboard($injector) {
|
||||
|
||||
// Required types
|
||||
const ClipboardData = $injector.get('ClipboardData');
|
||||
|
||||
// Required services
|
||||
const $window = $injector.get('$window');
|
||||
const clipboardService = $injector.get('clipboardService');
|
||||
|
||||
/**
|
||||
* Configuration object for the guacClipboard directive.
|
||||
*
|
||||
* @type Object.<String, Object>
|
||||
*/
|
||||
var config = {
|
||||
restrict : 'E',
|
||||
replace : true,
|
||||
templateUrl : 'app/clipboard/templates/guacClipboard.html'
|
||||
};
|
||||
|
||||
// guacClipboard directive controller
|
||||
config.controller = ['$scope', '$injector', '$element',
|
||||
function guacClipboardController($scope, $injector, $element) {
|
||||
|
||||
/**
|
||||
* The DOM element which will contain the clipboard contents within the
|
||||
* user interface provided by this directive. We populate the clipboard
|
||||
* editor via this DOM element rather than updating a model so that we
|
||||
* are prepared for future support of rich text contents.
|
||||
*
|
||||
* @type {!Element}
|
||||
*/
|
||||
var element = $element[0].querySelectorAll('.clipboard')[0];
|
||||
|
||||
/**
|
||||
* Whether clipboard contents should be displayed in the clipboard
|
||||
* editor. If false, clipboard contents will not be displayed until
|
||||
* the user manually reveals them.
|
||||
*
|
||||
* @type {!boolean}
|
||||
*/
|
||||
$scope.contentsShown = false;
|
||||
|
||||
/**
|
||||
* Reveals the contents of the clipboard editor, automatically
|
||||
* assigning input focus to the editor if possible.
|
||||
*/
|
||||
$scope.showContents = function showContents() {
|
||||
$scope.contentsShown = true;
|
||||
$window.setTimeout(function setFocus() {
|
||||
element.focus();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rereads the contents of the clipboard field, updating the
|
||||
* ClipboardData object on the scope as necessary. The type of data
|
||||
* stored within the ClipboardData object will be heuristically
|
||||
* determined from the HTML contents of the clipboard field.
|
||||
*/
|
||||
var updateClipboardData = function updateClipboardData() {
|
||||
|
||||
// Read contents of clipboard textarea
|
||||
clipboardService.setClipboard(new ClipboardData({
|
||||
type : 'text/plain',
|
||||
data : element.value
|
||||
}));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the contents of the clipboard editor to the given data.
|
||||
*
|
||||
* @param {ClipboardData} data
|
||||
* The ClipboardData to display within the clipboard editor for
|
||||
* editing.
|
||||
*/
|
||||
const updateClipboardEditor = function updateClipboardEditor(data) {
|
||||
|
||||
// If the clipboard data is a string, render it as text
|
||||
if (typeof data.data === 'string')
|
||||
element.value = data.data;
|
||||
|
||||
// Ignore other data types for now
|
||||
|
||||
};
|
||||
|
||||
// Update the internally-stored clipboard data when events are fired
|
||||
// that indicate the clipboard field may have been changed
|
||||
element.addEventListener('input', updateClipboardData);
|
||||
element.addEventListener('change', updateClipboardData);
|
||||
|
||||
// Update remote clipboard if local clipboard changes
|
||||
$scope.$on('guacClipboard', function clipboardChanged(event, data) {
|
||||
updateClipboardEditor(data);
|
||||
});
|
||||
|
||||
// Init clipboard editor with current clipboard contents
|
||||
clipboardService.getClipboard().then((data) => {
|
||||
updateClipboardEditor(data);
|
||||
}, angular.noop);
|
||||
|
||||
}];
|
||||
|
||||
return config;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,625 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A service for maintaining and accessing clipboard data. If possible, this
|
||||
* service will leverage the local clipboard. If the local clipboard is not
|
||||
* available, an internal in-memory clipboard will be used instead.
|
||||
*/
|
||||
angular.module('clipboard').factory('clipboardService', ['$injector',
|
||||
function clipboardService($injector) {
|
||||
|
||||
// Get required services
|
||||
const $q = $injector.get('$q');
|
||||
const $window = $injector.get('$window');
|
||||
const $rootScope = $injector.get('$rootScope');
|
||||
const sessionStorageFactory = $injector.get('sessionStorageFactory');
|
||||
|
||||
// Required types
|
||||
const ClipboardData = $injector.get('ClipboardData');
|
||||
|
||||
/**
|
||||
* Getter/setter which retrieves or sets the current stored clipboard
|
||||
* contents. The stored clipboard contents are strictly internal to
|
||||
* Guacamole, and may not reflect the local clipboard if local clipboard
|
||||
* access is unavailable.
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
const storedClipboardData = sessionStorageFactory.create(new ClipboardData());
|
||||
|
||||
var service = {};
|
||||
|
||||
/**
|
||||
* The amount of time to wait before actually serving a request to read
|
||||
* clipboard data, in milliseconds. Providing a reasonable delay between
|
||||
* request and read attempt allows the cut/copy operation to settle, in
|
||||
* case the data we are anticipating to be present is not actually present
|
||||
* in the clipboard yet.
|
||||
*
|
||||
* @constant
|
||||
* @type Number
|
||||
*/
|
||||
var CLIPBOARD_READ_DELAY = 100;
|
||||
|
||||
/**
|
||||
* The promise associated with the current pending clipboard read attempt.
|
||||
* If no clipboard read is active, this will be null.
|
||||
*
|
||||
* @type Promise.<ClipboardData>
|
||||
*/
|
||||
var pendingRead = null;
|
||||
|
||||
/**
|
||||
* Reference to the window.document object.
|
||||
*
|
||||
* @private
|
||||
* @type HTMLDocument
|
||||
*/
|
||||
var document = $window.document;
|
||||
|
||||
/**
|
||||
* The textarea that will be used to hold the local clipboard contents.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var clipboardContent = document.createElement('textarea');
|
||||
|
||||
// Ensure clipboard target is selectable but not visible
|
||||
clipboardContent.className = 'clipboard-service-target';
|
||||
|
||||
// Add clipboard target to DOM
|
||||
document.body.appendChild(clipboardContent);
|
||||
|
||||
/**
|
||||
* Stops the propagation of the given event through the DOM tree. This is
|
||||
* identical to invoking stopPropagation() on the event directly, except
|
||||
* that this function is usable as an event handler itself.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event whose propagation through the DOM tree should be stopped.
|
||||
*/
|
||||
var stopEventPropagation = function stopEventPropagation(e) {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
// Prevent events generated due to execCommand() from disturbing external things
|
||||
clipboardContent.addEventListener('cut', stopEventPropagation);
|
||||
clipboardContent.addEventListener('copy', stopEventPropagation);
|
||||
clipboardContent.addEventListener('paste', stopEventPropagation);
|
||||
clipboardContent.addEventListener('input', stopEventPropagation);
|
||||
|
||||
/**
|
||||
* A stack of past node selection ranges. A range convering the nodes
|
||||
* currently selected within the document can be pushed onto this stack
|
||||
* with pushSelection(), and the most recently pushed selection can be
|
||||
* popped off the stack (and thus re-selected) with popSelection().
|
||||
*
|
||||
* @type Range[]
|
||||
*/
|
||||
var selectionStack = [];
|
||||
|
||||
/**
|
||||
* Pushes the current selection range to the selection stack such that it
|
||||
* can later be restored with popSelection().
|
||||
*/
|
||||
var pushSelection = function pushSelection() {
|
||||
|
||||
// Add a range representing the current selection to the stack
|
||||
var selection = $window.getSelection();
|
||||
if (selection.getRangeAt && selection.rangeCount)
|
||||
selectionStack.push(selection.getRangeAt(0));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Pops a selection range off the selection stack restoring the document's
|
||||
* previous selection state. The selection range will be the most recent
|
||||
* selection range pushed by pushSelection(). If there are no selection
|
||||
* ranges currently on the stack, this function has no effect.
|
||||
*/
|
||||
var popSelection = function popSelection() {
|
||||
|
||||
// Pull one selection range from the stack
|
||||
var range = selectionStack.pop();
|
||||
if (!range)
|
||||
return;
|
||||
|
||||
// Replace any current selection with the retrieved selection
|
||||
var selection = $window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects all nodes within the given element. This will replace the
|
||||
* current selection with a new selection range that covers the element's
|
||||
* contents. If the original selection should be preserved, use
|
||||
* pushSelection() and popSelection().
|
||||
*
|
||||
* @param {Element} element
|
||||
* The element whose contents should be selected.
|
||||
*/
|
||||
var selectAll = function selectAll(element) {
|
||||
|
||||
// Use the select() function defined for input elements, if available
|
||||
if (element.select)
|
||||
element.select();
|
||||
|
||||
// Fallback to manual manipulation of the selection
|
||||
else {
|
||||
|
||||
// Generate a range which selects all nodes within the given element
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
|
||||
// Replace any current selection with the generated range
|
||||
var selection = $window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the local clipboard, if possible, to the given text.
|
||||
*
|
||||
* @param {ClipboardData} data
|
||||
* The data to assign to the local clipboard should be set.
|
||||
*
|
||||
* @return {Promise}
|
||||
* A promise that will resolve if setting the clipboard was successful,
|
||||
* and will reject if it failed.
|
||||
*/
|
||||
const setLocalClipboard = function setLocalClipboard(data) {
|
||||
|
||||
var deferred = $q.defer();
|
||||
|
||||
try {
|
||||
|
||||
// Attempt to read the clipboard using the Asynchronous Clipboard
|
||||
// API, if it's available
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
if (data.type === 'text/plain') {
|
||||
navigator.clipboard.writeText(data.data).then(deferred.resolve, deferred.reject);
|
||||
return deferred.promise;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Ignore any hard failures to use Asynchronous Clipboard API, falling
|
||||
// back to traditional document.execCommand()
|
||||
catch (ignore) {}
|
||||
|
||||
// Track the originally-focused element prior to changing focus
|
||||
var originalElement = document.activeElement;
|
||||
pushSelection();
|
||||
|
||||
// Copy the given value into the clipboard DOM element
|
||||
if (typeof data.data === 'string')
|
||||
clipboardContent.value = data.data;
|
||||
else {
|
||||
clipboardContent.innerHTML = '';
|
||||
var img = document.createElement('img');
|
||||
img.src = URL.createObjectURL(data.data);
|
||||
clipboardContent.appendChild(img);
|
||||
}
|
||||
|
||||
// Select all data within the clipboard target
|
||||
clipboardContent.focus();
|
||||
selectAll(clipboardContent);
|
||||
|
||||
// Attempt to copy data from clipboard element into local clipboard
|
||||
if (document.execCommand('copy'))
|
||||
deferred.resolve();
|
||||
else
|
||||
deferred.reject();
|
||||
|
||||
// Unfocus the clipboard DOM event to avoid mobile keyboard opening,
|
||||
// restoring whichever element was originally focused
|
||||
clipboardContent.blur();
|
||||
originalElement.focus();
|
||||
popSelection();
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the given data URL, returning its decoded contents as a new Blob.
|
||||
* If the URL is not a valid data URL, null will be returned instead.
|
||||
*
|
||||
* @param {String} url
|
||||
* The data URL to parse.
|
||||
*
|
||||
* @returns {Blob}
|
||||
* A new Blob containing the decoded contents of the data URL, or null
|
||||
* if the URL is not a valid data URL.
|
||||
*/
|
||||
service.parseDataURL = function parseDataURL(url) {
|
||||
|
||||
// Parse given string as a data URL
|
||||
var result = /^data:([^;]*);base64,([a-zA-Z0-9+/]*[=]*)$/.exec(url);
|
||||
if (!result)
|
||||
return null;
|
||||
|
||||
// Pull the mimetype and base64 contents of the data URL
|
||||
var type = result[1];
|
||||
var data = $window.atob(result[2]);
|
||||
|
||||
// Convert the decoded binary string into a typed array
|
||||
var buffer = new Uint8Array(data.length);
|
||||
for (var i = 0; i < data.length; i++)
|
||||
buffer[i] = data.charCodeAt(i);
|
||||
|
||||
// Produce a proper blob containing the data and type provided in
|
||||
// the data URL
|
||||
return new Blob([buffer], { type : type });
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the content of the given element as plain, unformatted text,
|
||||
* preserving only individual characters and newlines. Formatting, images,
|
||||
* etc. are not taken into account.
|
||||
*
|
||||
* @param {Element} element
|
||||
* The element whose text content should be returned.
|
||||
*
|
||||
* @returns {String}
|
||||
* The plain text contents of the given element, including newlines and
|
||||
* spacing but otherwise without any formatting.
|
||||
*/
|
||||
service.getTextContent = function getTextContent(element) {
|
||||
|
||||
var blocks = [];
|
||||
var currentBlock = '';
|
||||
|
||||
// For each child of the given element
|
||||
var current = element.firstChild;
|
||||
while (current) {
|
||||
|
||||
// Simply append the content of any text nodes
|
||||
if (current.nodeType === Node.TEXT_NODE)
|
||||
currentBlock += current.nodeValue;
|
||||
|
||||
// Render <br> as a newline character
|
||||
else if (current.nodeName === 'BR')
|
||||
currentBlock += '\n';
|
||||
|
||||
// Render <img> as alt text, if available
|
||||
else if (current.nodeName === 'IMG')
|
||||
currentBlock += current.getAttribute('alt') || '';
|
||||
|
||||
// For all other nodes, handling depends on whether they are
|
||||
// block-level elements
|
||||
else {
|
||||
|
||||
// If we are entering a new block context, start a new block if
|
||||
// the current block is non-empty
|
||||
if (currentBlock.length && $window.getComputedStyle(current).display === 'block') {
|
||||
|
||||
// Trim trailing newline (would otherwise inflate the line count by 1)
|
||||
if (currentBlock.substring(currentBlock.length - 1) === '\n')
|
||||
currentBlock = currentBlock.substring(0, currentBlock.length - 1);
|
||||
|
||||
// Finish current block and start a new block
|
||||
blocks.push(currentBlock);
|
||||
currentBlock = '';
|
||||
|
||||
}
|
||||
|
||||
// Append the content of the current element to the current block
|
||||
currentBlock += service.getTextContent(current);
|
||||
|
||||
}
|
||||
|
||||
current = current.nextSibling;
|
||||
|
||||
}
|
||||
|
||||
// Add any in-progress block
|
||||
if (currentBlock.length)
|
||||
blocks.push(currentBlock);
|
||||
|
||||
// Combine all non-empty blocks, separated by newlines
|
||||
return blocks.join('\n');
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces the current text content of the given element with the given
|
||||
* text. To avoid affecting the position of the cursor within an editable
|
||||
* element, or firing unnecessary DOM modification events, the underlying
|
||||
* <code>textContent</code> property of the element is only touched if
|
||||
* doing so would actually change the text.
|
||||
*
|
||||
* @param {Element} element
|
||||
* The element whose text content should be changed.
|
||||
*
|
||||
* @param {String} text
|
||||
* The text content to assign to the given element.
|
||||
*/
|
||||
service.setTextContent = function setTextContent(element, text) {
|
||||
|
||||
// Strip out any images
|
||||
$(element).find('img').remove();
|
||||
|
||||
// Reset text content only if doing so will actually change the content
|
||||
if (service.getTextContent(element) !== text)
|
||||
element.textContent = text;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the URL of the single image within the given element, if the
|
||||
* element truly contains only one child and that child is an image. If the
|
||||
* content of the element is mixed or not an image, null is returned.
|
||||
*
|
||||
* @param {Element} element
|
||||
* The element whose image content should be retrieved.
|
||||
*
|
||||
* @returns {String}
|
||||
* The URL of the image contained within the given element, if that
|
||||
* element contains only a single child element which happens to be an
|
||||
* image, or null if the content of the element is not purely an image.
|
||||
*/
|
||||
service.getImageContent = function getImageContent(element) {
|
||||
|
||||
// Return the source of the single child element, if it is an image
|
||||
var firstChild = element.firstChild;
|
||||
if (firstChild && firstChild.nodeName === 'IMG' && !firstChild.nextSibling)
|
||||
return firstChild.getAttribute('src');
|
||||
|
||||
// Otherwise, the content of this element is not simply an image
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces the current contents of the given element with a single image
|
||||
* having the given URL. To avoid affecting the position of the cursor
|
||||
* within an editable element, or firing unnecessary DOM modification
|
||||
* events, the content of the element is only touched if doing so would
|
||||
* actually change content.
|
||||
*
|
||||
* @param {Element} element
|
||||
* The element whose image content should be changed.
|
||||
*
|
||||
* @param {String} url
|
||||
* The URL of the image which should be assigned as the contents of the
|
||||
* given element.
|
||||
*/
|
||||
service.setImageContent = function setImageContent(element, url) {
|
||||
|
||||
// Retrieve the URL of the current image contents, if any
|
||||
var currentImage = service.getImageContent(element);
|
||||
|
||||
// If the current contents are not the given image (or not an image
|
||||
// at all), reassign the contents
|
||||
if (currentImage !== url) {
|
||||
|
||||
// Clear current contents
|
||||
element.innerHTML = '';
|
||||
|
||||
// Add a new image as the sole contents of the element
|
||||
var img = document.createElement('img');
|
||||
img.src = url;
|
||||
element.appendChild(img);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current value of the local clipboard.
|
||||
*
|
||||
* @return {Promise.<ClipboardData>}
|
||||
* A promise that will resolve with the contents of the local clipboard
|
||||
* if getting the clipboard was successful, and will reject if it
|
||||
* failed.
|
||||
*/
|
||||
const getLocalClipboard = function getLocalClipboard() {
|
||||
|
||||
// If the clipboard is already being read, do not overlap the read
|
||||
// attempts; instead share the result across all requests
|
||||
if (pendingRead)
|
||||
return pendingRead;
|
||||
|
||||
var deferred = $q.defer();
|
||||
|
||||
try {
|
||||
|
||||
// Attempt to read the clipboard using the Asynchronous Clipboard
|
||||
// API, if it's available
|
||||
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||
|
||||
navigator.clipboard.readText().then(function textRead(text) {
|
||||
deferred.resolve(new ClipboardData({
|
||||
type : 'text/plain',
|
||||
data : text
|
||||
}));
|
||||
}, deferred.reject);
|
||||
|
||||
return deferred.promise;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Ignore any hard failures to use Asynchronous Clipboard API, falling
|
||||
// back to traditional document.execCommand()
|
||||
catch (ignore) {}
|
||||
|
||||
// Track the originally-focused element prior to changing focus
|
||||
var originalElement = document.activeElement;
|
||||
|
||||
/**
|
||||
* Attempts to paste the clipboard contents into the
|
||||
* currently-focused element. The promise related to the current
|
||||
* attempt to read the clipboard will be resolved or rejected
|
||||
* depending on whether the attempt to paste succeeds.
|
||||
*/
|
||||
var performPaste = function performPaste() {
|
||||
|
||||
// Attempt paste local clipboard into clipboard DOM element
|
||||
if (document.execCommand('paste')) {
|
||||
|
||||
// If the pasted data is a single image, resolve with a blob
|
||||
// containing that image
|
||||
var currentImage = service.getImageContent(clipboardContent);
|
||||
if (currentImage) {
|
||||
|
||||
// Convert the image's data URL into a blob
|
||||
var blob = service.parseDataURL(currentImage);
|
||||
if (blob) {
|
||||
deferred.resolve(new ClipboardData({
|
||||
type : blob.type,
|
||||
data : blob
|
||||
}));
|
||||
}
|
||||
|
||||
// Reject if conversion fails
|
||||
else
|
||||
deferred.reject();
|
||||
|
||||
} // end if clipboard is an image
|
||||
|
||||
// Otherwise, assume the clipboard contains plain text
|
||||
else
|
||||
deferred.resolve(new ClipboardData({
|
||||
type : 'text/plain',
|
||||
data : clipboardContent.value
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
// Otherwise, reading from the clipboard has failed
|
||||
else
|
||||
deferred.reject();
|
||||
|
||||
};
|
||||
|
||||
// Mark read attempt as in progress, cleaning up event listener and
|
||||
// selection once the paste attempt has completed
|
||||
pendingRead = deferred.promise['finally'](function cleanupReadAttempt() {
|
||||
|
||||
// Do not use future changes in focus
|
||||
clipboardContent.removeEventListener('focus', performPaste);
|
||||
|
||||
// Unfocus the clipboard DOM event to avoid mobile keyboard opening,
|
||||
// restoring whichever element was originally focused
|
||||
clipboardContent.blur();
|
||||
originalElement.focus();
|
||||
popSelection();
|
||||
|
||||
// No read is pending any longer
|
||||
pendingRead = null;
|
||||
|
||||
});
|
||||
|
||||
// Wait for the next event queue run before attempting to read
|
||||
// clipboard data (in case the copy/cut has not yet completed)
|
||||
$window.setTimeout(function deferredClipboardRead() {
|
||||
|
||||
pushSelection();
|
||||
|
||||
// Ensure clipboard element is blurred (and that the "focus" event
|
||||
// will fire)
|
||||
clipboardContent.blur();
|
||||
clipboardContent.addEventListener('focus', performPaste);
|
||||
|
||||
// Clear and select the clipboard DOM element
|
||||
clipboardContent.value = '';
|
||||
clipboardContent.focus();
|
||||
selectAll(clipboardContent);
|
||||
|
||||
// If focus failed to be set, we cannot read the clipboard
|
||||
if (document.activeElement !== clipboardContent)
|
||||
deferred.reject();
|
||||
|
||||
}, CLIPBOARD_READ_DELAY);
|
||||
|
||||
return pendingRead;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current value of the internal clipboard shared across all
|
||||
* active Guacamole connections running within the current browser tab. If
|
||||
* access to the local clipboard is available, the internal clipboard is
|
||||
* first synchronized with the current local clipboard contents. If access
|
||||
* to the local clipboard is unavailable, only the internal clipboard will
|
||||
* be used.
|
||||
*
|
||||
* @return {Promise.<ClipboardData>}
|
||||
* A promise that will resolve with the contents of the internal
|
||||
* clipboard, first retrieving those contents from the local clipboard
|
||||
* if permission to do so has been granted. This promise is always
|
||||
* resolved.
|
||||
*/
|
||||
service.getClipboard = function getClipboard() {
|
||||
return getLocalClipboard().then((data) => storedClipboardData(data), () => storedClipboardData());
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the content of the internal clipboard shared across all active
|
||||
* Guacamole connections running within the current browser tab. If
|
||||
* access to the local clipboard is available, the local clipboard is
|
||||
* first set to the provided clipboard content. If access to the local
|
||||
* clipboard is unavailable, only the internal clipboard will be used. A
|
||||
* "guacClipboard" event will be broadcast with the assigned data once the
|
||||
* operation has completed.
|
||||
*
|
||||
* @param {ClipboardData} data
|
||||
* The data to assign to the clipboard.
|
||||
*
|
||||
* @return {Promise}
|
||||
* A promise that will resolve after the clipboard content has been
|
||||
* set. This promise is always resolved.
|
||||
*/
|
||||
service.setClipboard = function setClipboard(data) {
|
||||
return setLocalClipboard(data)['catch'](angular.noop).finally(() => {
|
||||
|
||||
// Update internal clipboard and broadcast event notifying of
|
||||
// updated contents
|
||||
storedClipboardData(data);
|
||||
$rootScope.$broadcast('guacClipboard', data);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Resynchronizes the local and internal clipboards, setting the contents
|
||||
* of the internal clipboard to that of the local clipboard (if local
|
||||
* clipboard access is granted) and broadcasting a "guacClipboard" event
|
||||
* with the current internal clipboard contents for consumption by external
|
||||
* components like the "guacClient" directive.
|
||||
*/
|
||||
service.resyncClipboard = function resyncClipboard() {
|
||||
getLocalClipboard().then(function clipboardRead(data) {
|
||||
return service.setClipboard(data);
|
||||
}, angular.noop);
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.clipboard, .clipboard-service-target {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.clipboard {
|
||||
position: relative;
|
||||
border: 1px solid #AAA;
|
||||
-moz-border-radius: 0.25em;
|
||||
-webkit-border-radius: 0.25em;
|
||||
-khtml-border-radius: 0.25em;
|
||||
border-radius: 0.25em;
|
||||
width: 100%;
|
||||
height: 2in;
|
||||
white-space: pre;
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
.clipboard p,
|
||||
.clipboard div {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clipboard img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
border: 1px solid black;
|
||||
background: url('images/checker.svg');
|
||||
}
|
||||
|
||||
.clipboard-service-target {
|
||||
position: fixed;
|
||||
left: -1em;
|
||||
right: -1em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clipboard-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clipboard-editor .clipboard {
|
||||
overflow: auto;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.clipboard-editor .clipboard.clipboard-contents-hidden {
|
||||
color: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clipboard-editor .clipboard-contents-hidden-hint {
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.clipboard-editor .clipboard-contents-hidden-hint .clipboard-contents-hidden-hint-text {
|
||||
|
||||
flex: 1;
|
||||
|
||||
background: rgba(0, 0, 0, 0.125);
|
||||
color: #888;
|
||||
|
||||
padding: 0.5em;
|
||||
overflow: hidden;
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
|
||||
}
|
||||
|
||||
.clipboard-editor .clipboard-contents-hidden-hint:hover .clipboard-contents-hidden-hint-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="clipboard-editor">
|
||||
<textarea class="clipboard"
|
||||
ng-class="{
|
||||
'clipboard-contents-hidden' : !contentsShown
|
||||
}"
|
||||
ng-disabled="!contentsShown"></textarea>
|
||||
<div class="clipboard-contents-hidden-hint"
|
||||
ng-click="showContents()"
|
||||
ng-show="!contentsShown">
|
||||
<p class="clipboard-contents-hidden-hint-text">{{ 'CLIENT.ACTION_SHOW_CLIPBOARD' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ClipboardData class used for interchange between the
|
||||
* guacClipboard directive, clipboardService service, etc.
|
||||
*/
|
||||
angular.module('clipboard').factory('ClipboardData', [function defineClipboardData() {
|
||||
|
||||
/**
|
||||
* Arbitrary data which can be contained by the clipboard.
|
||||
*
|
||||
* @constructor
|
||||
* @param {ClipboardData|Object} [template={}]
|
||||
* The object whose properties should be copied within the new
|
||||
* ClipboardData.
|
||||
*/
|
||||
var ClipboardData = function ClipboardData(template) {
|
||||
|
||||
// Use empty object by default
|
||||
template = template || {};
|
||||
|
||||
/**
|
||||
* The ID of the ManagedClient handling the remote desktop connection
|
||||
* that originated this clipboard data, or null if the data originated
|
||||
* from the clipboard editor or local clipboard.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
this.source = template.source;
|
||||
|
||||
/**
|
||||
* The mimetype of the data currently stored within the clipboard.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
this.type = template.type || 'text/plain';
|
||||
|
||||
/**
|
||||
* The data currently stored within the clipboard. Depending on the
|
||||
* nature of the stored data, this may be either a String, a Blob, or a
|
||||
* File.
|
||||
*
|
||||
* @type String|Blob|File
|
||||
*/
|
||||
this.data = template.data || '';
|
||||
|
||||
};
|
||||
|
||||
return ClipboardData;
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which provides handling of click and click-like touch events.
|
||||
* The state of Shift and Ctrl modifiers is tracked through these click events
|
||||
* to allow for specific handling of Shift+Click and Ctrl+Click.
|
||||
*/
|
||||
angular.module('element').directive('guacClick', [function guacClick() {
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
link: function linkGuacClick($scope, $element, $attrs) {
|
||||
|
||||
/**
|
||||
* A callback that is invoked by the guacClick directive when a
|
||||
* click or click-like event is received.
|
||||
*
|
||||
* @callback guacClick~callback
|
||||
* @param {boolean} shift
|
||||
* Whether Shift was held down at the time the click occurred.
|
||||
*
|
||||
* @param {boolean} ctrl
|
||||
* Whether Ctrl or Meta (the Mac "Command" key) was held down
|
||||
* at the time the click occurred.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The callback to invoke when a click or click-like event is
|
||||
* received on the associated element.
|
||||
*
|
||||
* @type guacClick~callback
|
||||
*/
|
||||
const guacClick = $scope.$eval($attrs.guacClick);
|
||||
|
||||
/**
|
||||
* The element which will register the click.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
const element = $element[0];
|
||||
|
||||
/**
|
||||
* Whether either Shift key is currently pressed.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
let shift = false;
|
||||
|
||||
/**
|
||||
* Whether either Ctrl key is currently pressed. To allow the
|
||||
* Command key to be used on Mac platforms, this flag also
|
||||
* considers the state of either Meta key.
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
let ctrl = false;
|
||||
|
||||
/**
|
||||
* Updates the state of the {@link shift} and {@link ctrl} flags
|
||||
* based on which keys are currently marked as held down by the
|
||||
* given Guacamole.Keyboard.
|
||||
*
|
||||
* @param {Guacamole.Keyboard} keyboard
|
||||
* The Guacamole.Keyboard instance to read key states from.
|
||||
*/
|
||||
const updateModifiers = function updateModifiers(keyboard) {
|
||||
|
||||
shift = !!(
|
||||
keyboard.pressed[0xFFE1] // Left shift
|
||||
|| keyboard.pressed[0xFFE2] // Right shift
|
||||
);
|
||||
|
||||
ctrl = !!(
|
||||
keyboard.pressed[0xFFE3] // Left ctrl
|
||||
|| keyboard.pressed[0xFFE4] // Right ctrl
|
||||
|| keyboard.pressed[0xFFE7] // Left meta (command)
|
||||
|| keyboard.pressed[0xFFE8] // Right meta (command)
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
// Update tracking of modifier states for each key press
|
||||
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
|
||||
updateModifiers(keyboard);
|
||||
});
|
||||
|
||||
// Update tracking of modifier states for each key release
|
||||
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
|
||||
updateModifiers(keyboard);
|
||||
});
|
||||
|
||||
// Fire provided callback for each mouse-initiated "click" event ...
|
||||
element.addEventListener('click', function elementClicked(e) {
|
||||
if (element.contains(e.target))
|
||||
$scope.$apply(() => guacClick(shift, ctrl));
|
||||
});
|
||||
|
||||
// ... and for touch-initiated click-like events
|
||||
element.addEventListener('touchstart', function elementClicked(e) {
|
||||
if (element.contains(e.target))
|
||||
$scope.$apply(() => guacClick(shift, ctrl));
|
||||
});
|
||||
|
||||
} // end guacClick link function
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which allows multiple files to be uploaded. Dragging files onto
|
||||
* the associated element will call the provided callback function with any
|
||||
* dragged files.
|
||||
*/
|
||||
angular.module('element').directive('guacDrop', ['$injector', function guacDrop($injector) {
|
||||
|
||||
// Required services
|
||||
const guacNotification = $injector.get('guacNotification');
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
link: function linkGuacDrop($scope, $element, $attrs) {
|
||||
|
||||
/**
|
||||
* The function to call whenever files are dragged. The callback is
|
||||
* provided a single parameter: the FileList containing all dragged
|
||||
* files.
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
const guacDrop = $scope.$eval($attrs.guacDrop);
|
||||
|
||||
/**
|
||||
* Any number of space-seperated classes to be applied to the
|
||||
* element a drop is pending: when the user has dragged something
|
||||
* over the element, but not yet dropped. These classes will be
|
||||
* removed when a drop is not pending.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
const guacDraggedClass = $scope.$eval($attrs.guacDraggedClass);
|
||||
|
||||
/**
|
||||
* Whether upload of multiple files should be allowed. If false, an
|
||||
* error will be displayed explaining the restriction, otherwise
|
||||
* any number of files may be dragged. Defaults to true if not set.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
const guacMultiple = 'guacMultiple' in $attrs
|
||||
? $scope.$eval($attrs.guacMultiple) : true;
|
||||
|
||||
/**
|
||||
* The element which will register drag event.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
const element = $element[0];
|
||||
|
||||
/**
|
||||
* Applies any classes provided in the guacDraggedClass attribute.
|
||||
* Further propagation and default behavior of the given event is
|
||||
* automatically prevented.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event related to the in-progress drag/drop operation.
|
||||
*/
|
||||
const notifyDragStart = function notifyDragStart(e) {
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Skip further processing if no classes were provided
|
||||
if (!guacDraggedClass)
|
||||
return;
|
||||
|
||||
// Add each provided class
|
||||
guacDraggedClass.split(' ').forEach(classToApply =>
|
||||
element.classList.add(classToApply));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes any classes provided in the guacDraggedClass attribute.
|
||||
* Further propagation and default behavior of the given event is
|
||||
* automatically prevented.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event related to the end of the drag/drop operation.
|
||||
*/
|
||||
const notifyDragEnd = function notifyDragEnd(e) {
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Skip further processing if no classes were provided
|
||||
if (!guacDraggedClass)
|
||||
return;
|
||||
|
||||
// Remove each provided class
|
||||
guacDraggedClass.split(' ').forEach(classToRemove =>
|
||||
element.classList.remove(classToRemove));
|
||||
|
||||
};
|
||||
|
||||
// Add listeners to the drop target to ensure that the visual state
|
||||
// stays up to date
|
||||
element.addEventListener('dragenter', notifyDragStart);
|
||||
element.addEventListener('dragover', notifyDragStart);
|
||||
element.addEventListener('dragleave', notifyDragEnd);
|
||||
|
||||
/**
|
||||
* Event listener that will be invoked if the user drops anything
|
||||
* onto the event. If a valid file is provided, the onFile callback
|
||||
* provided to this directive will be called; otherwise an error
|
||||
* will be displayed, if appropriate.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The drop event that triggered this handler.
|
||||
*/
|
||||
element.addEventListener('drop', e => {
|
||||
|
||||
notifyDragEnd(e);
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
|
||||
// Ignore any non-files that are dragged into the drop area
|
||||
if (files.length < 1)
|
||||
return;
|
||||
|
||||
// If multi-file upload is disabled, If more than one file was
|
||||
// provided, print an error explaining the problem
|
||||
if (!guacMultiple && files.length >= 2) {
|
||||
|
||||
guacNotification.showStatus({
|
||||
className : 'error',
|
||||
title : 'APP.DIALOG_HEADER_ERROR',
|
||||
text: { key : 'APP.ERROR_SINGLE_FILE_ONLY'},
|
||||
|
||||
// Add a button to hide the error
|
||||
actions : [{
|
||||
name : 'APP.ACTION_ACKNOWLEDGE',
|
||||
callback : () => guacNotification.showStatus(false)
|
||||
}]
|
||||
});
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Invoke the callback with the files. Note that if guacMultiple
|
||||
// is set to false, this will always be a single file.
|
||||
guacDrop(files);
|
||||
|
||||
});
|
||||
|
||||
} // end guacDrop link function
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which allows elements to be manually focused / blurred.
|
||||
*/
|
||||
angular.module('element').directive('guacFocus', ['$injector', function guacFocus($injector) {
|
||||
|
||||
// Required services
|
||||
var $parse = $injector.get('$parse');
|
||||
var $timeout = $injector.get('$timeout');
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
link: function linkGuacFocus($scope, $element, $attrs) {
|
||||
|
||||
/**
|
||||
* Whether the element associated with this directive should be
|
||||
* focussed.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
var guacFocus = $parse($attrs.guacFocus);
|
||||
|
||||
/**
|
||||
* The element which will be focused / blurred.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var element = $element[0];
|
||||
|
||||
// Set/unset focus depending on value of guacFocus
|
||||
$scope.$watch(guacFocus, function updateFocus(value) {
|
||||
$timeout(function updateFocusAfterRender() {
|
||||
if (value)
|
||||
element.focus();
|
||||
else
|
||||
element.blur();
|
||||
});
|
||||
});
|
||||
|
||||
} // end guacFocus link function
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which stores a marker which refers to a specific element,
|
||||
* allowing that element to be scrolled into view when desired.
|
||||
*/
|
||||
angular.module('element').directive('guacMarker', ['$injector', function guacMarker($injector) {
|
||||
|
||||
// Required types
|
||||
var Marker = $injector.get('Marker');
|
||||
|
||||
// Required services
|
||||
var $parse = $injector.get('$parse');
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
link: function linkGuacMarker($scope, $element, $attrs) {
|
||||
|
||||
/**
|
||||
* The property in which a new Marker should be stored. The new
|
||||
* Marker will refer to the element associated with this directive.
|
||||
*
|
||||
* @type Marker
|
||||
*/
|
||||
var guacMarker = $parse($attrs.guacMarker);
|
||||
|
||||
/**
|
||||
* The element to associate with the new Marker.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var element = $element[0];
|
||||
|
||||
// Assign new marker
|
||||
guacMarker.assign($scope, new Marker(element));
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which calls a given callback when its associated element is
|
||||
* resized. This will modify the internal DOM tree of the associated element,
|
||||
* and the associated element MUST have position (for example,
|
||||
* "position: relative").
|
||||
*/
|
||||
angular.module('element').directive('guacResize', ['$document', function guacResize($document) {
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
link: function linkGuacResize($scope, $element, $attrs) {
|
||||
|
||||
/**
|
||||
* The function to call whenever the associated element is
|
||||
* resized. The function will be passed the width and height of
|
||||
* the element, in pixels.
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
var guacResize = $scope.$eval($attrs.guacResize);
|
||||
|
||||
/**
|
||||
* The element which will monitored for size changes.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var element = $element[0];
|
||||
|
||||
/**
|
||||
* The resize sensor - an HTML object element.
|
||||
*
|
||||
* @type HTMLObjectElement
|
||||
*/
|
||||
var resizeSensor = $document[0].createElement('object');
|
||||
|
||||
/**
|
||||
* The width of the associated element, in pixels.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var lastWidth = element.offsetWidth;
|
||||
|
||||
/**
|
||||
* The height of the associated element, in pixels.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var lastHeight = element.offsetHeight;
|
||||
|
||||
/**
|
||||
* Checks whether the size of the associated element has changed
|
||||
* and, if so, calls the resize callback with the new width and
|
||||
* height as parameters.
|
||||
*/
|
||||
var checkSize = function checkSize() {
|
||||
|
||||
// Call callback only if size actually changed
|
||||
if (element.offsetWidth !== lastWidth
|
||||
|| element.offsetHeight !== lastHeight) {
|
||||
|
||||
// Call resize callback, if defined
|
||||
if (guacResize) {
|
||||
$scope.$evalAsync(function elementSizeChanged() {
|
||||
guacResize(element.offsetWidth, element.offsetHeight);
|
||||
});
|
||||
}
|
||||
|
||||
// Update stored size
|
||||
lastWidth = element.offsetWidth;
|
||||
lastHeight = element.offsetHeight;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Register event listener once window object exists
|
||||
resizeSensor.onload = function resizeSensorReady() {
|
||||
resizeSensor.contentDocument.defaultView.addEventListener('resize', checkSize);
|
||||
checkSize();
|
||||
};
|
||||
|
||||
// Load blank contents
|
||||
resizeSensor.className = 'resize-sensor';
|
||||
resizeSensor.type = 'text/html';
|
||||
resizeSensor.data = 'app/element/templates/blank.html';
|
||||
|
||||
// Add resize sensor to associated element
|
||||
element.insertBefore(resizeSensor, element.firstChild);
|
||||
|
||||
} // end guacResize link function
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which allows elements to be manually scrolled, and for their
|
||||
* scroll state to be observed.
|
||||
*/
|
||||
angular.module('element').directive('guacScroll', [function guacScroll() {
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
link: function linkGuacScroll($scope, $element, $attrs) {
|
||||
|
||||
/**
|
||||
* The current scroll state of the element.
|
||||
*
|
||||
* @type ScrollState
|
||||
*/
|
||||
var guacScroll = $scope.$eval($attrs.guacScroll);
|
||||
|
||||
/**
|
||||
* The element which is being scrolled, or monitored for changes
|
||||
* in scroll.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var element = $element[0];
|
||||
|
||||
/**
|
||||
* Returns the current left edge of the scrolling rectangle.
|
||||
*
|
||||
* @returns {Number}
|
||||
* The current left edge of the scrolling rectangle.
|
||||
*/
|
||||
var getScrollLeft = function getScrollLeft() {
|
||||
return guacScroll.left;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current top edge of the scrolling rectangle.
|
||||
*
|
||||
* @returns {Number}
|
||||
* The current top edge of the scrolling rectangle.
|
||||
*/
|
||||
var getScrollTop = function getScrollTop() {
|
||||
return guacScroll.top;
|
||||
};
|
||||
|
||||
// Update underlying scrollLeft property when left changes
|
||||
$scope.$watch(getScrollLeft, function scrollLeftChanged(left) {
|
||||
element.scrollLeft = left;
|
||||
guacScroll.left = element.scrollLeft;
|
||||
});
|
||||
|
||||
// Update underlying scrollTop property when top changes
|
||||
$scope.$watch(getScrollTop, function scrollTopChanged(top) {
|
||||
element.scrollTop = top;
|
||||
guacScroll.top = element.scrollTop;
|
||||
});
|
||||
|
||||
} // end guacScroll link function
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which allows files to be uploaded. Clicking on the associated
|
||||
* element will result in a file selector dialog, which then calls the provided
|
||||
* callback function with any chosen files.
|
||||
*/
|
||||
angular.module('element').directive('guacUpload', ['$document', function guacUpload($document) {
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
link: function linkGuacUpload($scope, $element, $attrs) {
|
||||
|
||||
/**
|
||||
* The function to call whenever files are chosen. The callback is
|
||||
* provided a single parameter: the FileList containing all chosen
|
||||
* files.
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
const guacUpload = $scope.$eval($attrs.guacUpload);
|
||||
|
||||
/**
|
||||
* Whether upload of multiple files should be allowed. If false, the
|
||||
* file dialog will only allow a single file to be chosen at once,
|
||||
* otherwise any number of files may be chosen. Defaults to true if
|
||||
* not set.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
const guacMultiple = 'guacMultiple' in $attrs
|
||||
? $scope.$eval($attrs.guacMultiple) : true;
|
||||
|
||||
/**
|
||||
* The element which will register the click.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
const element = $element[0];
|
||||
|
||||
/**
|
||||
* Internal form, containing a single file input element.
|
||||
*
|
||||
* @type HTMLFormElement
|
||||
*/
|
||||
const form = $document[0].createElement('form');
|
||||
|
||||
/**
|
||||
* Internal file input element.
|
||||
*
|
||||
* @type HTMLInputElement
|
||||
*/
|
||||
const input = $document[0].createElement('input');
|
||||
|
||||
// Init input element
|
||||
input.type = 'file';
|
||||
input.multiple = guacMultiple;
|
||||
|
||||
// Add input element to internal form
|
||||
form.appendChild(input);
|
||||
|
||||
// Notify of any chosen files
|
||||
input.addEventListener('change', function filesSelected() {
|
||||
$scope.$apply(function setSelectedFiles() {
|
||||
|
||||
// Only set chosen files selection is not canceled
|
||||
if (guacUpload && input.files.length > 0)
|
||||
guacUpload(input.files);
|
||||
|
||||
// Reset selection
|
||||
form.reset();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
// Open file chooser when element is clicked
|
||||
element.addEventListener('click', function elementClicked() {
|
||||
input.click();
|
||||
});
|
||||
|
||||
} // end guacUpload link function
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
24
guacamole/src/main/frontend/src/app/element/elementModule.js
Normal file
24
guacamole/src/main/frontend/src/app/element/elementModule.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module for manipulating element state, such as focus or scroll position, as
|
||||
* well as handling browser events.
|
||||
*/
|
||||
angular.module('element', []);
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
.resize-sensor {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user