Commit e38548ee authored by jamiewalch@google.com's avatar jamiewalch@google.com

Added connection history (minus the history data)

Since this functionality is not yet available, I've removed the link to it on the home page, but I wanted to get it ready for when we have connection history data so that I can trim the CSS and get the strings translated.

BUG=115350
TEST=Manual

Review URL: https://chromiumcodereview.appspot.com/9562044

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@124556 0039d316-1c4b-4281-b951-d872f2087c98
parent 804d0728
...@@ -112,12 +112,12 @@ ...@@ -112,12 +112,12 @@
'webapp/client_plugin_v1.js', 'webapp/client_plugin_v1.js',
'webapp/client_screen.js', 'webapp/client_screen.js',
'webapp/client_session.js', 'webapp/client_session.js',
'webapp/connection_history.css',
'webapp/connection_history.js',
'webapp/connection_stats.css', 'webapp/connection_stats.css',
'webapp/connection_stats.js', 'webapp/connection_stats.js',
'webapp/cs_oauth2_trampoline.js', 'webapp/cs_oauth2_trampoline.js',
'webapp/daemon_plugin.js', 'webapp/daemon_plugin.js',
'webapp/dividerbottom.png',
'webapp/dividertop.png',
'webapp/event_handlers.js', 'webapp/event_handlers.js',
'webapp/format_iq.js', 'webapp/format_iq.js',
'webapp/host_list.js', 'webapp/host_list.js',
...@@ -147,6 +147,8 @@ ...@@ -147,6 +147,8 @@
'resources/chromoting16.png', 'resources/chromoting16.png',
'resources/chromoting48.png', 'resources/chromoting48.png',
'resources/chromoting128.png', 'resources/chromoting128.png',
'resources/disclosure_arrow_down.png',
'resources/disclosure_arrow_right.png',
], ],
}, },
......
...@@ -13,6 +13,10 @@ ...@@ -13,6 +13,10 @@
} }
} }
}, },
"ALL_CONNECTIONS": {
"message": "All connections",
"description": "In the connection history dialog, clicking this button shows all recent connections unfiltered."
},
"ASK_PIN_DIALOG_DESCRIPTION": { "ASK_PIN_DIALOG_DESCRIPTION": {
"message": "To protect access to this computer, please choose a PIN. This PIN will be required when connecting from another location.", "message": "To protect access to this computer, please choose a PIN. This PIN will be required when connecting from another location.",
"description": "Explanatory text displayed when the user enables remote access or changes the PIN." "description": "Explanatory text displayed when the user enables remote access or changes the PIN."
...@@ -29,6 +33,14 @@ ...@@ -29,6 +33,14 @@
"message": "Cancel", "message": "Cancel",
"description": "Label for general-purpose Cancel buttons." "description": "Label for general-purpose Cancel buttons."
}, },
"CLEAR_HISTORY": {
"message": "Clear history",
"description": "In the connection history dialog, clicking this button will delete the connection history."
},
"CLOSE": {
"message": "Close",
"description": "Label for general-purpose Close buttons."
},
"CLOSE_PROMPT": { "CLOSE_PROMPT": {
"message": "Leaving this page will end your Chromoting session.", "message": "Leaving this page will end your Chromoting session.",
"description": "Message shown when the Chromoting client tab is closed while a connection is active." "description": "Message shown when the Chromoting client tab is closed while a connection is active."
...@@ -37,10 +49,22 @@ ...@@ -37,10 +49,22 @@
"message": "Connect", "message": "Connect",
"description": "Label for the connect button. Clicking this button will start the Chromoting session if the access code is correct." "description": "Label for the connect button. Clicking this button will start the Chromoting session if the access code is correct."
}, },
"CONNECTION_FROM_HEADER": {
"message": "From",
"description": "Column header in the connection history table showing the email address of the client end of the connection (the initiator) which may be this or another computer."
},
"CONNECTION_HISTORY_BUTTON": { "CONNECTION_HISTORY_BUTTON": {
"message": "Connection history", "message": "Connection history",
"description": "Label for the connection history button. Clicking this button opens a dialog showing recent connections." "description": "Label for the connection history button. Clicking this button opens a dialog showing recent connections."
}, },
"CONNECTION_HISTORY_TITLE": {
"message": "Connection History",
"description": "Title for the connection history dialog. This dialog shows recent connections made to and from this computer"
},
"CONNECTION_TO_HEADER": {
"message": "To",
"description": "Column header in the connection history table showing the email address of the host end of the connection (the connectee) which may be this or another computer."
},
"CONTINUE_BUTTON": { "CONTINUE_BUTTON": {
"message": "Continue", "message": "Continue",
"description": "Label for the continue button on the pre-authorization page. Clicking this button takes the user to the standard Google Accounts authorization page." "description": "Label for the continue button on the pre-authorization page. Clicking this button takes the user to the standard Google Accounts authorization page."
...@@ -87,6 +111,10 @@ ...@@ -87,6 +111,10 @@
"message": "Disconnect", "message": "Disconnect",
"description": "Label for the host-side disconnect button, without keyboard shortcuts. Only used in case we aren't able to enable hot-key support. Clicking this button disconnects the remote user." "description": "Label for the host-side disconnect button, without keyboard shortcuts. Only used in case we aren't able to enable hot-key support. Clicking this button disconnects the remote user."
}, },
"DURATION_HEADER": {
"message": "Duration",
"description": "Column header in the connection history table showing the length of time for which a connection was active, if available."
},
"ERROR_AUTHENTICATION_FAILED": { "ERROR_AUTHENTICATION_FAILED": {
"message": "Authentication failed. Please sign out of Chromoting and try again.", "message": "Authentication failed. Please sign out of Chromoting and try again.",
"description": "Error displayed if authentication fails. This can be caused by stale credentials, in which logging out of the web-app and retrying can fix the problem." "description": "Error displayed if authentication fails. This can be caused by stale credentials, in which logging out of the web-app and retrying can fix the problem."
...@@ -175,6 +203,10 @@ ...@@ -175,6 +203,10 @@
"message": "(this feature is not yet available for Chromebooks\u2026 stay tuned)", "message": "(this feature is not yet available for Chromebooks\u2026 stay tuned)",
"description": "Text displayed below the description of the 'share' or 'host' functionality on ChromeOS devices, where it is not yet supported." "description": "Text displayed below the description of the 'share' or 'host' functionality on ChromeOS devices, where it is not yet supported."
}, },
"INCOMING_CONNECTIONS": {
"message": "To this computer",
"description": "In the connection history dialog, clicking this button shows only recent connections to this computer."
},
"INSTRUCTIONS_SHARE_ABOVE": { "INSTRUCTIONS_SHARE_ABOVE": {
"message": "To begin sharing your desktop, give the access code below to the person who will be assisting you.", "message": "To begin sharing your desktop, give the access code below to the person who will be assisting you.",
"description": "Instructions shown above the access code when it is ready to be conveyed to the client." "description": "Instructions shown above the access code when it is ready to be conveyed to the client."
...@@ -231,6 +263,10 @@ ...@@ -231,6 +263,10 @@
} }
} }
}, },
"OUTGOING_CONNECTIONS": {
"message": "From this computer",
"description": "In the connection history dialog, clicking this button shows only recent connections from this computer."
},
"PIN": { "PIN": {
"message": "PIN", "message": "PIN",
"description": "Label for the PIN entry box." "description": "Label for the PIN entry box."
...@@ -255,6 +291,10 @@ ...@@ -255,6 +291,10 @@
"message": "Stop Sharing", "message": "Stop Sharing",
"description": "Label for the 'stop sharing' button on the host-side. Clicking this button disconnects the client." "description": "Label for the 'stop sharing' button on the host-side. Clicking this button disconnects the client."
}, },
"TIME_HEADER": {
"message": "Time",
"description": "Column header in the connection history table showing the time and date of a connection."
},
"TOOLTIP_CONNECT": { "TOOLTIP_CONNECT": {
"message": "Connect to $hostname$", "message": "Connect to $hostname$",
"description": "The tool-tip shown when the user hovers over an on-line host. Clicking the host will initiate a connection to it.", "description": "The tool-tip shown when the user hovers over an on-line host. Clicking the host will initiate a connection to it.",
......
/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#connection-history-dialog {
margin-top: -80px;
height: 580px;
width: 880px;
}
#connection-history-options {
margin-top: 30px;
display: -webkit-box;
}
#connection-history-scroller {
margin-top: 30px;
height: 370px;
width: 100%;
overflow: auto;
}
#connection-history-table {
width: 100%;
line-height: 2.1;
border-bottom: 1px solid #ebebeb;
}
#connection-history-table td:last-child {
text-align: right;
}
#connection-history-table thead td {
font-weight: bold;
}
#close-connection-history {
position: absolute;
bottom: 0;
__MSG_@@bidi_end_edge__: 0;
}
.internal-frame-of-reference {
position: relative;
width: 100%;
height: 100%;
}
.link-list > a {
margin-right: 16px;
}
a.no-link {
color: inherit;
cursor: inherit;
}
.no-expand {
width: 1px;
padding-right: 20px;
}
.connection-history-summary td {
border-top: 1px solid #EBEBEB;
}
.connection-history-summary:hover,
.connection-history-summary.expanded,
.connection-history-summary.expanded + .connection-history-detail {
background-color: #f8f8f8;
}
.connection-history-detail td div {
height: 0;
overflow: hidden;
}
.connection-history-summary.expanded + .connection-history-detail td div {
height: auto;
}
.connection-history-summary.expanded .zippy {
background-image: url('disclosure_arrow_down.png');
}
.zippy {
width: 30px;
height: 11px;
background-image: url('disclosure_arrow_right.png');
background-repeat: no-repeat;
background-position: 10px 50%;
}
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* Class to track of connections to and from this computer and display them.
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/** @constructor */
remoting.ConnectionHistory = function() {
/** @type {HTMLElement} @private */
this.viewAll_ = document.getElementById('history-view-all');
/** @type {HTMLElement} @private */
this.viewOutgoing_ = document.getElementById('history-view-outgoing');
/** @type {HTMLElement} @private */
this.viewIncoming_ = document.getElementById('history-view-incoming');
/** @type {HTMLElement} @private */
this.clear_ = document.getElementById('clear-connection-history');
/** @type {HTMLElement} @private */
this.historyEntries_ = document.getElementById('connection-history-entries');
/** @type {remoting.ConnectionHistory.Filter} @private */
this.filter_ = remoting.ConnectionHistory.Filter.VIEW_ALL;
/** @type {remoting.ConnectionHistory} */
var that = this;
var closeButton = document.getElementById('close-connection-history');
closeButton.addEventListener('click', function() { that.hide(); }, false);
/** @param {Event} event Event identifying which button was clicked. */
var setFilter = function(event) { that.setFilter_(event.target); };
var clearHistory = function() { that.clearHistory_(); };
this.viewAll_.addEventListener('click', setFilter, false);
this.viewOutgoing_.addEventListener('click', setFilter, false);
this.viewIncoming_.addEventListener('click', setFilter, false);
this.clear_.addEventListener('click', clearHistory, false);
};
/** @enum {string} */
remoting.ConnectionHistory.Filter = {
VIEW_ALL: 'history-view-all',
VIEW_OUTGOING: 'history-view-outgoing',
VIEW_INCOMING: 'history-view-incoming'
};
/** Show the dialog and refresh its contents */
remoting.ConnectionHistory.prototype.show = function() {
this.load();
remoting.setMode(remoting.AppMode.HISTORY);
};
/** Hide the dialog */
remoting.ConnectionHistory.prototype.hide = function() {
remoting.setMode(remoting.AppMode.HOME);
};
/**
* A saved entry in the connection history.
* @param {Object} object A Javascript object, which may or may not be of the
* correct type.
* @constructor
*/
remoting.ConnectionHistory.Entry = function(object) {
this.valid =
'date' in object && typeof(object['date']) == 'number' &&
'from' in object && typeof(object['from']) == 'string' &&
'to' in object && typeof(object['to']) == 'string' &&
'duration' in object && typeof(object['duration']) == 'number';
if (this.valid) {
/** @type {Date} */
this.date = new Date(object['date']);
/** @type {string} */
this.from = object['from'];
/** @type {string} */
this.to = object['to'];
/** @type {number} */
this.duration = object['duration'];
}
};
/**
* @return {string} The connection duration, formatted as a string, or 'Not
* available' if there is no duration stored.
* @private
*/
remoting.ConnectionHistory.Entry.prototype.durationString_ = function() {
var secs = this.duration % 60;
var mins = ((this.duration - secs) / 60) % 60;
var hours = (this.duration - secs - 60 * mins) / 3600;
if (secs < 10) {
secs = '0' + secs;
}
var result = mins + ':' + secs;
if (hours > 0) {
if (mins < 10) {
result = '0' + result;
}
result = hours + ':' + result;
}
return result;
};
/**
* @return {{summary: Element, detail: Element}} Two table rows containing the
* summary and detail information, respectively, for the connection.
*/
remoting.ConnectionHistory.Entry.prototype.createTableRows = function() {
var summary = document.createElement('tr');
addClass(summary, 'connection-history-summary');
var zippy = document.createElement('td');
addClass(zippy, 'zippy');
summary.appendChild(zippy);
// TODO(jamiewalch): Find a way of combining date and time such both align
// vertically without being considered separate columns, which puts too much
// space between them.
var date = document.createElement('td');
// TODO(jamiewalch): Use a shorter localized version of the date.
date.innerText = this.date.toLocaleDateString();
summary.appendChild(date);
var time = document.createElement('td');
time.innerText = this.date.toLocaleTimeString();
summary.appendChild(time);
var from = document.createElement('td');
from.innerText = this.from;
summary.appendChild(from);
var to = document.createElement('td');
to.innerText = this.to;
summary.appendChild(to);
var duration = document.createElement('td');
duration.innerText = this.durationString_();
summary.appendChild(duration);
// TODO(jamiewalch): Fill out the detail row correctly.
var detail = document.createElement('tr');
addClass(detail, 'connection-history-detail');
for (var i = 0; i < summary.childElementCount; ++i) {
var td = document.createElement('td');
if (i != 0) {
// The inner div allows the details rows to be hidden without changing
// the column widths.
var div = document.createElement('div');
div.innerText = 'Nothing to see here';
td.appendChild(div);
}
detail.appendChild(td);
}
/** @param {Element} node The summary row. */
var toggleDetail = function(node) {
if (hasClass(node.className, 'expanded')) {
removeClass(node, 'expanded');
} else {
addClass(node, 'expanded');
}
};
summary.addEventListener('click',
function() { toggleDetail(summary); },
false);
detail.addEventListener('click',
function() { toggleDetail(summary); },
false);
return { 'summary': summary, 'detail': detail };
};
/** Refresh the contents of the connection history table */
remoting.ConnectionHistory.prototype.load = function() {
// TODO(jamiewalch): Load connection history data when it's available.
var history = [];
// Remove existing entries from the DOM and repopulate.
// TODO(jamiewalch): Enforce the filter.
this.historyEntries_.innerHTML = '';
for (var i in history) {
var connection = new remoting.ConnectionHistory.Entry(history[i]);
if (connection.valid) {
var rows = connection.createTableRows();
this.historyEntries_.appendChild(rows.summary);
this.historyEntries_.appendChild(rows.detail);
}
}
};
/**
* @param {EventTarget} element The element that was clicked.
* @private
*/
remoting.ConnectionHistory.prototype.setFilter_ = function(element) {
for (var i in remoting.ConnectionHistory.Filter) {
var link = document.getElementById(remoting.ConnectionHistory.Filter[i]);
if (element == link) {
addClass(link, 'no-link');
this.filter_ = /** @type {remoting.ConnectionHistory.Filter} */ (i);
} else {
removeClass(link, 'no-link');
}
}
};
/**
* @private
*/
remoting.ConnectionHistory.prototype.clearHistory_ = function() {
// TODO(jamiewalch): Implement once we store users' connection histories.
};
/** @type {remoting.ConnectionHistory} */
remoting.ConnectionHistory.connectionHistory = null;
...@@ -33,10 +33,6 @@ table { ...@@ -33,10 +33,6 @@ table {
border-collapse: separate; border-collapse: separate;
border-spacing: 0; border-spacing: 0;
} }
caption,th,td {
text-align: left;
font-weight: normal;
}
blockquote:before,blockquote:after, blockquote:before,blockquote:after,
q:before,q:after { q:before,q:after {
content: ""; content: "";
...@@ -4451,7 +4447,7 @@ h1.icon-label { ...@@ -4451,7 +4447,7 @@ h1.icon-label {
font-family: "Open sans", "Ariel", sans-serif font-family: "Open sans", "Ariel", sans-serif
} }
h2 { section > h2 {
color: #666; color: #666;
} }
...@@ -4761,14 +4757,6 @@ td { ...@@ -4761,14 +4757,6 @@ td {
opacity: 0.75; opacity: 0.75;
} }
#divider-top {
margin: 10px 0 15px 0;
}
#divider-bottom {
margin: 25px 0 10px 0;
}
#email-status { #email-status {
margin-__MSG_@@bidi_end_edge__: 0.5ex; margin-__MSG_@@bidi_end_edge__: 0.5ex;
} }
......
...@@ -12,6 +12,7 @@ found in the LICENSE file. ...@@ -12,6 +12,7 @@ found in the LICENSE file.
rel="stylesheet" type="text/css"> rel="stylesheet" type="text/css">
<link rel="icon" type="image/png" href="chromoting16.png"> <link rel="icon" type="image/png" href="chromoting16.png">
<link rel="stylesheet" href="connection_stats.css"> <link rel="stylesheet" href="connection_stats.css">
<link rel="stylesheet" href="connection_history.css">
<link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="main.css">
<link rel="stylesheet" href="toolbar.css"> <link rel="stylesheet" href="toolbar.css">
<script src="ask_pin_dialog.js"></script> <script src="ask_pin_dialog.js"></script>
...@@ -19,6 +20,7 @@ found in the LICENSE file. ...@@ -19,6 +20,7 @@ found in the LICENSE file.
<script src="client_plugin_v1.js"></script> <script src="client_plugin_v1.js"></script>
<script src="client_screen.js"></script> <script src="client_screen.js"></script>
<script src="client_session.js"></script> <script src="client_session.js"></script>
<script src="connection_history.js"></script>
<script src="connection_stats.js"></script> <script src="connection_stats.js"></script>
<script src="daemon_plugin.js"></script> <script src="daemon_plugin.js"></script>
<script src="event_handlers.js"></script> <script src="event_handlers.js"></script>
...@@ -64,8 +66,10 @@ found in the LICENSE file. ...@@ -64,8 +66,10 @@ found in the LICENSE file.
<span id="current-email"></span> <span id="current-email"></span>
<span data-ui-mode="home"> <span data-ui-mode="home">
<a id="clear-oauth" href="#" i18n-content="SIGN_OUT_BUTTON"></a> | <a id="clear-oauth" href="#" i18n-content="SIGN_OUT_BUTTON"></a> |
<!-- TODO(jamiewalch): Add this back in when we support it.
<a id="connection-history" href="#" <a id="connection-history" href="#"
i18n-content="CONNECTION_HISTORY_BUTTON"></a> | i18n-content="CONNECTION_HISTORY_BUTTON"></a> |
-->
</span> </span>
<a href="https://www.google.com/support/chrome/bin/answer.py?answer=1649523" <a href="https://www.google.com/support/chrome/bin/answer.py?answer=1649523"
target="_blank" i18n-content="HELP"></a> target="_blank" i18n-content="HELP"></a>
...@@ -159,11 +163,11 @@ found in the LICENSE file. ...@@ -159,11 +163,11 @@ found in the LICENSE file.
</div> <!-- home --> </div> <!-- home -->
<div id="dialog-screen" <div id="dialog-screen"
data-ui-mode="home.host home.client home.auth" data-ui-mode="home.host home.client home.auth home.history"
hidden></div> hidden></div>
<div id="dialog-container" <div id="dialog-container"
data-ui-mode="home.host home.client home.auth" data-ui-mode="home.host home.client home.auth home.history"
hidden> hidden>
<div class="box-spacer"></div> <div class="box-spacer"></div>
...@@ -182,7 +186,7 @@ found in the LICENSE file. ...@@ -182,7 +186,7 @@ found in the LICENSE file.
<button id="daemon-pin-ok" type="submit" i18n-content="OK"></button> <button id="daemon-pin-ok" type="submit" i18n-content="OK"></button>
<img id="start-daemon-spinner" src="spinner.gif" hidden> <img id="start-daemon-spinner" src="spinner.gif" hidden>
</form> </form>
</div> </div> <!-- ask-pin-dialog -->
<div id="auth-dialog" <div id="auth-dialog"
data-ui-mode="home.auth" data-ui-mode="home.auth"
...@@ -337,7 +341,7 @@ found in the LICENSE file. ...@@ -337,7 +341,7 @@ found in the LICENSE file.
i18n-content="OK" i18n-content="OK"
autofocus="autofocus"> autofocus="autofocus">
</button> </button>
</div> <!-- client.connect-failed.it2me client.session-finished.it2me --> </div> <!-- connect-failed.it2me session-finished.it2me -->
<div data-ui-mode="home.client.connect-failed.me2me home.client.session-finished.me2me" <div data-ui-mode="home.client.connect-failed.me2me home.client.session-finished.me2me"
class="centered-button"> class="centered-button">
...@@ -352,10 +356,52 @@ found in the LICENSE file. ...@@ -352,10 +356,52 @@ found in the LICENSE file.
class="kd-button" class="kd-button"
i18n-content="RECONNECT"> i18n-content="RECONNECT">
</button> </button>
</div> <!-- client.connect-failed.me2me client.session-finished.me2me --> </div> <!-- connect-failed.me2me session-finished.me2me -->
</div> <!-- client-dialog --> </div> <!-- client-dialog -->
<div id="connection-history-dialog"
class="kd-modaldialog visible"
data-ui-mode="home.history"
hidden>
<div class="internal-frame-of-reference">
<h2 i18n-content="CONNECTION_HISTORY_TITLE"></h2>
<div id="connection-history-options">
<div class="link-list">
<a id="history-view-all"
i18n-content="ALL_CONNECTIONS"
class="no-link"></a>
<a id="history-view-outgoing"
i18n-content="OUTGOING_CONNECTIONS"></a>
<a id="history-view-incoming"
i18n-content="INCOMING_CONNECTIONS"></a>
</div>
<div class="box-spacer"></div>
<a id="clear-connection-history" i18n-content="CLEAR_HISTORY"></a>
</div>
<div id="connection-history-scroller">
<table id="connection-history-table">
<thead>
<tr>
<td></td>
<td i18n-content="TIME_HEADER"></td>
<td></td>
<td i18n-content="CONNECTION_FROM_HEADER"></td>
<td i18n-content="CONNECTION_TO_HEADER"></td>
<td i18n-content="DURATION_HEADER"></td>
</tr>
</thead>
<tbody id="connection-history-entries" class="selectable">
</tbody>
</table>
</div>
<button id="close-connection-history"
i18n-content="CLOSE"
class="kd-button"
type="button"></button>
</div>
</div> <!-- connection-history-dialog -->
<div class="box-spacer"></div> <div class="box-spacer"></div>
</div> <!-- dialog-container --> </div> <!-- dialog-container -->
......
...@@ -36,6 +36,7 @@ remoting.AppMode = { ...@@ -36,6 +36,7 @@ remoting.AppMode = {
CLIENT_CONNECT_FAILED_ME2ME: 'home.client.connect-failed.me2me', CLIENT_CONNECT_FAILED_ME2ME: 'home.client.connect-failed.me2me',
CLIENT_SESSION_FINISHED_IT2ME: 'home.client.session-finished.it2me', CLIENT_SESSION_FINISHED_IT2ME: 'home.client.session-finished.it2me',
CLIENT_SESSION_FINISHED_ME2ME: 'home.client.session-finished.me2me', CLIENT_SESSION_FINISHED_ME2ME: 'home.client.session-finished.me2me',
HISTORY: 'home.history',
IN_SESSION: 'in-session' IN_SESSION: 'in-session'
}; };
...@@ -62,7 +63,7 @@ remoting.updateModalUi = function(mode, attr) { ...@@ -62,7 +63,7 @@ remoting.updateModalUi = function(mode, attr) {
} }
element.hidden = hidden; element.hidden = hidden;
} }
} };
/** /**
* @type {remoting.AppMode} The current app mode * @type {remoting.AppMode} The current app mode
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment