Import: choice on first startup, workflow, ported to Node.js fs API
FREEBIE
This commit is contained in:
parent
9c8fe1a9d8
commit
ba347744ff
12 changed files with 616 additions and 217 deletions
|
@ -15,10 +15,18 @@
|
|||
"message": "Migrate",
|
||||
"description": "Button label to begin migrating this client to Electron"
|
||||
},
|
||||
"export": {
|
||||
"chooseDirectory": {
|
||||
"message": "Choose directory",
|
||||
"description": "Button to allow the user to export all data from app as part of migration process"
|
||||
},
|
||||
"exportButton": {
|
||||
"message": "Export",
|
||||
"desription": "Button shown on the choose directory dialog which starts the export process"
|
||||
},
|
||||
"exportChooserTitle": {
|
||||
"message": "Choose target directory for data",
|
||||
"description": "Title of the popup window used to select data storage location"
|
||||
},
|
||||
"exportAgain": {
|
||||
"message": "Export again",
|
||||
"description": "If user has already exported once, this button allows user to do it again if needed"
|
||||
|
@ -53,6 +61,34 @@
|
|||
"message": "Install new Signal Desktop",
|
||||
"description": "When export is complete, a button shows which sends user to Signal Desktop install instructions"
|
||||
},
|
||||
"importButton": {
|
||||
"message": "Import",
|
||||
"desription": "Button shown on the choose directory dialog which starts the import process"
|
||||
},
|
||||
"importChooserTitle": {
|
||||
"message": "Choose directory with exported data",
|
||||
"description": "Title of the popup window used to select data previously exported"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Unfortunately, something went wrong during the import. First, make sure all of the originally exported files are available. Then, please submit a debug log so we can help you get migrated!",
|
||||
"description": "Message shown if the import went wrong."
|
||||
},
|
||||
"tryAgain": {
|
||||
"message": "Try again",
|
||||
"description": "Button shown if the user runs into an error during import, allowing them to start over"
|
||||
},
|
||||
"importInstructions": {
|
||||
"message": "The first step is to tell us where you previously exported your Signal data. It will be a directory whose name starts with 'Signal Export.'",
|
||||
"description": "Description of the export process"
|
||||
},
|
||||
"importing": {
|
||||
"message": "Please wait while we import your data...",
|
||||
"description": "Shown as we are loading the user's data from disk"
|
||||
},
|
||||
"importComplete": {
|
||||
"message": "We've successfully loaded your data. The next step is to restart the application!",
|
||||
"description": "Shown when the import is complete."
|
||||
},
|
||||
"selectedLocation": {
|
||||
"message": "your selected location",
|
||||
"description": "Message shown as the export location if we didn't capture the target directory"
|
||||
|
@ -442,6 +478,14 @@
|
|||
"message": "Privacy is possible. Signal makes it easy.",
|
||||
"description": "Tagline displayed under 'installWelcome' string on the install page"
|
||||
},
|
||||
"installNew": {
|
||||
"message": "Set up as new install",
|
||||
"description": "One of two choices presented on the screen shown on first launch"
|
||||
},
|
||||
"installImport": {
|
||||
"message": "Set up with exported data",
|
||||
"description": "One of two choices presented on the screen shown on first launch"
|
||||
},
|
||||
"installGetStartedButton": {
|
||||
"message": "Get started"
|
||||
},
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
<script type='text/x-tmpl-mustache' id='app-migration-screen'>
|
||||
<div class='content'>
|
||||
<img src='/images/icon_128.png'>
|
||||
<img src='images/icon_128.png'>
|
||||
{{ ^hideProgress }}
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
|
@ -32,15 +32,15 @@
|
|||
{{ /hideProgress }}
|
||||
<div class='message'>{{& message }}</div>
|
||||
<div>
|
||||
{{ #installButton }}
|
||||
<button class='install grey'>{{ installButton }}</button>
|
||||
{{ /installButton }}
|
||||
{{ #exportButton }}
|
||||
<button class='export grey'>{{ exportButton }}</button>
|
||||
{{ /exportButton }}
|
||||
{{ #debugLogButton }}
|
||||
<button class='debug-log grey'>{{ debugLogButton }}</button>
|
||||
{{ /debugLogButton }}
|
||||
{{ #importButton }}
|
||||
<button class='import grey'>{{ importButton }}</button>
|
||||
{{ /importButton }}
|
||||
{{ #restartButton }}
|
||||
<button class='restart grey'>{{ restartButton }}</button>
|
||||
{{ /restartButton }}
|
||||
{{ #cancelButton }}
|
||||
<button class='cancel grey'>{{ cancelButton }}</button>
|
||||
{{ /cancelButton }}
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
@ -629,8 +629,24 @@
|
|||
{{/action }}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='install-choice'>
|
||||
<div class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<img id='signal-icon' src='images/icon_250.png'/>
|
||||
<h1>{{ installWelcome }}</h1>
|
||||
<p>{{ installTagline }}</p>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<div> <a class='button new'>{{ installNew }}</a> </div>
|
||||
<div> <a class='button import'>{{ installImport }}</a> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='install_flow_template'>
|
||||
<div id='step1' class='step'>
|
||||
<div id='step1' class='step hidden'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<img id='signal-icon' src='images/icon_250.png'/>
|
||||
|
@ -639,14 +655,16 @@
|
|||
</div>
|
||||
<div class='nav'>
|
||||
<div> <a class='button step2'>{{ installGetStartedButton }}</a> </div>
|
||||
<span class='dot step1 selected'></span>
|
||||
<span class='dot step2'></span>
|
||||
<span class='dot step3'></span>
|
||||
<div class='dot-container'>
|
||||
<span class='dot step1 selected'></span>
|
||||
<span class='dot step2'></span>
|
||||
<span class='dot step3'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='step2' class='step'>
|
||||
<div id='step2' class='step hidden'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<img id='signal-phone' src='images/signal-phone.png'>
|
||||
|
@ -654,14 +672,16 @@
|
|||
</div>
|
||||
<div class='nav'>
|
||||
<div> <a class='button step3'>{{ installIHaveSignalButton }}</a> </div>
|
||||
<span class='dot step1'></span>
|
||||
<span class='dot step2 selected'></span>
|
||||
<span class='dot step3'></span>
|
||||
<div class='dot-container'>
|
||||
<span class='dot step1'></span>
|
||||
<span class='dot step2 selected'></span>
|
||||
<span class='dot step3'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='step3' class='step'>
|
||||
<div id='step3' class='step hidden'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<div id="qr"></div>
|
||||
|
@ -671,14 +691,16 @@
|
|||
{{ #development }}
|
||||
<div> <a class='button openStandalone'>Standalone</a> </div>
|
||||
{{ /development }}
|
||||
<span class='dot step1'></span>
|
||||
<span class='dot step2'></span>
|
||||
<span class='dot step3 selected'></span>
|
||||
<div class='dot-container'>
|
||||
<span class='dot step1'></span>
|
||||
<span class='dot step2'></span>
|
||||
<span class='dot step3 selected'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id='step4' class='step'>
|
||||
<form id='step4' class='step hidden'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<p>{{ installLinkingWithNumber }}</p>
|
||||
|
@ -697,7 +719,7 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<div id='step5' class='step'>
|
||||
<div id='step5' class='step hidden'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<img id='signal-icon' src='images/icon_250.png'/>
|
||||
|
@ -711,7 +733,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id='stepTooManyDevices' class='step'>
|
||||
<div id='stepTooManyDevices' class='step hidden'>
|
||||
<div class='inner error-dialog clearfix'>
|
||||
<div class='panel step-body'>{{ installTooManyDevices }}</div>
|
||||
<div class='nav'>
|
||||
|
@ -822,11 +844,12 @@
|
|||
<script type="text/javascript" src="js/views/phone-input-view.js"></script>
|
||||
<script type='text/javascript' src='js/views/standalone_registration_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/app_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/install_choice_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/import_view.js'></script>
|
||||
|
||||
<script type='text/javascript' src='js/wall_clock_listener.js'></script>
|
||||
<script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script>
|
||||
<script type='text/javascript' src='js/keychange_listener.js'></script>
|
||||
<script type='text/javascript' src='js/backup.js'></script>
|
||||
<script type='text/javascript' src='js/background.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -53,7 +53,35 @@
|
|||
};
|
||||
|
||||
storage.fetch();
|
||||
|
||||
// We need this 'first' check because we don't want to start the app up any other time
|
||||
// than the first time. And storage.fetch() will cause onready() to fire.
|
||||
var first = true;
|
||||
storage.onready(function() {
|
||||
if (!first) {
|
||||
return;
|
||||
}
|
||||
first = false;
|
||||
|
||||
start();
|
||||
});
|
||||
|
||||
window.getSyncRequest = function() {
|
||||
return new textsecure.SyncRequest(textsecure.messaging, messageReceiver);
|
||||
};
|
||||
|
||||
Whisper.events.on('shutdown', function() {
|
||||
if (messageReceiver) {
|
||||
messageReceiver.close().then(function() {
|
||||
messageReceiver = null;
|
||||
Whisper.events.trigger('shutdown-complete');
|
||||
});
|
||||
} else {
|
||||
Whisper.events.trigger('shutdown-complete');
|
||||
}
|
||||
});
|
||||
|
||||
function start() {
|
||||
ConversationController.load();
|
||||
|
||||
window.dispatchEvent(new Event('storage_ready'));
|
||||
|
@ -61,7 +89,7 @@
|
|||
console.log("listening for registration events");
|
||||
Whisper.events.on('registration_done', function() {
|
||||
console.log("handling registration event");
|
||||
init(true);
|
||||
connect(true);
|
||||
});
|
||||
|
||||
var appView = window.owsDesktopApp.appView = new Whisper.AppView({el: $('body')});
|
||||
|
@ -70,13 +98,16 @@
|
|||
Whisper.RotateSignedPreKeyListener.init(Whisper.events);
|
||||
Whisper.ExpiringMessagesListener.init(Whisper.events);
|
||||
|
||||
if (Whisper.Registration.everDone()) {
|
||||
init();
|
||||
if (Whisper.Import.isIncomplete()) {
|
||||
console.log('Import was interrupted, showing import error screen');
|
||||
appView.openImporter();
|
||||
} else if (Whisper.Registration.everDone()) {
|
||||
connect();
|
||||
appView.openInbox({
|
||||
initialLoadComplete: initialLoadComplete
|
||||
});
|
||||
} else {
|
||||
appView.openInstaller();
|
||||
appView.openInstallChoice();
|
||||
}
|
||||
|
||||
Whisper.events.on('showDebugLog', function() {
|
||||
|
@ -109,7 +140,7 @@
|
|||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.getSyncRequest = function() {
|
||||
return new textsecure.SyncRequest(textsecure.messaging, messageReceiver);
|
||||
|
@ -126,11 +157,12 @@
|
|||
}
|
||||
});
|
||||
|
||||
function init(firstRun) {
|
||||
window.removeEventListener('online', init);
|
||||
function connect(firstRun) {
|
||||
window.removeEventListener('online', connect);
|
||||
|
||||
if (!Whisper.Registration.isDone()) { return; }
|
||||
if (Whisper.Migration.inProgress()) { return; }
|
||||
if (Whisper.Import.isIncomplete()) { return; }
|
||||
|
||||
if (messageReceiver) { messageReceiver.close(); }
|
||||
|
||||
|
@ -398,13 +430,13 @@
|
|||
// Failed to connect to server
|
||||
if (navigator.onLine) {
|
||||
console.log('retrying in 1 minute');
|
||||
setTimeout(init, 60000);
|
||||
setTimeout(connect, 60000);
|
||||
|
||||
Whisper.events.trigger('reconnectTimer');
|
||||
} else {
|
||||
console.log('offline');
|
||||
if (messageReceiver) { messageReceiver.close(); }
|
||||
window.addEventListener('online', init);
|
||||
window.addEventListener('online', connect);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
272
js/backup.js
272
js/backup.js
|
@ -2,10 +2,12 @@
|
|||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
function stringToBlob(string) {
|
||||
var buffer = dcodeIO.ByteBuffer.wrap(string).toArrayBuffer();
|
||||
return new Blob([buffer]);
|
||||
}
|
||||
var electronRemote = require('electron').remote;
|
||||
var dialog = electronRemote.dialog;
|
||||
var BrowserWindow = electronRemote.BrowserWindow;
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
function stringify(object) {
|
||||
for (var key in object) {
|
||||
|
@ -41,21 +43,34 @@
|
|||
return object;
|
||||
}
|
||||
|
||||
function createOutputStream(fileWriter) {
|
||||
function createOutputStream(writer) {
|
||||
var wait = Promise.resolve();
|
||||
var count = 0;
|
||||
return {
|
||||
write: function(string) {
|
||||
var i = count++;
|
||||
wait = wait.then(function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fileWriter.onwriteend = resolve;
|
||||
fileWriter.onerror = reject;
|
||||
fileWriter.onabort = reject;
|
||||
fileWriter.write(stringToBlob(string));
|
||||
return new Promise(function(resolve) {
|
||||
if (writer.write(string)) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
// If write() returns true, we don't need to wait for the drain event
|
||||
// https://nodejs.org/dist/latest-v7.x/docs/api/stream.html#stream_class_stream_writable
|
||||
writer.once('drain', resolve);
|
||||
|
||||
// We don't register for the 'error' event here, only in close(). Otherwise,
|
||||
// we'll get "Possible EventEmitter memory leak detected" warnings.
|
||||
});
|
||||
});
|
||||
return wait;
|
||||
},
|
||||
close: function() {
|
||||
return wait.then(function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
writer.once('finish', resolve);
|
||||
writer.once('error', reject);
|
||||
writer.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -85,7 +100,7 @@
|
|||
stream.write('{');
|
||||
|
||||
_.each(storeNames, function(storeName) {
|
||||
var transaction = idb_db.transaction(storeNames, "readwrite");
|
||||
var transaction = idb_db.transaction(storeNames, 'readwrite');
|
||||
transaction.onerror = function(error) {
|
||||
console.log(
|
||||
'exportToJsonFile: transaction error',
|
||||
|
@ -129,7 +144,9 @@
|
|||
stream.write(',');
|
||||
} else {
|
||||
console.log('Exported all stores');
|
||||
stream.write('}').then(function() {
|
||||
stream.write('}');
|
||||
|
||||
stream.close().then(function() {
|
||||
console.log('Finished writing all stores to disk');
|
||||
resolve();
|
||||
});
|
||||
|
@ -158,13 +175,19 @@
|
|||
var importObject = JSON.parse(jsonString);
|
||||
var storeNames = _.keys(importObject);
|
||||
|
||||
console.log('Importing to these stores:', storeNames);
|
||||
console.log('Importing to these stores:', storeNames.join(', '));
|
||||
|
||||
var transaction = idb_db.transaction(storeNames, "readwrite");
|
||||
var transaction = idb_db.transaction(storeNames, 'readwrite');
|
||||
transaction.onerror = reject;
|
||||
|
||||
_.each(storeNames, function(storeName) {
|
||||
console.log('Importing items for store', storeName);
|
||||
|
||||
if (!importObject[storeName].length) {
|
||||
delete importObject[storeName];
|
||||
return;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
_.each(importObject[storeName], function(toAdd) {
|
||||
toAdd = unstringify(toAdd);
|
||||
|
@ -218,52 +241,56 @@
|
|||
}
|
||||
|
||||
function createDirectory(parent, name) {
|
||||
var sanitized = sanitizeFileName(name);
|
||||
return new Promise(function(resolve, reject) {
|
||||
parent.getDirectory(sanitized, {create: true, exclusive: true}, resolve, reject);
|
||||
var sanitized = sanitizeFileName(name);
|
||||
var targetDir = path.join(parent, sanitized);
|
||||
fs.mkdir(targetDir, function(error) {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
return resolve(targetDir);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createFileAndWriter(parent, name) {
|
||||
var sanitized = sanitizeFileName(name);
|
||||
return new Promise(function(resolve, reject) {
|
||||
parent.getFile(sanitized, {create: true, exclusive: true}, function(file) {
|
||||
return file.createWriter(function(writer) {
|
||||
resolve(writer);
|
||||
}, reject);
|
||||
}, reject);
|
||||
return new Promise(function(resolve) {
|
||||
var sanitized = sanitizeFileName(name);
|
||||
var targetPath = path.join(parent, sanitized);
|
||||
var options = {
|
||||
flags: 'wx'
|
||||
};
|
||||
return resolve(fs.createWriteStream(targetPath, options));
|
||||
});
|
||||
}
|
||||
|
||||
function readFileAsText(parent, name) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
parent.getFile(name, {create: false, exclusive: true}, function(fileEntry) {
|
||||
fileEntry.file(function(file) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
resolve(e.target.result);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.onabort = reject;
|
||||
reader.readAsText(file);
|
||||
}, reject);
|
||||
}, reject);
|
||||
var targetPath = path.join(parent, name);
|
||||
fs.readFile(targetPath, 'utf8', function(error, string) {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
return resolve(string);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function readFileAsArrayBuffer(parent, name) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
parent.getFile(name, {create: false, exclusive: true}, function(fileEntry) {
|
||||
fileEntry.file(function(file) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
resolve(e.target.result);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.onabort = reject;
|
||||
reader.readAsArrayBuffer(file);
|
||||
}, reject);
|
||||
}, reject);
|
||||
var targetPath = path.join(parent, name);
|
||||
// omitting the encoding to get a buffer back
|
||||
fs.readFile(targetPath, function(error, buffer) {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
// Buffer instances are also Uint8Array instances
|
||||
// https://nodejs.org/docs/latest/api/buffer.html#buffer_buffers_and_typedarray
|
||||
return resolve(buffer.buffer);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -299,15 +326,14 @@
|
|||
}
|
||||
|
||||
function readAttachment(parent, message, attachment) {
|
||||
var name = getAttachmentFileName(attachment);
|
||||
var sanitized = sanitizeFileName(name);
|
||||
var attachmentDir = message.received_at;
|
||||
return new Promise(function(resolve, reject) {
|
||||
parent.getDirectory(attachmentDir, {create: false, exclusive: true}, function(dir) {
|
||||
return readFileAsArrayBuffer(dir, sanitized ).then(function(contents) {
|
||||
attachment.data = contents;
|
||||
return resolve();
|
||||
}, reject);
|
||||
var name = getAttachmentFileName(attachment);
|
||||
var sanitized = sanitizeFileName(name);
|
||||
var attachmentDir = path.join(parent, message.received_at.toString());
|
||||
|
||||
return readFileAsArrayBuffer(attachmentDir, sanitized).then(function(contents) {
|
||||
attachment.data = contents;
|
||||
return resolve();
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
@ -316,7 +342,8 @@
|
|||
var filename = getAttachmentFileName(attachment);
|
||||
return createFileAndWriter(dir, filename).then(function(writer) {
|
||||
var stream = createOutputStream(writer);
|
||||
return stream.write(attachment.data);
|
||||
stream.write(new Buffer(attachment.data));
|
||||
return stream.close();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -344,7 +371,7 @@
|
|||
console.log('exporting conversation', name);
|
||||
return createFileAndWriter(dir, 'messages.json').then(function(writer) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var transaction = idb_db.transaction('messages', "readwrite");
|
||||
var transaction = idb_db.transaction('messages', 'readwrite');
|
||||
transaction.onerror = function(e) {
|
||||
console.log(
|
||||
'exportConversation transaction error for conversation',
|
||||
|
@ -406,10 +433,11 @@
|
|||
count += 1;
|
||||
cursor.continue();
|
||||
} else {
|
||||
var promise = stream.write(']}');
|
||||
promiseChain = promiseChain.then(promise);
|
||||
stream.write(']}');
|
||||
|
||||
return promiseChain.then(function() {
|
||||
var promise = stream.close();
|
||||
|
||||
return promiseChain.then(promise).then(function() {
|
||||
console.log('done exporting conversation', name);
|
||||
return resolve();
|
||||
}, function(error) {
|
||||
|
@ -457,7 +485,7 @@
|
|||
|
||||
function exportConversations(idb_db, parentDir) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var transaction = idb_db.transaction('conversations', "readwrite");
|
||||
var transaction = idb_db.transaction('conversations', 'readwrite');
|
||||
transaction.onerror = function(e) {
|
||||
console.log(
|
||||
'exportConversations: transaction error:',
|
||||
|
@ -503,44 +531,40 @@
|
|||
});
|
||||
}
|
||||
|
||||
function getDirectory() {
|
||||
function getDirectory(options) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var w = extension.windows.getViews()[0];
|
||||
if (!w || !w.chrome || !w.chrome.fileSystem) {
|
||||
return reject(new Error('Ran into problem accessing Chrome filesystem API'));
|
||||
}
|
||||
var browserWindow = BrowserWindow.getFocusedWindow();
|
||||
var dialogOptions = {
|
||||
title: options.title,
|
||||
properties: ['openDirectory'],
|
||||
buttonLabel: options.buttonLabel
|
||||
};
|
||||
|
||||
w.chrome.fileSystem.chooseEntry({type: 'openDirectory'}, function(entry) {
|
||||
if (!entry) {
|
||||
dialog.showOpenDialog(browserWindow, dialogOptions, function(directory) {
|
||||
if (!directory || !directory[0]) {
|
||||
var error = new Error('Error choosing directory');
|
||||
error.name = 'ChooseError';
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
return resolve(entry);
|
||||
return resolve(directory[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getDirContents(dir) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var reader = dir.createReader();
|
||||
var contents = [];
|
||||
fs.readdir(dir, function(err, files) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
var getContents = function() {
|
||||
reader.readEntries(function(results) {
|
||||
if (results.length) {
|
||||
contents = contents.concat(results);
|
||||
getContents();
|
||||
} else {
|
||||
return resolve(contents);
|
||||
}
|
||||
}, function(error) {
|
||||
return reject(error);
|
||||
files = _.map(files, function(file) {
|
||||
return path.join(dir, file);
|
||||
});
|
||||
};
|
||||
|
||||
getContents();
|
||||
resolve(files);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -556,10 +580,10 @@
|
|||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var transaction = idb_db.transaction('messages', "readwrite");
|
||||
var transaction = idb_db.transaction('messages', 'readwrite');
|
||||
transaction.onerror = function(e) {
|
||||
console.log(
|
||||
'importConversations transaction error:',
|
||||
'saveAllMessages transaction error:',
|
||||
e && e.stack ? e.stack : e
|
||||
);
|
||||
return reject(e);
|
||||
|
@ -614,7 +638,7 @@
|
|||
return saveAllMessages(idb_db, messages);
|
||||
});
|
||||
}, function() {
|
||||
console.log('Warning: could not access messages.json in directory: ' + dir.fullPath);
|
||||
console.log('Warning: could not access messages.json in directory: ' + dir);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -623,7 +647,7 @@
|
|||
var promiseChain = Promise.resolve();
|
||||
|
||||
_.forEach(contents, function(conversationDir) {
|
||||
if (!conversationDir.isDirectory) {
|
||||
if (!fs.statSync(conversationDir).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -638,10 +662,45 @@
|
|||
});
|
||||
}
|
||||
|
||||
function getDisplayPath(entry) {
|
||||
return new Promise(function(resolve) {
|
||||
chrome.fileSystem.getDisplayPath(entry, function(path) {
|
||||
return resolve(path);
|
||||
function clearAllStores(idb_db) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
console.log('Clearing all indexeddb stores');
|
||||
var storeNames = idb_db.objectStoreNames;
|
||||
var transaction = idb_db.transaction(storeNames, 'readwrite');
|
||||
|
||||
transaction.oncomplete = function() {
|
||||
// unused
|
||||
};
|
||||
transaction.onerror = function(error) {
|
||||
console.log(
|
||||
'saveAllMessages transaction error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
return reject(error);
|
||||
};
|
||||
|
||||
var count = 0;
|
||||
_.forEach(storeNames, function(storeName) {
|
||||
var store = transaction.objectStore(storeName);
|
||||
var request = store.clear();
|
||||
|
||||
request.onsuccess = function() {
|
||||
count += 1;
|
||||
console.log('Done clearing store', storeName);
|
||||
|
||||
if (count >= storeNames.length) {
|
||||
console.log('Done clearing all indexeddb stores');
|
||||
return resolve();
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = function(error) {
|
||||
console.log(
|
||||
'clearAllStores transaction error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
return reject(error);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -651,21 +710,30 @@
|
|||
}
|
||||
|
||||
Whisper.Backup = {
|
||||
clearDatabase: function() {
|
||||
return openDatabase().then(function(idb_db) {
|
||||
return clearAllStores(idb_db);
|
||||
});
|
||||
},
|
||||
backupToDirectory: function() {
|
||||
return getDirectory().then(function(directoryEntry) {
|
||||
var options = {
|
||||
title: i18n('exportChooserTitle'),
|
||||
buttonLabel: i18n('exportButton'),
|
||||
};
|
||||
return getDirectory(options).then(function(directory) {
|
||||
var idb;
|
||||
var dir;
|
||||
return openDatabase().then(function(idb_db) {
|
||||
idb = idb_db;
|
||||
var name = 'Signal Export ' + getTimestamp();
|
||||
return createDirectory(directoryEntry, name);
|
||||
}).then(function(directory) {
|
||||
dir = directory;
|
||||
return createDirectory(directory, name);
|
||||
}).then(function(created) {
|
||||
dir = created;
|
||||
return exportNonMessages(idb, dir);
|
||||
}).then(function() {
|
||||
return exportConversations(idb, dir);
|
||||
}).then(function() {
|
||||
return getDisplayPath(dir);
|
||||
return dir;
|
||||
});
|
||||
}).then(function(path) {
|
||||
console.log('done backing up!');
|
||||
|
@ -679,15 +747,19 @@
|
|||
});
|
||||
},
|
||||
importFromDirectory: function() {
|
||||
return getDirectory().then(function(directoryEntry) {
|
||||
var options = {
|
||||
title: i18n('importChooserTitle'),
|
||||
buttonLabel: i18n('importButton'),
|
||||
};
|
||||
return getDirectory(options).then(function(directory) {
|
||||
var idb;
|
||||
return openDatabase().then(function(idb_db) {
|
||||
idb = idb_db;
|
||||
return importNonMessages(idb_db, directoryEntry);
|
||||
return importNonMessages(idb_db, directory);
|
||||
}).then(function() {
|
||||
return importConversations(idb, directoryEntry);
|
||||
return importConversations(idb, directory);
|
||||
}).then(function() {
|
||||
return displayPath(directoryEntry);
|
||||
return directory;
|
||||
});
|
||||
}).then(function(path) {
|
||||
console.log('done restoring from backup!');
|
||||
|
|
|
@ -29,15 +29,47 @@
|
|||
this.debugLogView = null;
|
||||
}
|
||||
},
|
||||
openInstallChoice: function() {
|
||||
this.closeInstallChoice();
|
||||
var installChoice = this.installChoice = new Whisper.InstallChoiceView();
|
||||
|
||||
this.listenTo(installChoice, 'install-new', this.openInstaller.bind(this));
|
||||
this.listenTo(installChoice, 'install-import', this.openImporter.bind(this));
|
||||
|
||||
this.openView(this.installChoice);
|
||||
},
|
||||
closeInstallChoice: function() {
|
||||
if (this.installChoice) {
|
||||
this.installChoice.remove();
|
||||
this.installChoice = null;
|
||||
}
|
||||
},
|
||||
openImporter: function() {
|
||||
this.closeImporter();
|
||||
this.closeInstallChoice();
|
||||
var importView = this.importView = new Whisper.ImportView();
|
||||
this.listenTo(importView, 'cancel', this.openInstallChoice.bind(this));
|
||||
this.openView(this.importView);
|
||||
},
|
||||
closeImporter: function() {
|
||||
if (this.importView) {
|
||||
this.importView.remove();
|
||||
this.importView = null;
|
||||
}
|
||||
},
|
||||
openInstaller: function() {
|
||||
this.closeInstaller();
|
||||
this.installView = new Whisper.InstallView();
|
||||
if (Whisper.Registration.everDone()) {
|
||||
this.installView.selectStep(3);
|
||||
this.installView.hideDots();
|
||||
}
|
||||
this.closeInstallChoice();
|
||||
var installView = this.installView = new Whisper.InstallView();
|
||||
this.listenTo(installView, 'cancel', this.openInstallChoice.bind(this));
|
||||
this.openView(this.installView);
|
||||
},
|
||||
closeInstaller: function() {
|
||||
if (this.installView) {
|
||||
this.installView.remove();
|
||||
this.installView = null;
|
||||
}
|
||||
},
|
||||
openStandalone: function() {
|
||||
if (window.config.environment !== 'production') {
|
||||
this.closeInstaller();
|
||||
|
@ -45,12 +77,6 @@
|
|||
this.openView(this.installView);
|
||||
}
|
||||
},
|
||||
closeInstaller: function() {
|
||||
if (this.installView) {
|
||||
this.installView.remove();
|
||||
this.installView = null;
|
||||
}
|
||||
},
|
||||
openInbox: function(options) {
|
||||
options = options || {};
|
||||
_.defaults(options, {initialLoadComplete: false});
|
||||
|
|
153
js/views/import_view.js
Normal file
153
js/views/import_view.js
Normal file
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
var State = {
|
||||
IMPORTING: 1,
|
||||
COMPLETE: 2
|
||||
};
|
||||
|
||||
var IMPORT_STARTED = 'importStarted';
|
||||
var IMPORT_COMPLETE = 'importComplete';
|
||||
var IMPORT_LOCATION = 'importLocation';
|
||||
|
||||
Whisper.Import = {
|
||||
isStarted: function() {
|
||||
return Boolean(storage.get(IMPORT_STARTED));
|
||||
},
|
||||
isComplete: function() {
|
||||
return Boolean(storage.get(IMPORT_COMPLETE));
|
||||
},
|
||||
isIncomplete: function() {
|
||||
return this.isStarted() && !this.isComplete();
|
||||
},
|
||||
start: function() {
|
||||
storage.put(IMPORT_STARTED, true);
|
||||
},
|
||||
complete: function() {
|
||||
storage.put(IMPORT_COMPLETE, true);
|
||||
},
|
||||
saveLocation: function(location) {
|
||||
storage.put(IMPORT_LOCATION, location);
|
||||
},
|
||||
reset: function() {
|
||||
return Whisper.Backup.clearDatabase();
|
||||
}
|
||||
};
|
||||
|
||||
Whisper.ImportView = Whisper.View.extend({
|
||||
templateName: 'app-migration-screen',
|
||||
className: 'app-loading-screen',
|
||||
events: {
|
||||
'click .import': 'onImport',
|
||||
'click .restart': 'onRestart',
|
||||
'click .cancel': 'onCancel',
|
||||
},
|
||||
initialize: function() {
|
||||
if (Whisper.Import.isIncomplete()) {
|
||||
this.error = true;
|
||||
}
|
||||
|
||||
this.render();
|
||||
this.pending = Promise.resolve();
|
||||
},
|
||||
render_attributes: function() {
|
||||
var message;
|
||||
var importButton;
|
||||
var hideProgress = true;
|
||||
var restartButton;
|
||||
var cancelButton;
|
||||
|
||||
if (this.error) {
|
||||
return {
|
||||
message: i18n('importError'),
|
||||
hideProgress: true,
|
||||
importButton: i18n('tryAgain'),
|
||||
};
|
||||
}
|
||||
|
||||
switch (this.state) {
|
||||
case State.COMPLETE:
|
||||
message = i18n('importComplete');
|
||||
restartButton = i18n('restartSignal');
|
||||
break;
|
||||
case State.IMPORTING:
|
||||
message = i18n('importing');
|
||||
hideProgress = false;
|
||||
break;
|
||||
default:
|
||||
message = i18n('importInstructions');
|
||||
importButton = i18n('chooseDirectory');
|
||||
cancelButton = i18n('cancel');
|
||||
}
|
||||
|
||||
return {
|
||||
hideProgress: hideProgress,
|
||||
message: message,
|
||||
importButton: importButton,
|
||||
restartButton: restartButton,
|
||||
cancelButton: cancelButton,
|
||||
};
|
||||
},
|
||||
onRestart: function() {
|
||||
return window.restart();
|
||||
},
|
||||
onCancel: function() {
|
||||
this.trigger('cancel');
|
||||
},
|
||||
onImport: function() {
|
||||
this.error = null;
|
||||
|
||||
this.state = State.IMPORTING;
|
||||
this.render();
|
||||
|
||||
var importLocation;
|
||||
|
||||
// Wait for prior database interaction to complete
|
||||
this.pending = this.pending.then(function() {
|
||||
// For resilience to interruptions, clear database both before import and after
|
||||
return Whisper.Backup.clearDatabase();
|
||||
}).then(function() {
|
||||
Whisper.Import.start();
|
||||
return Whisper.Backup.importFromDirectory();
|
||||
}).then(function(directory) {
|
||||
importLocation = directory;
|
||||
|
||||
// Catching in-memory cache up with what's in indexeddb now...
|
||||
// NOTE: this fires storage.onready, listened to across the app. We'll restart
|
||||
// to complete the install to start up cleanly with everything now in the DB.
|
||||
return storage.fetch();
|
||||
}).then(function() {
|
||||
// Clearing any migration-related state inherited from the Chome App
|
||||
storage.remove('migrationState');
|
||||
storage.remove('migrationEnabled');
|
||||
storage.remove('migrationEverCompleted');
|
||||
storage.remove('migrationStorageLocation');
|
||||
|
||||
if (importLocation) {
|
||||
Whisper.Import.saveLocation(importLocation);
|
||||
}
|
||||
|
||||
Whisper.Import.complete();
|
||||
|
||||
this.state = State.COMPLETE;
|
||||
this.render();
|
||||
}.bind(this)).catch(function(error) {
|
||||
if (error.name !== 'ChooseError') {
|
||||
this.error = error.message;
|
||||
console.log('Error importing:', error && error.stack ? error.stack : error);
|
||||
}
|
||||
|
||||
this.state = null;
|
||||
this.render();
|
||||
|
||||
if (this.error) {
|
||||
return Whisper.Backup.clearDatabase();
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
});
|
||||
})();
|
31
js/views/install_choice_view.js
Normal file
31
js/views/install_choice_view.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* vim: ts=4:sw=4:expandtab
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.InstallChoiceView = Whisper.View.extend({
|
||||
templateName: 'install-choice',
|
||||
className: 'install install-choice',
|
||||
events: {
|
||||
'click .new': 'onClickNew',
|
||||
'click .import': 'onClickImport'
|
||||
},
|
||||
initialize: function() {
|
||||
this.render();
|
||||
},
|
||||
render_attributes: {
|
||||
installWelcome: i18n('installWelcome'),
|
||||
installTagline: i18n('installTagline'),
|
||||
installNew: i18n('installNew'),
|
||||
installImport: i18n('installImport')
|
||||
},
|
||||
onClickNew: function() {
|
||||
this.trigger('install-new');
|
||||
},
|
||||
onClickImport: function() {
|
||||
this.trigger('install-import');
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -7,8 +7,7 @@
|
|||
|
||||
Whisper.InstallView = Whisper.View.extend({
|
||||
templateName: 'install_flow_template',
|
||||
id: 'install',
|
||||
className: 'main',
|
||||
className: 'main install',
|
||||
render_attributes: function() {
|
||||
var twitterHref = 'https://twitter.com/whispersystems';
|
||||
var signalHref = 'https://signal.org/install';
|
||||
|
@ -48,6 +47,11 @@
|
|||
this.$('#step1').show();
|
||||
this.connect();
|
||||
this.on('disconnected', this.reconnect);
|
||||
|
||||
if (Whisper.Registration.everDone()) {
|
||||
this.installView.selectStep(3);
|
||||
this.installView.hideDots();
|
||||
}
|
||||
},
|
||||
connect: function() {
|
||||
this.clearQR();
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
|
||||
Whisper.StandaloneRegistrationView = Whisper.View.extend({
|
||||
templateName: 'standalone',
|
||||
id: 'install',
|
||||
className: 'main',
|
||||
className: 'install main',
|
||||
initialize: function() {
|
||||
this.accountManager = getAccountManager();
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
console.log('preload');
|
||||
const electron = require('electron')
|
||||
|
||||
|
@ -88,4 +89,7 @@
|
|||
}, 30);
|
||||
});
|
||||
|
||||
// we have to pull this in this way because it references node APIs
|
||||
require('./js/backup');
|
||||
|
||||
})();
|
||||
|
|
|
@ -3330,48 +3330,49 @@ li.entry .error-icon-container {
|
|||
.iti-flag {
|
||||
background: url("../images/flags.png"); }
|
||||
|
||||
#install {
|
||||
.install {
|
||||
height: 100%;
|
||||
background: #2090ea;
|
||||
color: white;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
overflow: auto; }
|
||||
#install input, #install button, #install select, #install textarea {
|
||||
.install input, .install button, .install select, .install textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit; }
|
||||
#install .main {
|
||||
.install .main {
|
||||
padding: 70px 0 50px; }
|
||||
#install .step {
|
||||
display: none;
|
||||
.install .hidden {
|
||||
display: none; }
|
||||
.install .step {
|
||||
height: 100%; }
|
||||
#install .inner {
|
||||
.install .inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: 100%; }
|
||||
#install .inner .step-body {
|
||||
.install .inner .step-body {
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
max-width: 600px; }
|
||||
#install #signal-computer,
|
||||
#install #signal-phone {
|
||||
.install #signal-computer,
|
||||
.install #signal-phone {
|
||||
max-width: 50%;
|
||||
max-height: 250px; }
|
||||
#install p {
|
||||
.install p {
|
||||
max-width: 35em;
|
||||
margin: 1em auto;
|
||||
padding: 0 1em;
|
||||
line-height: 1.5em;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold; }
|
||||
#install a {
|
||||
.install a {
|
||||
cursor: pointer; }
|
||||
#install a, #install a:visited, #install a:hover {
|
||||
.install a, .install a:visited, .install a:hover {
|
||||
text-decoration: none; }
|
||||
#install .button {
|
||||
.install .button {
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
border: none;
|
||||
|
@ -3381,14 +3382,14 @@ li.entry .error-icon-container {
|
|||
margin: 0.5em 0;
|
||||
background: white;
|
||||
color: #2090ea; }
|
||||
#install .nav {
|
||||
.install .nav {
|
||||
width: 100%;
|
||||
bottom: 50px;
|
||||
margin-top: auto;
|
||||
padding: 20px; }
|
||||
#install .nav .button {
|
||||
margin-bottom: 3em; }
|
||||
#install .nav .dot {
|
||||
.install .nav .dot-container {
|
||||
margin-top: 3em; }
|
||||
.install .nav .dot {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
|
@ -3397,40 +3398,43 @@ li.entry .error-icon-container {
|
|||
border-radius: 10px;
|
||||
background: white;
|
||||
border: solid 5px #2090ea; }
|
||||
#install .nav .dot.selected {
|
||||
.install .nav .dot.selected {
|
||||
background: #a2d2f4; }
|
||||
#install .link:hover, #install .link:focus {
|
||||
.install.install-choice .nav {
|
||||
top: 20px;
|
||||
margin-bottom: auto; }
|
||||
.install .link:hover, .install .link:focus {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
outline: none; }
|
||||
#install .link, #install .link:visited, #install .link:hover {
|
||||
.install .link, .install .link:visited, .install .link:hover {
|
||||
padding: 0 3px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border-bottom: dashed 2px white;
|
||||
text-decoration: none; }
|
||||
#install .container {
|
||||
.install .container {
|
||||
min-width: 650px; }
|
||||
#install h1 {
|
||||
.install h1 {
|
||||
font-size: 30pt;
|
||||
font-weight: normal;
|
||||
padding-bottom: 10px; }
|
||||
#install h3.step {
|
||||
.install h3.step {
|
||||
margin-top: 0;
|
||||
font-weight: bold; }
|
||||
#install .help {
|
||||
.install .help {
|
||||
border-top: 2px solid #f3f3f3;
|
||||
padding: 1.5em 0.1em; }
|
||||
#install .install {
|
||||
.install .install {
|
||||
display: inline-block;
|
||||
margin-top: 90px; }
|
||||
#install #qr {
|
||||
.install #qr {
|
||||
display: inline-block;
|
||||
min-height: 266px; }
|
||||
#install #qr img {
|
||||
.install #qr img {
|
||||
border: 5px solid white; }
|
||||
#install #qr canvas {
|
||||
.install #qr canvas {
|
||||
display: none; }
|
||||
#install #device-name {
|
||||
.install #device-name {
|
||||
border: none;
|
||||
border-bottom: 1px solid white;
|
||||
padding: 8px;
|
||||
|
@ -3438,40 +3442,40 @@ li.entry .error-icon-container {
|
|||
color: white;
|
||||
font-weight: bold;
|
||||
text-align: center; }
|
||||
#install #device-name::selection, #install #device-name a::selection {
|
||||
.install #device-name::selection, .install #device-name a::selection {
|
||||
color: #454545;
|
||||
background: white; }
|
||||
#install #device-name::-moz-selection, #install #device-name a::-moz-selection {
|
||||
.install #device-name::-moz-selection, .install #device-name a::-moz-selection {
|
||||
color: #454545;
|
||||
background: white; }
|
||||
#install #device-name:focus {
|
||||
.install #device-name:focus {
|
||||
outline: none; }
|
||||
#install #device-name:hover, #install #device-name:focus {
|
||||
.install #device-name:hover, .install #device-name:focus {
|
||||
background: rgba(255, 255, 255, 0.1); }
|
||||
#install #verifyCode,
|
||||
#install #code,
|
||||
#install #number {
|
||||
.install #verifyCode,
|
||||
.install #code,
|
||||
.install #number {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
text-align: center; }
|
||||
#install #request-voice,
|
||||
#install #request-sms {
|
||||
.install #request-voice,
|
||||
.install #request-sms {
|
||||
box-sizing: border-box; }
|
||||
#install #request-sms {
|
||||
.install #request-sms {
|
||||
width: 57%;
|
||||
float: right; }
|
||||
#install #request-voice {
|
||||
.install #request-voice {
|
||||
width: 40%;
|
||||
float: left; }
|
||||
#install .number-container {
|
||||
.install .number-container {
|
||||
position: relative;
|
||||
margin-bottom: 0.5em; }
|
||||
#install .number-container .intl-tel-input,
|
||||
#install .number-container .number {
|
||||
.install .number-container .intl-tel-input,
|
||||
.install .number-container .number {
|
||||
width: 100%; }
|
||||
#install .number-container::after {
|
||||
.install .number-container::after {
|
||||
visibility: hidden;
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
|
@ -3485,61 +3489,61 @@ li.entry .error-icon-container {
|
|||
left: 100%;
|
||||
margin: 3px 8px;
|
||||
text-align: center; }
|
||||
#install .number-container.valid::after {
|
||||
.install .number-container.valid::after {
|
||||
visibility: visible;
|
||||
content: '✓';
|
||||
background-color: #0f9d58;
|
||||
color: #ffffff; }
|
||||
#install .number-container.invalid::after {
|
||||
.install .number-container.invalid::after {
|
||||
visibility: visible;
|
||||
content: '!';
|
||||
background-color: #f44336;
|
||||
color: #ffffff; }
|
||||
#install #error {
|
||||
.install #error {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 0.5em;
|
||||
text-align: center; }
|
||||
#install #error {
|
||||
.install #error {
|
||||
background-color: #f44336; }
|
||||
#install #error:before {
|
||||
.install #error:before {
|
||||
content: '\26a0';
|
||||
padding-right: 0.5em; }
|
||||
#install .narrow {
|
||||
.install .narrow {
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
width: 275px;
|
||||
max-width: 100%; }
|
||||
#install ul.country-list {
|
||||
.install ul.country-list {
|
||||
min-width: 197px !important; }
|
||||
#install .confirmation-dialog, #install .progress-dialog, #install .error-dialog {
|
||||
.install .confirmation-dialog, .install .progress-dialog, .install .error-dialog {
|
||||
padding: 1em;
|
||||
text-align: left; }
|
||||
#install .number {
|
||||
.install .number {
|
||||
text-align: center; }
|
||||
#install .confirmation-dialog button, #install .error-dialog button {
|
||||
.install .confirmation-dialog button, .install .error-dialog button {
|
||||
float: right;
|
||||
margin-left: 10px; }
|
||||
#install .progress-dialog {
|
||||
.install .progress-dialog {
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: auto; }
|
||||
#install .progress-dialog .status {
|
||||
.install .progress-dialog .status {
|
||||
padding: 1em; }
|
||||
#install .progress-dialog .bar-container {
|
||||
.install .progress-dialog .bar-container {
|
||||
height: 1em;
|
||||
background-color: #f3f3f3;
|
||||
border: solid 1px white; }
|
||||
#install .progress-dialog .bar {
|
||||
.install .progress-dialog .bar {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
background-color: #a2d2f4;
|
||||
transition: width 0.25s; }
|
||||
#install .error-dialog {
|
||||
.install .error-dialog {
|
||||
display: none; }
|
||||
#install .modal-container {
|
||||
.install .modal-container {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
@ -3548,7 +3552,7 @@ li.entry .error-icon-container {
|
|||
top: 0;
|
||||
padding-top: 10em;
|
||||
text-align: center; }
|
||||
#install .modal-container .modal-main {
|
||||
.install .modal-container .modal-main {
|
||||
display: inline-block;
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
|
@ -3556,7 +3560,7 @@ li.entry .error-icon-container {
|
|||
background: white;
|
||||
margin: 10% auto;
|
||||
box-shadow: 0 0 5px 3px rgba(10, 62, 103, 0.2); }
|
||||
#install .modal-container .modal-main h4 {
|
||||
.install .modal-container .modal-main h4 {
|
||||
background-color: #2090ea;
|
||||
color: white;
|
||||
padding: 1em;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
background: url("../images/flags.png");
|
||||
}
|
||||
|
||||
#install {
|
||||
.install {
|
||||
height: 100%;
|
||||
background: #2090ea;
|
||||
color: white;
|
||||
|
@ -23,8 +23,10 @@
|
|||
.main {
|
||||
padding: 70px 0 50px;
|
||||
}
|
||||
.step {
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.step {
|
||||
height: 100%;
|
||||
}
|
||||
.inner {
|
||||
|
@ -81,8 +83,8 @@
|
|||
margin-top: auto;
|
||||
padding: 20px;
|
||||
|
||||
.button {
|
||||
margin-bottom: 3em;
|
||||
.dot-container {
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
.dot {
|
||||
|
@ -101,6 +103,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.install-choice .nav {
|
||||
top: 20px;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.link {
|
||||
&:hover, &:focus {
|
||||
background: rgba(255,255,255,0.3);
|
||||
|
|
Loading…
Reference in a new issue