зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1191067 - Add Fennec version of about:accounts. r=antlam,margaret
This is a Fennec version of about:accounts, cribbed largely from Desktop's implementation. This implementation serves two purposes: One, it allows all fxa-content-server pref handling to remain in Gecko. Java-side consumers redirect to about:accounts?action=... and have pref munging and parameter addition (like context=fx_fennec_v1, etc) handled by about:accounts itself. Two, it handles network connectivity display and error handling. When a request is started, we display an animated spinner. We transition smoothly from the spinner to the iframe display if we can, and if not we hide any network error and offer to retry. This is more important in Fennec than it is on Desktop. This approach agrees with Firefox for iOS. Some additional notes: The spinner to iframe transition uses the WebChannel listener to send LOADED messages to the appropriate XUL <browser> element. It's worth remembering that Fennec's Gecko is single process, so the <browser> in question is in the same process. None-the-less, we are close to e10s safe. There are four actions: signup/signin/force_reauth, and manage. The first three try to produce a LOGIN message. The last uses the fxa-content-server to manage the Account settings. *This is not how this is arranged on Desktop: Desktop redirects to a new tab, not wrapped in about:accounts.* --HG-- extra : commitid : F2waTwe355B extra : rebase_source : f63c96f676d1300c774d091968ec8d88bb7a86dc
This commit is contained in:
Родитель
811d17425f
Коммит
cdb092fe66
|
@ -945,3 +945,15 @@ pref("browser.tabs.showAudioPlayingIcon", true);
|
|||
pref("dom.serviceWorkers.enabled", true);
|
||||
pref("dom.serviceWorkers.interception.enabled", true);
|
||||
#endif
|
||||
|
||||
// The remote content URL where FxAccountsWebChannel messages originate. Must use HTTPS.
|
||||
pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com");
|
||||
|
||||
// The remote URL of the Firefox Account profile server.
|
||||
pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
|
||||
|
||||
// The remote URL of the Firefox Account oauth server.
|
||||
pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
|
||||
|
||||
// Token server used by Firefox Account-authenticated Sync.
|
||||
pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.mozilla.gecko.util.StringUtils;
|
|||
|
||||
public class AboutPages {
|
||||
// All of our special pages.
|
||||
public static final String ACCOUNTS = "about:accounts";
|
||||
public static final String ADDONS = "about:addons";
|
||||
public static final String CONFIG = "about:config";
|
||||
public static final String DOWNLOADS = "about:downloads";
|
||||
|
@ -72,6 +73,7 @@ public class AboutPages {
|
|||
}
|
||||
|
||||
private static final String[] DEFAULT_ICON_PAGES = new String[] {
|
||||
ACCOUNTS,
|
||||
ADDONS,
|
||||
CONFIG,
|
||||
DOWNLOADS,
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* Wrap a remote fxa-content-server.
|
||||
*
|
||||
* An about:accounts tab loads and displays an fxa-content-server page,
|
||||
* depending on the current Android Account status and an optional 'action'
|
||||
* parameter.
|
||||
*
|
||||
* We show a spinner while the remote iframe is loading. We expect the
|
||||
* WebChannel message listening to the fxa-content-server to send this tab's
|
||||
* <browser>'s messageManager a LOADED message when the remote iframe provides
|
||||
* the WebChannel LOADED message. See the messageManager registration and the
|
||||
* |loadedDeferred| promise. This loosely couples the WebChannel implementation
|
||||
* and about:accounts! (We need this coupling in order to distinguish
|
||||
* WebChannel LOADED messages produced by multiple about:accounts tabs.)
|
||||
*
|
||||
* We capture error conditions by accessing the inner nsIWebNavigation of the
|
||||
* iframe directly.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components; /*global Components */
|
||||
|
||||
Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
|
||||
Cu.import("resource://gre/modules/PromiseUtils.jsm"); /*global PromiseUtils */
|
||||
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
|
||||
|
||||
const ACTION_URL_PARAM = "action";
|
||||
|
||||
const COMMAND_LOADED = "fxaccounts:loaded";
|
||||
|
||||
const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
|
||||
|
||||
// Shows the toplevel element with |id| to be shown - all other top-level
|
||||
// elements are hidden.
|
||||
// If |id| is 'spinner', then 'remote' is also shown, with opacity 0.
|
||||
function show(id) {
|
||||
let allTop = document.querySelectorAll(".toplevel");
|
||||
for (let elt of allTop) {
|
||||
if (elt.getAttribute("id") == id) {
|
||||
elt.style.display = 'block';
|
||||
} else {
|
||||
elt.style.display = 'none';
|
||||
}
|
||||
}
|
||||
if (id == 'spinner') {
|
||||
document.getElementById('remote').style.display = 'block';
|
||||
document.getElementById('remote').style.opacity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var loadedDeferred = null;
|
||||
|
||||
// We have a new load starting. Replace the existing promise with a new one,
|
||||
// and queue up the transition to remote content.
|
||||
function deferTransitionToRemoteAfterLoaded() {
|
||||
log.d('Waiting for LOADED message.');
|
||||
loadedDeferred = PromiseUtils.defer();
|
||||
loadedDeferred.promise.then(() => {
|
||||
document.getElementById("remote").style.opacity = 0;
|
||||
show("remote");
|
||||
document.getElementById("remote").style.opacity = 1;
|
||||
});
|
||||
}
|
||||
|
||||
function handleLoadedMessage(message) {
|
||||
log.d('Got LOADED message!');
|
||||
loadedDeferred.resolve();
|
||||
};
|
||||
|
||||
let wrapper = {
|
||||
iframe: null,
|
||||
|
||||
url: null,
|
||||
|
||||
init: function (url) {
|
||||
deferTransitionToRemoteAfterLoaded();
|
||||
|
||||
let iframe = document.getElementById("remote");
|
||||
this.iframe = iframe;
|
||||
this.iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
|
||||
let docShell = this.iframe.frameLoader.docShell;
|
||||
docShell.QueryInterface(Ci.nsIWebProgress);
|
||||
docShell.addProgressListener(this.iframeListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
|
||||
|
||||
this.url = url;
|
||||
// Set the iframe's location with loadURI/LOAD_FLAGS_BYPASS_HISTORY to
|
||||
// avoid having a new history entry being added.
|
||||
let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
|
||||
},
|
||||
|
||||
retry: function () {
|
||||
deferTransitionToRemoteAfterLoaded();
|
||||
|
||||
let webNav = this.iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
webNav.loadURI(this.url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
|
||||
},
|
||||
|
||||
iframeListener: {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
|
||||
Ci.nsISupportsWeakReference,
|
||||
Ci.nsISupports]),
|
||||
|
||||
onStateChange: function(aWebProgress, aRequest, aState, aStatus) {
|
||||
let failure = false;
|
||||
|
||||
// Captive portals sometimes redirect users
|
||||
if ((aState & Ci.nsIWebProgressListener.STATE_REDIRECTING)) {
|
||||
failure = true;
|
||||
} else if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
|
||||
if (aRequest instanceof Ci.nsIHttpChannel) {
|
||||
try {
|
||||
failure = aRequest.responseStatus != 200;
|
||||
} catch (e) {
|
||||
failure = aStatus != Components.results.NS_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calling cancel() will raise some OnStateChange notifications by itself,
|
||||
// so avoid doing that more than once
|
||||
if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
|
||||
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
|
||||
show("networkError");
|
||||
}
|
||||
},
|
||||
|
||||
onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
|
||||
if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
|
||||
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
|
||||
show("networkError");
|
||||
}
|
||||
},
|
||||
|
||||
onProgressChange: function() {},
|
||||
onStatusChange: function() {},
|
||||
onSecurityChange: function() {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
function retry() {
|
||||
log.i("Retrying.");
|
||||
show("spinner");
|
||||
wrapper.retry();
|
||||
}
|
||||
|
||||
function openPrefs() {
|
||||
log.i("Opening Sync preferences.");
|
||||
// If an Android Account exists, this will open the Status Activity.
|
||||
// Otherwise, it will begin the Get Started flow. This should only be shown
|
||||
// when an Account actually exists.
|
||||
Accounts.launchSetup();
|
||||
}
|
||||
|
||||
function getURLForAction(action, urlParams) {
|
||||
let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
|
||||
url = url + (url.endsWith("/") ? "" : "/") + action;
|
||||
const CONTEXT = "fx_fennec_v1";
|
||||
// The only service managed by Fennec, to date, is Firefox Sync.
|
||||
const SERVICE = "sync";
|
||||
urlParams = urlParams || new URLSearchParams("");
|
||||
urlParams.set('service', SERVICE);
|
||||
urlParams.set('context', CONTEXT);
|
||||
// Ideally we'd just merge urlParams with new URL(url).searchParams, but our
|
||||
// URLSearchParams implementation doesn't support iteration (bug 1085284).
|
||||
let urlParamStr = urlParams.toString();
|
||||
if (urlParamStr) {
|
||||
url += (url.includes("?") ? "&" : "?") + urlParamStr;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function updateDisplayedEmail(user) {
|
||||
let emailDiv = document.getElementById("email");
|
||||
if (emailDiv && user) {
|
||||
emailDiv.textContent = user.email;
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
Accounts.getFirefoxAccount().then(user => {
|
||||
// It's possible for the window to start closing before getting the user
|
||||
// completes. Tests in particular can cause this.
|
||||
if (window.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateDisplayedEmail(user);
|
||||
|
||||
// Ideally we'd use new URL(document.URL).searchParams, but for about: URIs,
|
||||
// searchParams is empty.
|
||||
let urlParams = new URLSearchParams(document.URL.split("?")[1] || "");
|
||||
let action = urlParams.get(ACTION_URL_PARAM);
|
||||
urlParams.delete(ACTION_URL_PARAM);
|
||||
|
||||
switch (action) {
|
||||
case "signup":
|
||||
if (user) {
|
||||
// Asking to sign-up when already signed in just shows prefs.
|
||||
show("prefs");
|
||||
} else {
|
||||
show("spinner");
|
||||
wrapper.init(getURLForAction("signup", urlParams));
|
||||
}
|
||||
break;
|
||||
case "signin":
|
||||
if (user) {
|
||||
// Asking to sign-in when already signed in just shows prefs.
|
||||
show("prefs");
|
||||
} else {
|
||||
show("spinner");
|
||||
wrapper.init(getURLForAction("signin", urlParams));
|
||||
}
|
||||
break;
|
||||
case "force_auth":
|
||||
if (user) {
|
||||
show("spinner");
|
||||
urlParams.set("email", user.email); // In future, pin using the UID.
|
||||
wrapper.init(getURLForAction("force_auth", urlParams));
|
||||
} else {
|
||||
show("spinner");
|
||||
wrapper.init(getURLForAction("signup", urlParams));
|
||||
}
|
||||
break;
|
||||
case "manage":
|
||||
if (user) {
|
||||
show("spinner");
|
||||
urlParams.set("email", user.email); // In future, pin using the UID.
|
||||
wrapper.init(getURLForAction("settings", urlParams));
|
||||
} else {
|
||||
show("spinner");
|
||||
wrapper.init(getURLForAction("signup", urlParams));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Unrecognized or no action specified.
|
||||
if (action) {
|
||||
log.w("Ignoring unrecognized action: " + action);
|
||||
}
|
||||
if (user) {
|
||||
show("prefs");
|
||||
} else {
|
||||
show("spinner");
|
||||
wrapper.init(getURLForAction("signup", urlParams));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}).catch(e => {
|
||||
log.e("Failed to get the signed in user: " + e.toString());
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function onload() {
|
||||
document.removeEventListener("DOMContentLoaded", onload, true);
|
||||
init();
|
||||
var buttonRetry = document.getElementById('buttonRetry');
|
||||
buttonRetry.addEventListener('click', retry);
|
||||
|
||||
var buttonOpenPrefs = document.getElementById('buttonOpenPrefs');
|
||||
buttonOpenPrefs.addEventListener('click', openPrefs);
|
||||
}, true);
|
||||
|
||||
// This window is contained in a XUL <browser> element. Return the
|
||||
// messageManager of that <browser> element, or null.
|
||||
function getBrowserMessageManager() {
|
||||
let browser = window
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow)
|
||||
.QueryInterface(Ci.nsIDOMChromeWindow)
|
||||
.BrowserApp
|
||||
.getBrowserForDocument(document);
|
||||
if (browser) {
|
||||
return browser.messageManager;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add a single listener for 'loaded' messages from the iframe in this
|
||||
// <browser>. These 'loaded' messages are ferried from the WebChannel to just
|
||||
// this <browser>.
|
||||
let mm = getBrowserMessageManager();
|
||||
if (mm) {
|
||||
mm.addMessageListener(COMMAND_LOADED, handleLoadedMessage);
|
||||
} else {
|
||||
log.e('No messageManager, not listening for LOADED message!');
|
||||
}
|
||||
|
||||
window.addEventListener("unload", function(event) {
|
||||
try {
|
||||
let mm = getBrowserMessageManager();
|
||||
if (mm) {
|
||||
mm.removeMessageListener(COMMAND_LOADED, handleLoadedMessage);
|
||||
}
|
||||
} catch (e) {
|
||||
// This could fail if the page is being torn down, the tab is being
|
||||
// destroyed, etc.
|
||||
log.w('Not removing listener for LOADED message: ' + e.toString());
|
||||
}
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
|
||||
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
|
||||
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
|
||||
%brandDTD;
|
||||
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
|
||||
%globalDTD;
|
||||
<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd">
|
||||
%aboutDTD;
|
||||
]>
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;">
|
||||
<head>
|
||||
<title>Firefox Sync</title>
|
||||
<meta name="viewport" content="width=device-width; user-scalable=0" />
|
||||
<link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
|
||||
<link rel="stylesheet" href="chrome://browser/skin/spinner.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://browser/skin/aboutAccounts.css" type="text/css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="spinner" class="toplevel">
|
||||
<div class="container flex-column">
|
||||
<!-- Empty text-container for spacing. -->
|
||||
<div class="text-container flex-column" />
|
||||
|
||||
<div class="mui-refresh-main">
|
||||
<div class="mui-refresh-wrapper">
|
||||
<div class="mui-spinner-wrapper">
|
||||
<div class="mui-spinner-main">
|
||||
<div class="mui-spinner-left">
|
||||
<div class="mui-half-circle-left" />
|
||||
</div>
|
||||
<div class="mui-spinner-right">
|
||||
<div class="mui-half-circle-right" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<iframe mozframetype="content" id="remote" class="toplevel" />
|
||||
|
||||
<div id="prefs" class="toplevel">
|
||||
<div class="container flex-column">
|
||||
<div class="text-container flex-column">
|
||||
<div class="text">&aboutAccounts.connected.title;</div>
|
||||
<div class="hint">&aboutAccounts.connected.description;</div>
|
||||
<div id="email" class="hint"></div>
|
||||
</div>
|
||||
<a id="buttonOpenPrefs" tabindex="0" href="#">&aboutAccounts.syncPreferences.label;</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="networkError" class="toplevel">
|
||||
<div class="container flex-column">
|
||||
<div class="text-container flex-column">
|
||||
<div class="text">&aboutAccounts.noConnection.title;</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button id="buttonRetry" class="button" tabindex="1">&aboutAccounts.retry.label;</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutAccounts.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -59,6 +59,10 @@ chrome.jar:
|
|||
#ifdef MOZ_DEVICES
|
||||
content/aboutDevices.xhtml (content/aboutDevices.xhtml)
|
||||
content/aboutDevices.js (content/aboutDevices.js)
|
||||
#endif
|
||||
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
|
||||
content/aboutAccounts.xhtml (content/aboutAccounts.xhtml)
|
||||
content/aboutAccounts.js (content/aboutAccounts.js)
|
||||
#endif
|
||||
content/aboutLogins.xhtml (content/aboutLogins.xhtml)
|
||||
content/aboutLogins.js (content/aboutLogins.js)
|
||||
|
|
|
@ -89,6 +89,12 @@ if (AppConstants.MOZ_DEVICES) {
|
|||
privileged: true
|
||||
};
|
||||
}
|
||||
if (!AppConstants.MOZ_ANDROID_NATIVE_ACCOUNT_UI) {
|
||||
modules['accounts'] = {
|
||||
uri: "chrome://browser/content/aboutAccounts.xhtml",
|
||||
privileged: true
|
||||
};
|
||||
}
|
||||
|
||||
function AboutRedirector() {}
|
||||
AboutRedirector.prototype = {
|
||||
|
|
|
@ -20,7 +20,9 @@ contract @mozilla.org/network/protocol/about;1?what=blocked {322ba47e-7047-4f71-
|
|||
#ifdef MOZ_DEVICES
|
||||
contract @mozilla.org/network/protocol/about;1?what=devices {322ba47e-7047-4f71-aebf-cb7d69325cd9}
|
||||
#endif
|
||||
|
||||
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
|
||||
contract @mozilla.org/network/protocol/about;1?what=accounts {322ba47e-7047-4f71-aebf-cb7d69325cd9}
|
||||
#endif
|
||||
contract @mozilla.org/network/protocol/about;1?what=logins {322ba47e-7047-4f71-aebf-cb7d69325cd9}
|
||||
|
||||
# DirectoryProvider.js
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!ENTITY aboutAccounts.connected.title "Firefox Accounts">
|
||||
<!ENTITY aboutAccounts.connected.description "You are connected as">
|
||||
<!ENTITY aboutAccounts.syncPreferences.label "Tap here to check Sync settings">
|
||||
|
||||
<!ENTITY aboutAccounts.noConnection.title "No Internet connection">
|
||||
<!ENTITY aboutAccounts.retry.label "Try again">
|
|
@ -8,6 +8,7 @@
|
|||
% locale browser @AB_CD@ %locale/@AB_CD@/browser/
|
||||
locale/@AB_CD@/browser/about.dtd (%chrome/about.dtd)
|
||||
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
|
||||
locale/@AB_CD@/browser/aboutAccounts.dtd (%chrome/aboutAccounts.dtd)
|
||||
locale/@AB_CD@/browser/aboutAccounts.properties (%chrome/aboutAccounts.properties)
|
||||
#endif
|
||||
locale/@AB_CD@/browser/aboutAddons.dtd (%chrome/aboutAddons.dtd)
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div {
|
||||
transition: opacity 0.4s ease-in;
|
||||
}
|
||||
|
||||
#spinner {
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
|
||||
#remote {
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease-in;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #363B40;
|
||||
font-size: 25px;
|
||||
font-weight: lighter;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #777777;
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0096DD; /* link_blue */
|
||||
text-decoration: none;
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #0082C6; /* link_blue_pressed */
|
||||
}
|
||||
|
||||
.toplevel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
padding-top: 60px;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
flex: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
flex: 1;
|
||||
height: 60px;
|
||||
background-color: #E66000; /*matched to action_orange in java codebase*/
|
||||
color: #FFFFFF;
|
||||
font-size: 20px;
|
||||
border-radius: 4px;
|
||||
border-width: 0px;
|
||||
}
|
|
@ -8,6 +8,9 @@ chrome.jar:
|
|||
% skin browser classic/1.0 %skin/
|
||||
skin/aboutPage.css (aboutPage.css)
|
||||
skin/about.css (about.css)
|
||||
#ifndef MOZ_ANDROID_NATIVE_ACCOUNT_UI
|
||||
skin/aboutAccounts.css (aboutAccounts.css)
|
||||
#endif
|
||||
* skin/aboutAddons.css (aboutAddons.css)
|
||||
* skin/aboutBase.css (aboutBase.css)
|
||||
#ifdef MOZ_DEVICES
|
||||
|
|
Загрузка…
Ссылка в новой задаче