mirror of
https://thingvellir.net/git/overte
synced 2025-03-27 23:52:03 +01:00
Merge branch 'screenshareElectronApp' of github.com:MiladNazeri/hifi into MiladNazeri-screenshareElectronApp
This commit is contained in:
commit
4af1ddf48b
21 changed files with 3310 additions and 6 deletions
|
@ -256,6 +256,7 @@ endif()
|
|||
|
||||
if (BUILD_CLIENT)
|
||||
add_subdirectory(interface)
|
||||
add_subdirectory(screenshare)
|
||||
set_target_properties(interface PROPERTIES FOLDER "Apps")
|
||||
|
||||
option(USE_SIXENSE "Build Interface with sixense library/plugin" OFF)
|
||||
|
|
|
@ -146,23 +146,27 @@ macro(SET_PACKAGING_PARAMETERS)
|
|||
|
||||
set(DMG_SUBFOLDER_ICON "${HF_CMAKE_DIR}/installer/install-folder.rsrc")
|
||||
|
||||
set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
|
||||
set(INTERFACE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
|
||||
set(NITPICK_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
|
||||
set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
|
||||
set(INTERFACE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
|
||||
set(SCREENSHARE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
|
||||
set(NITPICK_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
|
||||
|
||||
if (CLIENT_ONLY)
|
||||
set(CONSOLE_EXEC_NAME "Console.app")
|
||||
else ()
|
||||
set(CONSOLE_EXEC_NAME "Sandbox.app")
|
||||
endif()
|
||||
|
||||
set(CONSOLE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${CONSOLE_EXEC_NAME}")
|
||||
|
||||
set(SCREENSHARE_EXEC_NAME "hifi-screenshare.app")
|
||||
set(SCREENSHARE_INSTALL_APP_PATH "${SCREENSHARE_INSTALL_DIR}/${SCREENSHARE_EXEC_NAME}")
|
||||
|
||||
set(CONSOLE_APP_CONTENTS "${CONSOLE_INSTALL_APP_PATH}/Contents")
|
||||
set(COMPONENT_APP_PATH "${CONSOLE_APP_CONTENTS}/MacOS/Components.app")
|
||||
set(COMPONENT_INSTALL_DIR "${COMPONENT_APP_PATH}/Contents/MacOS")
|
||||
set(CONSOLE_PLUGIN_INSTALL_DIR "${COMPONENT_APP_PATH}/Contents/PlugIns")
|
||||
|
||||
|
||||
set(SCREENSHARE_APP_CONTENTS "${SCREENSHARE_INSTALL_APP_PATH}/Contents")
|
||||
|
||||
set(INTERFACE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${INTERFACE_BUNDLE_NAME}.app")
|
||||
set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.icns")
|
||||
|
@ -170,9 +174,11 @@ macro(SET_PACKAGING_PARAMETERS)
|
|||
else ()
|
||||
if (WIN32)
|
||||
set(CONSOLE_INSTALL_DIR "server-console")
|
||||
set(SCREENSHARE_INSTALL_DIR "hifi-screenshare")
|
||||
set(NITPICK_INSTALL_DIR "nitpick")
|
||||
else ()
|
||||
set(CONSOLE_INSTALL_DIR ".")
|
||||
set(SCREENSHARE_INSTALL_DIR ".")
|
||||
set(NITPICK_INSTALL_DIR ".")
|
||||
endif ()
|
||||
|
||||
|
@ -186,6 +192,7 @@ macro(SET_PACKAGING_PARAMETERS)
|
|||
set(NITPICK_ICON_FILENAME "${NITPICK_ICON_PREFIX}.ico")
|
||||
|
||||
set(CONSOLE_EXEC_NAME "server-console.exe")
|
||||
set(SCREENSHARE_EXEC_NAME "hifi-screenshare.exe")
|
||||
|
||||
set(DS_EXEC_NAME "domain-server.exe")
|
||||
set(AC_EXEC_NAME "assignment-client.exe")
|
||||
|
|
|
@ -49,4 +49,6 @@ Item {
|
|||
Component.onCompleted: {
|
||||
load(root.url, root.scriptUrl);
|
||||
}
|
||||
|
||||
signal sendToScript(var message);
|
||||
}
|
||||
|
|
|
@ -184,6 +184,7 @@
|
|||
#include "scripting/AssetMappingsScriptingInterface.h"
|
||||
#include "scripting/ClipboardScriptingInterface.h"
|
||||
#include "scripting/DesktopScriptingInterface.h"
|
||||
#include "scripting/ScreenshareScriptingInterface.h"
|
||||
#include "scripting/AccountServicesScriptingInterface.h"
|
||||
#include "scripting/HMDScriptingInterface.h"
|
||||
#include "scripting/MenuScriptingInterface.h"
|
||||
|
@ -888,6 +889,11 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
|
|||
DependencyManager::set<ScriptCache>();
|
||||
DependencyManager::set<SoundCache>();
|
||||
DependencyManager::set<SoundCacheScriptingInterface>();
|
||||
|
||||
#ifdef HAVE_DDE
|
||||
DependencyManager::set<DdeFaceTracker>();
|
||||
#endif
|
||||
DependencyManager::set<ScreenshareScriptingInterface>();
|
||||
DependencyManager::set<AudioClient>();
|
||||
DependencyManager::set<AudioScope>();
|
||||
DependencyManager::set<DeferredLightingEffect>();
|
||||
|
@ -2919,6 +2925,7 @@ Application::~Application() {
|
|||
DependencyManager::destroy<SoundCache>();
|
||||
DependencyManager::destroy<OctreeStatsProvider>();
|
||||
DependencyManager::destroy<GeometryCache>();
|
||||
DependencyManager::destroy<ScreenshareScriptingInterface>();
|
||||
|
||||
DependencyManager::get<ResourceManager>()->cleanup();
|
||||
|
||||
|
@ -3430,7 +3437,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) {
|
|||
surfaceContext->setContextProperty("Users", DependencyManager::get<UsersScriptingInterface>().data());
|
||||
|
||||
surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
|
||||
|
||||
surfaceContext->setContextProperty("Screenshare", DependencyManager::get<ScreenshareScriptingInterface>().data());
|
||||
surfaceContext->setContextProperty("Camera", &_myCamera);
|
||||
|
||||
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
|
||||
|
@ -3536,6 +3543,7 @@ void Application::userKickConfirmation(const QUuid& nodeID) {
|
|||
}
|
||||
|
||||
void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) {
|
||||
surfaceContext->setContextProperty("Screenshare", DependencyManager::get<ScreenshareScriptingInterface>().data());
|
||||
surfaceContext->setContextProperty("Users", DependencyManager::get<UsersScriptingInterface>().data());
|
||||
surfaceContext->setContextProperty("HMD", DependencyManager::get<HMDScriptingInterface>().data());
|
||||
surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
|
||||
|
@ -7314,6 +7322,7 @@ void Application::registerScriptEngineWithApplicationServices(const ScriptEngine
|
|||
scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get<AvatarManager>().data());
|
||||
|
||||
scriptEngine->registerGlobalObject("Camera", &_myCamera);
|
||||
scriptEngine->registerGlobalObject("Screenshare", DependencyManager::get<ScreenshareScriptingInterface>().data());
|
||||
|
||||
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
|
||||
scriptEngine->registerGlobalObject("SpeechRecognizer", DependencyManager::get<SpeechRecognizer>().data());
|
||||
|
|
174
interface/src/scripting/ScreenshareScriptingInterface.cpp
Normal file
174
interface/src/scripting/ScreenshareScriptingInterface.cpp
Normal file
|
@ -0,0 +1,174 @@
|
|||
//
|
||||
// ScreenshareScriptingInterface.cpp
|
||||
// interface/src/scripting/
|
||||
//
|
||||
// Created by Milad Nazeri on 2019-10-23.
|
||||
// Copyright 2019 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QJsonDocument>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
|
||||
#include <AddressManager.h>
|
||||
|
||||
#include "EntityScriptingInterface.h"
|
||||
#include "ScreenshareScriptingInterface.h"
|
||||
|
||||
ScreenshareScriptingInterface::ScreenshareScriptingInterface() {
|
||||
};
|
||||
|
||||
ScreenshareScriptingInterface::~ScreenshareScriptingInterface() {
|
||||
stopScreenshare();
|
||||
}
|
||||
|
||||
static const EntityTypes::EntityType LOCAL_SCREENSHARE_WEB_ENTITY_TYPE = EntityTypes::Web;
|
||||
static const uint8_t LOCAL_SCREENSHARE_WEB_ENTITY_FPS = 30;
|
||||
static const glm::vec3 LOCAL_SCREENSHARE_WEB_ENTITY_LOCAL_POSITION(0.0f, 0.0f, 0.1f);
|
||||
static const QString LOCAL_SCREENSHARE_WEB_ENTITY_URL = "https://hifi-content.s3.amazonaws.com/Experiences/Releases/usefulUtilities/smartBoard/screenshareViewer/screenshareClient.html?1";
|
||||
void ScreenshareScriptingInterface::startScreenshare(const QUuid& screenshareZoneID, const QUuid& smartboardEntityID, const bool& isPresenter) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
// We must start a new QProcess from the main thread.
|
||||
QMetaObject::invokeMethod(
|
||||
this, "startScreenshare",
|
||||
Q_ARG(const QUuid&, screenshareZoneID),
|
||||
Q_ARG(const QUuid&, smartboardEntityID),
|
||||
Q_ARG(const bool&, isPresenter)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPresenter && _screenshareProcess && _screenshareProcess->state() != QProcess::NotRunning) {
|
||||
qDebug() << "Screenshare process already running. Aborting...";
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPresenter) {
|
||||
_screenshareProcess.reset(new QProcess(this));
|
||||
|
||||
QFileInfo screenshareExecutable(SCREENSHARE_EXE_PATH);
|
||||
if (!screenshareExecutable.exists() || !screenshareExecutable.isFile()) {
|
||||
qDebug() << "Screenshare executable doesn't exist at" << SCREENSHARE_EXE_PATH;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QUuid currentDomainID = DependencyManager::get<AddressManager>()->getDomainID();
|
||||
|
||||
// Make HTTP GET request to:
|
||||
// `https://metaverse.highfidelity.com/api/v1/domain/:domain_id/screenshare`,
|
||||
// passing the Domain ID that the user is connected to, as well as the `roomName`.
|
||||
// The server will respond with the relevant OpenTok Token, Session ID, and API Key.
|
||||
// Upon error-free response, do the logic below, passing in that info as necessary.
|
||||
QString token = "";
|
||||
QString apiKey = "";
|
||||
QString sessionID = "";
|
||||
|
||||
if (isPresenter) {
|
||||
QStringList arguments;
|
||||
arguments << "--token=" + token;
|
||||
arguments << "--apiKey=" + apiKey;
|
||||
arguments << "--sessionID=" + sessionID;
|
||||
|
||||
connect(_screenshareProcess.get(), &QProcess::errorOccurred,
|
||||
[=](QProcess::ProcessError error) { qDebug() << "ZRF QProcess::errorOccurred. `error`:" << error; });
|
||||
connect(_screenshareProcess.get(), &QProcess::started, [=]() { qDebug() << "ZRF QProcess::started"; });
|
||||
connect(_screenshareProcess.get(), &QProcess::stateChanged,
|
||||
[=](QProcess::ProcessState newState) { qDebug() << "ZRF QProcess::stateChanged. `newState`:" << newState; });
|
||||
connect(_screenshareProcess.get(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
[=](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
qDebug() << "ZRF QProcess::finished. `exitCode`:" << exitCode << "`exitStatus`:" << exitStatus;
|
||||
emit screenshareStopped();
|
||||
});
|
||||
|
||||
_screenshareProcess->start(SCREENSHARE_EXE_PATH, arguments);
|
||||
}
|
||||
|
||||
if (!_screenshareViewerLocalWebEntityUUID.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto esi = DependencyManager::get<EntityScriptingInterface>();
|
||||
if (!esi) {
|
||||
return;
|
||||
}
|
||||
|
||||
EntityItemProperties localScreenshareWebEntityProps;
|
||||
localScreenshareWebEntityProps.setType(LOCAL_SCREENSHARE_WEB_ENTITY_TYPE);
|
||||
localScreenshareWebEntityProps.setMaxFPS(LOCAL_SCREENSHARE_WEB_ENTITY_FPS);
|
||||
localScreenshareWebEntityProps.setLocalPosition(LOCAL_SCREENSHARE_WEB_ENTITY_LOCAL_POSITION);
|
||||
localScreenshareWebEntityProps.setSourceUrl(LOCAL_SCREENSHARE_WEB_ENTITY_URL);
|
||||
localScreenshareWebEntityProps.setParentID(smartboardEntityID);
|
||||
|
||||
EntityPropertyFlags desiredSmartboardProperties;
|
||||
desiredSmartboardProperties += PROP_POSITION;
|
||||
desiredSmartboardProperties += PROP_DIMENSIONS;
|
||||
EntityItemProperties smartboardProps = esi->getEntityProperties(smartboardEntityID, desiredSmartboardProperties);
|
||||
|
||||
localScreenshareWebEntityProps.setPosition(smartboardProps.getPosition());
|
||||
localScreenshareWebEntityProps.setDimensions(smartboardProps.getDimensions());
|
||||
|
||||
QString hostType = "local";
|
||||
_screenshareViewerLocalWebEntityUUID = esi->addEntity(localScreenshareWebEntityProps, hostType);
|
||||
|
||||
QObject::connect(esi.data(), &EntityScriptingInterface::webEventReceived, this, [&](const QUuid& entityID, const QVariant& message) {
|
||||
if (entityID == _screenshareViewerLocalWebEntityUUID) {
|
||||
qDebug() << "ZRF HERE! Inside `webEventReceived(). `entityID`:" << entityID << "`_screenshareViewerLocalWebEntityUUID`:" << _screenshareViewerLocalWebEntityUUID;
|
||||
|
||||
auto esi = DependencyManager::get<EntityScriptingInterface>();
|
||||
if (!esi) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonDocument jsonMessage = QJsonDocument::fromVariant(message);
|
||||
QJsonObject jsonObject = jsonMessage.object();
|
||||
|
||||
qDebug() << "ZRF HERE! Inside `webEventReceived(). `message`:" << message << "`jsonMessage`:" << jsonMessage;
|
||||
|
||||
if (jsonObject["app"] != "screenshare") {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "ZRF HERE! Inside `webEventReceived(). we're still here!";
|
||||
|
||||
if (jsonObject["method"] == "eventBridgeReady") {
|
||||
QJsonObject responseObject;
|
||||
responseObject.insert("app", "screenshare");
|
||||
responseObject.insert("method", "receiveConnectionInfo");
|
||||
QJsonObject responseObjectData;
|
||||
responseObjectData.insert("token", token);
|
||||
responseObjectData.insert("projectAPIKey", apiKey);
|
||||
responseObjectData.insert("sessionID", sessionID);
|
||||
responseObject.insert("data", responseObjectData);
|
||||
|
||||
qDebug() << "ZRF HERE! Inside `webEventReceived(). `responseObject.toVariantMap()`:" << responseObject.toVariantMap();
|
||||
|
||||
esi->emitScriptEvent(_screenshareViewerLocalWebEntityUUID, responseObject.toVariantMap());
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
void ScreenshareScriptingInterface::stopScreenshare() {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "stopScreenshare");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_screenshareProcess && _screenshareProcess->state() != QProcess::NotRunning) {
|
||||
_screenshareProcess->terminate();
|
||||
}
|
||||
|
||||
if (!_screenshareViewerLocalWebEntityUUID.isNull()) {
|
||||
auto esi = DependencyManager::get<EntityScriptingInterface>();
|
||||
if (esi) {
|
||||
esi->deleteEntity(_screenshareViewerLocalWebEntityUUID);
|
||||
}
|
||||
}
|
||||
_screenshareViewerLocalWebEntityUUID = "{00000000-0000-0000-0000-000000000000}";
|
||||
}
|
49
interface/src/scripting/ScreenshareScriptingInterface.h
Normal file
49
interface/src/scripting/ScreenshareScriptingInterface.h
Normal file
|
@ -0,0 +1,49 @@
|
|||
#ifndef hifi_ScreenshareScriptingInterface_h
|
||||
#define hifi_ScreenshareScriptingInterface_h
|
||||
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QtCore/QCoreApplication>
|
||||
|
||||
#include <DependencyManager.h>
|
||||
#include <PathUtils.h>
|
||||
|
||||
class ScreenshareScriptingInterface : public QObject, public Dependency {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ScreenshareScriptingInterface();
|
||||
~ScreenshareScriptingInterface();
|
||||
|
||||
Q_INVOKABLE void startScreenshare(const QUuid& screenshareZoneID, const QUuid& smartboardEntityID, const bool& isPresenter = false);
|
||||
Q_INVOKABLE void stopScreenshare();
|
||||
|
||||
signals:
|
||||
void screenshareStopped();
|
||||
void startScreenshareViewer();
|
||||
|
||||
private:
|
||||
#if DEV_BUILD
|
||||
#ifdef Q_OS_WIN
|
||||
const QString SCREENSHARE_EXE_PATH{ PathUtils::projectRootPath() + "/screenshare/hifi-screenshare-win32-x64/hifi-screenshare.exe" };
|
||||
#elif defined(Q_OS_MAC)
|
||||
const QString SCREENSHARE_EXE_PATH{ PathUtils::projectRootPath() + "/screenshare/screenshare-darwin-x64/hifi-screenshare.app" };
|
||||
#else
|
||||
// This path won't exist on other platforms, so the Screenshare Scripting Interface will exit early when invoked.
|
||||
const QString SCREENSHARE_EXE_PATH{ PathUtils::projectRootPath() + "/screenshare/screenshare-other-os/hifi-screenshare" };
|
||||
#endif
|
||||
#else
|
||||
#ifdef Q_OS_WIN
|
||||
const QString SCREENSHARE_EXE_PATH{ QCoreApplication::applicationDirPath() + "/hifi-screenshare/hifi-screenshare.exe" };
|
||||
#elif defined(Q_OS_MAC)
|
||||
const QString SCREENSHARE_EXE_PATH{ QCoreApplication::applicationDirPath() + "/hifi-screenshare/hifi-screenshare.app" };
|
||||
#else
|
||||
// This path won't exist on other platforms, so the Screenshare Scripting Interface will exit early when invoked.
|
||||
const QString SCREENSHARE_EXE_PATH{ QCoreApplication::applicationDirPath() + "/hifi-screenshare/hifi-screenshare" };
|
||||
#endif
|
||||
#endif
|
||||
|
||||
std::unique_ptr<QProcess> _screenshareProcess{ nullptr };
|
||||
QUuid _screenshareViewerLocalWebEntityUUID;
|
||||
};
|
||||
|
||||
#endif // hifi_ScreenshareScriptingInterface_h
|
4
screenshare/.gitignore
vendored
Normal file
4
screenshare/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
hifi-screenshare-*/
|
||||
hifi-screenshare*.zip
|
||||
screenshare*.zip
|
||||
screenshare-*/
|
45
screenshare/CMakeLists.txt
Normal file
45
screenshare/CMakeLists.txt
Normal file
|
@ -0,0 +1,45 @@
|
|||
set(TARGET_NAME screenshare)
|
||||
|
||||
add_custom_target(${TARGET_NAME}-npm-install
|
||||
COMMAND npm install
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
add_custom_target(${TARGET_NAME}
|
||||
COMMAND npm run packager -- --out ${CMAKE_CURRENT_BINARY_DIR}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
DEPENDS ${TARGET_NAME}-npm-install
|
||||
)
|
||||
|
||||
set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "screenshare")
|
||||
set_target_properties(${TARGET_NAME}-npm-install PROPERTIES FOLDER "hidden/screenshare")
|
||||
|
||||
if (WIN32)
|
||||
set(PACKAGED_SCREENSHARE_FOLDER "hifi-screenshare-win32-x64")
|
||||
set(SCREENSHARE_DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_SCREENSHARE_FOLDER}")
|
||||
install(
|
||||
DIRECTORY "${SCREENSHARE_DESTINATION}/"
|
||||
DESTINATION ${SCREENSHARE_INSTALL_DIR}
|
||||
)
|
||||
|
||||
set(EXECUTABLE_PATH "${SCREENSHARE_DESTINATION}/${SCREENSHARE_EXEC_NAME}")
|
||||
optional_win_executable_signing()
|
||||
elseif (APPLE)
|
||||
set(PACKAGED_SCREENSHARE_FOLDER "hifi-screenshare-darwin-x64/${SCREENSHARE_EXEC_NAME}")
|
||||
install(
|
||||
DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_SCREENSHARE_FOLDER}"
|
||||
USE_SOURCE_PERMISSIONS
|
||||
DESTINATION ${SCREENSHARE_INSTALL_DIR}
|
||||
)
|
||||
endif()
|
||||
|
||||
if (PR_BUILD)
|
||||
# DO build the Screenshare Electron app when building the `ALL_BUILD` target.
|
||||
# DO build the Screenshare Electron app when a user selects "Build Solution" from within Visual Studio.
|
||||
set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL FALSE EXCLUDE_FROM_DEFAULT_BUILD FALSE)
|
||||
set_target_properties(${TARGET_NAME}-npm-install PROPERTIES EXCLUDE_FROM_ALL FALSE EXCLUDE_FROM_DEFAULT_BUILD FALSE)
|
||||
else ()
|
||||
# DO NOT build the Screenshare Electron app when building the `ALL_BUILD` target.
|
||||
# DO NOT build the Screenshare Electron app when a user selects "Build Solution" from within Visual Studio.
|
||||
set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE)
|
||||
set_target_properties(${TARGET_NAME}-npm-install PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE)
|
||||
endif ()
|
22
screenshare/README.md
Normal file
22
screenshare/README.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Hifi-Screenshare
|
||||
|
||||
The Screenshare app will allow easy desktop sharing by being launced from within the highfidelity interface.
|
||||
|
||||
# Setup
|
||||
Create the following environment variable the hifi-screenshare app will use to get the proper connection info:
|
||||
hifiScreenshareURL="<URL for authentication>"
|
||||
|
||||
# Screenshare API
|
||||
In order to launch the hifi-screenshare app from within interface, you will call the following:
|
||||
Screenshare.startScreenshare(displayName, userName, token, sessionID, apiKey);
|
||||
The app won't run without the correct info.
|
||||
|
||||
# Files included
|
||||
packager.js :
|
||||
Calling npm run packager will use this file to create the actual electron hifi-screenshare executable
|
||||
|
||||
src/main.js :
|
||||
The main process file to configure the electron app
|
||||
|
||||
srce/app.js :
|
||||
The render file to dispaly the screenshare UI
|
2289
screenshare/package-lock.json
generated
Normal file
2289
screenshare/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
screenshare/package.json
Normal file
27
screenshare/package.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "highfidelity_screenshare",
|
||||
"version": "1.0.0",
|
||||
"description": "High Fidelity Screenshare",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"packager": "node packager.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/highfidelity/hifi.git"
|
||||
},
|
||||
"author": "High Fidelity",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/highfidelity/hifi/issues"
|
||||
},
|
||||
"homepage": "https://github.com/highfidelity/hifi#readme",
|
||||
"devDependencies": {
|
||||
"electron": "^6.0.12",
|
||||
"electron-packager": "^14.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"yargs": "^14.2.0"
|
||||
}
|
||||
}
|
49
screenshare/packager.js
Normal file
49
screenshare/packager.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
var packager = require('electron-packager');
|
||||
var osType = require('os').type();
|
||||
var argv = require('yargs').argv;
|
||||
|
||||
var platform = null;
|
||||
if (osType == "Darwin" || osType == "Linux") {
|
||||
platform = osType.toLowerCase();
|
||||
} else if (osType == "Windows_NT") {
|
||||
platform = "win32"
|
||||
}
|
||||
|
||||
var NAME = "hifi-screenshare";
|
||||
var options = {
|
||||
dir: __dirname,
|
||||
name: NAME,
|
||||
version: "0.1.0",
|
||||
overwrite: true,
|
||||
prune: true,
|
||||
arch: "x64",
|
||||
platform: platform,
|
||||
ignore: "electron-packager|README.md|CMakeLists.txt|packager.js|.gitignore"
|
||||
};
|
||||
|
||||
// setup per OS options
|
||||
if (osType == "Darwin") {
|
||||
options["app-bundle-id"] = "com.highfidelity.hifi-screenshare";
|
||||
} else if (osType == "Windows_NT") {
|
||||
options["version-string"] = {
|
||||
CompanyName: "High Fidelity, Inc.",
|
||||
FileDescription: "High Fidelity Screenshare",
|
||||
ProductName: NAME,
|
||||
OriginalFilename: NAME + ".exe"
|
||||
}
|
||||
}
|
||||
|
||||
// check if we were passed a custom out directory, pass it along if so
|
||||
if (argv.out) {
|
||||
options.out = argv.out
|
||||
}
|
||||
|
||||
// call the packager to produce the executable
|
||||
packager(options)
|
||||
.then(appPath => {
|
||||
console.log("Wrote new app to " + appPath);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("There was an error writing the packaged console: " + error.message);
|
||||
process.exit(1);
|
||||
});
|
285
screenshare/src/app.js
Normal file
285
screenshare/src/app.js
Normal file
|
@ -0,0 +1,285 @@
|
|||
// Helpers
|
||||
function handleError(error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/* SOURCE EXAMPLE
|
||||
[23584:1028/110448.237:INFO:CONSOLE(67)] "{
|
||||
"id": "screen:0:0",
|
||||
"name": "Screen 1",
|
||||
"thumbnail": {},
|
||||
"display_id": "2528732444",
|
||||
"appIcon": null
|
||||
}"
|
||||
*/
|
||||
|
||||
var isBrowser = false;
|
||||
const imageWidth = 265;
|
||||
const imageHeight = 165;
|
||||
|
||||
var images = 10;
|
||||
var testImage = "resources/test.jpg";
|
||||
function MakeSource(name, thumbnail, id, newWidth, newHeight){
|
||||
this.name = name;
|
||||
this.thumbnail = thumbnail;
|
||||
this.id = id;
|
||||
this.width = newWidth;
|
||||
this.height = newHeight;
|
||||
}
|
||||
|
||||
let testSources = [];
|
||||
|
||||
for (let index = 0; index < images; index++) {
|
||||
let test = new MakeSource("REALLY LONG LONG title" + index, testImage, index, imageWidth, imageHeight);
|
||||
testSources.push(test);
|
||||
}
|
||||
|
||||
// if (!isBrowser) {
|
||||
const electron = require('electron');
|
||||
// }
|
||||
|
||||
let currentScreensharePickID = "";
|
||||
function screensharePicked(id){
|
||||
currentScreensharePickID = id;
|
||||
console.log(currentScreensharePickID);
|
||||
document.getElementById("share_pick").innerHTML = "";
|
||||
addSource(sourceMap[id], "share_pick");
|
||||
togglePage();
|
||||
}
|
||||
|
||||
|
||||
|
||||
function screenConfirmed(isConfirmed){
|
||||
if (isConfirmed === true){
|
||||
onAccessApproved(currentScreensharePickID);
|
||||
}
|
||||
togglePage();
|
||||
}
|
||||
|
||||
|
||||
let currentPage = "mainPage";
|
||||
function togglePage(){
|
||||
if (currentPage === "mainPage") {
|
||||
currentPage = "confirmationPage";
|
||||
document.getElementById("select_screen").style.display = "none";
|
||||
document.getElementById("confirmation_screen").style.display = "block";
|
||||
} else {
|
||||
currentPage = "mainPage";
|
||||
document.getElementById("select_screen").style.display = "block";
|
||||
document.getElementById("confirmation_screen").style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// UI
|
||||
function addSource(source, type) {
|
||||
let sourceBody = document.createElement('div')
|
||||
let thumbnail = isBrowser ? source.thumbnail : source.thumbnail.toDataURL();
|
||||
sourceBody.classList.add("box")
|
||||
if (type === "share_pick") {
|
||||
sourceBody.style.marginLeft = "0px";
|
||||
}
|
||||
|
||||
let circle = `<div class="circle" onclick="screensharePicked('${source.id}')"}></div>`
|
||||
let image = "";
|
||||
if (source.appIcon) {
|
||||
image = `<img class="icon" src="${source.appIcon.toDataURL()}" />`;
|
||||
}
|
||||
sourceBody.innerHTML = `
|
||||
<div class="heading">
|
||||
${image}
|
||||
<span class="screen_label">${source.name}</span>
|
||||
</div>
|
||||
<div class="${type === "share_pick" ? "image_no_hover" : "image"}" onclick="screensharePicked('${source.id}')">
|
||||
<img src="${thumbnail}" />
|
||||
</div>
|
||||
`
|
||||
// console.log(sourceBody.innerHTML);
|
||||
if (type === "selects") {
|
||||
document.getElementById("selects").appendChild(sourceBody);
|
||||
} else {
|
||||
document.getElementById("share_pick").appendChild(sourceBody);
|
||||
document.getElementById("content_name").innerHTML = source.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let sourceMap = {};
|
||||
function showSources() {
|
||||
document.getElementById("selects").innerHTML="";
|
||||
if (isBrowser) {
|
||||
for (let source of testSources) {
|
||||
sourceMap[source.id] = source;
|
||||
addSource(source, "selects");
|
||||
}
|
||||
} else {
|
||||
electron.desktopCapturer.getSources({
|
||||
types:['window', 'screen'],
|
||||
thumbnailSize: {
|
||||
width: imageWidth,
|
||||
height: imageHeight
|
||||
},
|
||||
fetchWindowIcons: true
|
||||
}, (error, sources) => {
|
||||
if (error) {
|
||||
console.log("Error getting sources", error);
|
||||
}
|
||||
|
||||
for (let source of sources) {
|
||||
// console.log(JSON.stringify(sources,null,4));
|
||||
sourceMap[source.id] = source;
|
||||
//*if (source.id.indexOf("screen") > -1) {
|
||||
// console.log("Adding:", source.id)
|
||||
addSource(source, "selects");
|
||||
//}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let localStream;
|
||||
function stopSharing(){
|
||||
desktopSharing = false;
|
||||
|
||||
if (localStream) {
|
||||
localStream.getTracks()[0].stop();
|
||||
localStream = null;
|
||||
}
|
||||
|
||||
document.getElementById('screenshare').style.display = "none";
|
||||
stopTokBoxPublisher();
|
||||
}
|
||||
|
||||
function gotStream(stream) {
|
||||
localStream = stream;
|
||||
startTokboxPublisher(localStream);
|
||||
|
||||
stream.onended = () => {
|
||||
if (desktopSharing) {
|
||||
toggle();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function onAccessApproved(desktop_id) {
|
||||
if (!desktop_id) {
|
||||
console.log('Desktop Capture access rejected.');
|
||||
return;
|
||||
}
|
||||
showSources();
|
||||
document.getElementById('screenshare').style.visibility = "block";
|
||||
desktopSharing = true;
|
||||
console.log("Desktop sharing started.. desktop_id:" + desktop_id);
|
||||
navigator.webkitGetUserMedia({
|
||||
audio: false,
|
||||
video: {
|
||||
mandatory: {
|
||||
chromeMediaSource: 'desktop',
|
||||
chromeMediaSourceId: desktop_id,
|
||||
minWidth: 1280,
|
||||
maxWidth: 1280,
|
||||
minHeight: 720,
|
||||
maxHeight: 720
|
||||
}
|
||||
}
|
||||
}, gotStream, handleError);
|
||||
}
|
||||
|
||||
// Tokbox
|
||||
|
||||
function initializeTokboxSession() {
|
||||
console.log("\n\n\n\n #$######\n TRYING TO START SESSION")
|
||||
session = OT.initSession(apiKey, sessionId);
|
||||
|
||||
session.on('sessionDisconnected', (event) => {
|
||||
console.log('You were disconnected from the session.', event.reason);
|
||||
});
|
||||
|
||||
// Connect to the session
|
||||
session.connect(token, (error) => {
|
||||
if (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var publisher;
|
||||
function startTokboxPublisher(stream){
|
||||
publisher = document.createElement("div");
|
||||
console.log("publisher pushed")
|
||||
|
||||
var publisherOptions = {
|
||||
videoSource: stream.getVideoTracks()[0],
|
||||
audioSource: null,
|
||||
insertMode: 'append',
|
||||
width: 1280,
|
||||
height: 720
|
||||
};
|
||||
|
||||
publisher = OT.initPublisher(publisher, publisherOptions, function(error){
|
||||
if (error) {
|
||||
console.log("ERROR: " + error);
|
||||
} else {
|
||||
session.publish(publisher, function(error) {
|
||||
if (error) {
|
||||
console.log("ERROR FROM Session.publish: " + error);
|
||||
return;
|
||||
}
|
||||
console.log("MADE IT TO PUBLISH")
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function stopTokBoxPublisher(){
|
||||
console.log("TOK BOX STOPPED!")
|
||||
publisher.destroy();
|
||||
}
|
||||
|
||||
|
||||
// main TODO:
|
||||
// const {ipcRenderer} = ipcRenderer;
|
||||
// let apiKey;
|
||||
// let sessionId;
|
||||
// let token;
|
||||
// let session;
|
||||
// ipcRenderer.on('connectionInfo', function(event, message){
|
||||
// console.log("event", event);
|
||||
// console.log("MESSAGE FROM MAIN", message);
|
||||
// const connectionInfo = JSON.parse(message);
|
||||
// apiKey = connectionInfo.apiKey;
|
||||
// sessionId = connectionInfo.sessionId;
|
||||
// token = connectionInfo.token;
|
||||
// initializeTokboxSession();
|
||||
// })
|
||||
|
||||
function startup(){
|
||||
console.log("\n\n IN STARTUP \n\n")
|
||||
// Make an Ajax request to get the OpenTok API key, session ID, and token from the server
|
||||
// TODO:
|
||||
fetch(process.env.hifiScreenshareURL)
|
||||
.then(function(res) {
|
||||
return res.json();
|
||||
})
|
||||
.then(function fetchJson(json) {
|
||||
apiKey = json.apiKey;
|
||||
sessionId = json.sessionId;
|
||||
token = json.token;
|
||||
|
||||
initializeTokboxSession();
|
||||
})
|
||||
.catch(function catchErr(error) {
|
||||
handleError(error);
|
||||
alert('Failed to get opentok sessionId and token. Make sure you have updated the config.js file.');
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
startup();
|
||||
showSources();
|
||||
})
|
55
screenshare/src/index.html
Normal file
55
screenshare/src/index.html
Normal file
|
@ -0,0 +1,55 @@
|
|||
<html>
|
||||
<head>
|
||||
<link href="styles.css" rel="stylesheet">
|
||||
</head>
|
||||
<body id="main">
|
||||
<div id="title" class="text_title">
|
||||
<h1>Share your screen</h1>
|
||||
<h3>Please select the content you'd like to share.</h3>
|
||||
</div>
|
||||
<button id="screenshare" onclick="stopSharing()" style="display: none;">Stop Screenshare</button>
|
||||
<div id="select_screen">
|
||||
<div class="scrollbar" id="style-1">
|
||||
<div class="force-overflow">
|
||||
|
||||
<div id="selects">
|
||||
|
||||
<!-- <div class="box"> -->
|
||||
<!-- <div class="heading"> -->
|
||||
<!-- <div class="circle"></div> -->
|
||||
<!-- <span class="screen_label">Screen 1</span> -->
|
||||
<!-- </div> heading -->
|
||||
<!-- <div class="image"> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> box -->
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="confirmation_screen" style="display: none;">
|
||||
<div id="share_pick">
|
||||
|
||||
</div> <!-- share_pick -->
|
||||
<div id="confirmation_text" style="color: white;">
|
||||
<p>
|
||||
Are you sure you'd like to share <span id="content_name">Content Name</span>?
|
||||
</p>
|
||||
<p>
|
||||
Others will be able to see everything contained within this view.
|
||||
</p>
|
||||
</div>
|
||||
<div id="button_selection">
|
||||
<div id="yes" class="button_confirmation" style="color: #ffffff" onClick="screenConfirmed(true)">
|
||||
Yes, share this content
|
||||
</div>
|
||||
<div id="no" class="button_confirmation" style="color: #009ee0" onClick="screenConfirmed(false)">
|
||||
No, don't share this content
|
||||
</div>
|
||||
</div> button_selection
|
||||
</div> <!-- confirmation screen -->
|
||||
<script src="app.js"></script>
|
||||
<script src="https://static.opentok.com/v2/js/opentok.min.js"></script>
|
||||
</body>
|
||||
</html>
|
56
screenshare/src/main.js
Normal file
56
screenshare/src/main.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
'use strict';
|
||||
|
||||
var userName, displayName, token, apiKey, sessionID;
|
||||
|
||||
const {app, BrowserWindow, ipcMain} = require('electron');
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
const argv = require('yargs').argv;
|
||||
// ./screenshare.exe --userName=miladN ...
|
||||
|
||||
const connectionInfo = {
|
||||
userName: argv.userName || "testName",
|
||||
displayName: argv.displayName || "displayName",
|
||||
token: argv.token || "token",
|
||||
apiKey: argv.apiKey || "apiKey",
|
||||
sessionID: argv.sessionID || "sessionID"
|
||||
}
|
||||
|
||||
if (!gotTheLock) {
|
||||
// log.warn("Another instance of the screenshare is already running - this instance will quit.");
|
||||
app.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
var window;
|
||||
function createWindow(){
|
||||
const zoomFactor = 1.0;
|
||||
window = new BrowserWindow({
|
||||
backgroundColor: "#000000",
|
||||
width: 1280 * zoomFactor,
|
||||
height: 720 * zoomFactor,
|
||||
center: true,
|
||||
frame: true,
|
||||
useContentSize: true,
|
||||
zoomFactor: zoomFactor,
|
||||
resizable: false,
|
||||
alwaysOnTop: false, // TRY
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
});
|
||||
window.loadURL('file://' + __dirname + '/index.html');
|
||||
window.setMenu(null);
|
||||
|
||||
window.once('ready-to-show', () => {
|
||||
window.show();
|
||||
window.webContents.openDevTools()
|
||||
})
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
app.on('ready', function() {
|
||||
createWindow();
|
||||
console.log("sending info");
|
||||
window.webContents.send('connectionInfo', JSON.stringify(connectionInfo))
|
||||
});
|
BIN
screenshare/src/resources/Graphik-Medium.ttf
Normal file
BIN
screenshare/src/resources/Graphik-Medium.ttf
Normal file
Binary file not shown.
BIN
screenshare/src/resources/Graphik-Regular.ttf
Normal file
BIN
screenshare/src/resources/Graphik-Regular.ttf
Normal file
Binary file not shown.
BIN
screenshare/src/resources/Graphik-Semibold.ttf
Normal file
BIN
screenshare/src/resources/Graphik-Semibold.ttf
Normal file
Binary file not shown.
BIN
screenshare/src/resources/Graphikbold.ttf
Normal file
BIN
screenshare/src/resources/Graphikbold.ttf
Normal file
Binary file not shown.
BIN
screenshare/src/resources/test.jpg
Normal file
BIN
screenshare/src/resources/test.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
230
screenshare/src/styles.css
Normal file
230
screenshare/src/styles.css
Normal file
|
@ -0,0 +1,230 @@
|
|||
body {
|
||||
background: black;
|
||||
box-sizing: border-box;
|
||||
/* display: -webkit-flex; */
|
||||
/* -webkit-justify-content: center; */
|
||||
/* -webkit-align-items: center; */
|
||||
/* -webkit-flex-direction: column; */
|
||||
font-family: "Graphik";
|
||||
margin: 0px;
|
||||
|
||||
}
|
||||
html::-webkit-scrollbar {
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
#confirmation_screen {
|
||||
/* background-color: orange; */
|
||||
width: 100%;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#confirmation_text {
|
||||
margin-top: 25px;
|
||||
font-size: 25px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
#confirmation_text p {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#button_selection {
|
||||
margin-top: 25px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
#share_pick {
|
||||
/* background-color: blue; */
|
||||
}
|
||||
|
||||
.text_title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Graphik";
|
||||
src: url("./resources/Graphik-Regular.ttf");
|
||||
}
|
||||
|
||||
#title {
|
||||
margin-left: 21px;
|
||||
margin-top: 21px;
|
||||
}
|
||||
|
||||
#selects {
|
||||
margin-left: 21px;
|
||||
margin-top: 70px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 48px;
|
||||
font-size: 48px;
|
||||
margin: 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
line-height: 24px;
|
||||
font-size: 24px;
|
||||
margin: 9px 0px 0px 0px;
|
||||
}
|
||||
|
||||
#publisher {
|
||||
visibility: hidden;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
z-index: 100;
|
||||
border: 3px solid white;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.screen_label {
|
||||
max-width: 220px;
|
||||
font-size: 25px;
|
||||
line-height: 25px;
|
||||
margin-left: 15px;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
background: #C4C4C4;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.circle:hover{
|
||||
background-color: yellow;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button_confirmation {
|
||||
margin: 4px;
|
||||
cursor: pointer;
|
||||
width: 250px;
|
||||
height: 75px;
|
||||
line-height: 75px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.button_confirmation:hover {
|
||||
outline: solid white 2px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.box {
|
||||
/* background-color: orange; */
|
||||
height: 165px;
|
||||
width: 265px;
|
||||
display: inline-block;
|
||||
margin-left: 35px;
|
||||
margin-bottom: 40px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
background-color: blue;
|
||||
width: 265px;
|
||||
height: 165px;
|
||||
max-height: 165px;
|
||||
max-width: 265px;
|
||||
}
|
||||
|
||||
.image:hover {
|
||||
outline: solid white 3px;
|
||||
}
|
||||
|
||||
.image_no_hover {
|
||||
background-color: blue;
|
||||
width: 265px;
|
||||
height: 165px;
|
||||
max-height: 165px;
|
||||
max-width: 265px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 265px;
|
||||
height: 165px;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
float: right;
|
||||
height: 470px;
|
||||
margin-right: 20px;
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#style-1::-webkit-scrollbar {
|
||||
width: 15px;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
#style-1::-webkit-scrollbar-thumb {
|
||||
background-color: #0198CB;
|
||||
|
||||
}
|
||||
|
||||
#style-1::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
|
||||
background-color: #848484;
|
||||
margin-right: 22px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
button {
|
||||
display: inline-block;
|
||||
background: -webkit-linear-gradient(#F9F9F9 40%, #E3E3E3 70%);
|
||||
background: linear-gradient(#F9F9F9 40%, #E3E3E3 70%);
|
||||
border: 1px solid #999;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
padding: 5px 8px;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
text-shadow: 1px 1px #fff;
|
||||
font-weight: 700;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
button.active {
|
||||
border-color: black;
|
||||
}
|
||||
button:active,
|
||||
button.active {
|
||||
background: -webkit-linear-gradient(#E3E3E3 40%, #F9F9F9 70%);
|
||||
background: linear-gradient(#E3E3E3 40%, #F9F9F9 70%);
|
||||
} */
|
Loading…
Reference in a new issue