diff --git a/Duplicati/Server/webroot/app.js b/Duplicati/Server/webroot/app.js
new file mode 100644
index 000000000..f8bdff582
--- /dev/null
+++ b/Duplicati/Server/webroot/app.js
@@ -0,0 +1,65 @@
+Ext.application({
+ requires: [
+ 'Ext.container.Viewport',
+ 'Ext.layout.container.Border',
+ 'BackupApp.Service',
+ 'BackupApp.Utility',
+ 'BackupApp.ListUtility',
+ 'BackupApp.view.AboutWindow'
+ ],
+
+ name: 'BackupApp',
+ appFolder: 'backup-app',
+
+ controllers: [
+ 'Schedules',
+ 'StatuswindowHeader',
+ 'StatuswindowFooter'
+ ],
+
+ //This object encapsulates all access to the web service
+ service: null,
+
+ launch: function() {
+
+ Ext.log = function(msg) {
+ console.log(msg);
+ }
+
+ //Pretend the app fires events too
+ this.addEvents('current-state-updated', 'count-down-pause-timer');
+
+ //Make a service object
+ this.service = new BackupApp.Service();
+ //Make the service globally accesible
+ BackupApp.service = this.service;
+ BackupApp.instance = this;
+ BackupApp.utility = this.utility;
+
+ //Pretend the app can fire the update event, this helps with initialization in controllers
+ this.service.on('current-state-updated', function(e) {
+ BackupApp.instance.fireEvent('current-state-updated', e)
+ });
+
+ this.service.on('count-down-pause-timer', function(e) {
+ BackupApp.instance.fireEvent('count-down-pause-timer', e)
+ });
+
+ //Fetch the current status
+ this.service.updateCurrentState();
+
+ Ext.create('Ext.container.Viewport', {
+ layout: 'border',
+ items: [{
+ region: 'center',
+ xtype: 'schedulelist'
+ },{
+ region: 'north',
+ xtype: 'statuswindowheader'
+ },{
+ region: 'south',
+ xtype: 'statuswindowfooter'
+ }]
+ });
+ }
+});
\ No newline at end of file
diff --git a/Duplicati/Server/webroot/backup-app/ListUtility.js b/Duplicati/Server/webroot/backup-app/ListUtility.js
new file mode 100644
index 000000000..afa88d26f
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/ListUtility.js
@@ -0,0 +1,84 @@
+Ext.define('BackupApp.ListUtility', {
+ statics: {
+
+ currentScheduleId: null,
+
+ showContextMenu: function(id, el) {
+ var lst = Ext.getCmp('status-window-schedule-list');
+ var mnu = lst.elementContextMenu;
+ this.currentScheduleId = id;
+ var elx = Ext.get(el);
+ mnu.showBy(elx, 'tr-br?');
+ },
+
+ formatSizeString: function(size) { return BackupApp.Utility.formatSizeString(size); },
+ timeAgo: function(date) { return BackupApp.Utility.timeAgo(date); },
+
+ formatBackupScheduleDetails: function(values)
+ {
+ var metadata = '';
+
+ if (values.MetadataLookup != null)
+ {
+ var metadataItems = [];
+ var mt = values.MetadataLookup;
+ if (mt['source-file-size'] != null)
+ metadataItems.push('Source files: ' + this.formatSizeString(mt['source-file-size']) + '');
+
+ if (mt['total-backup-size'] != null && parseInt(mt['total-backup-size']) > 0)
+ metadataItems.push('Backup size: ' + this.formatSizeString(mt['total-backup-size']) + '');
+
+ if (mt['assigned-quota-space'] != null || mt['free-quota-space'] != null)
+ {
+ var spaceLeft = null;
+ if (mt['assigned-quota-space'] != null && mt['free-quota-space'] != null && mt['total-backup-size'] != null)
+ spaceLeft = Math.min(parseInt(mt['assigned-quota-space']) - parseInt(mt['total-backup-size']), parseInt(mt['free-quota-space']));
+ else if (mt['assigned-quota-space'] != null && mt['total-backup-size'] != null)
+ spaceLeft = parseInt(mt['assigned-quota-space']) - parseInt(mt['total-backup-size']);
+ else if (mt['free-quota-space'] != null)
+ spaceLeft = parseInt(mt['free-quota-space']);
+
+ var extracls = '';
+ if (mt['total-backup-size'] != null)
+ {
+ //We mark it as low if there is less than 10% free
+ if (spaceLeft < (parseInt(mt['total-backup-size']) * 0.1))
+ extracls = 'class="low-storage-space"';
+ }
+
+ if (spaceLeft != null)
+ metadataItems.push('Free space: ' + this.formatSizeString(spaceLeft) + '');
+ }
+
+ if (mt['changed-file-count'] != null)
+ {
+ var changedFiles = parseInt(mt['changed-file-count']);
+ var str = changedFiles + ' ' + changedFiles == 1 ? "change" : "changes";
+
+ if (mt['last-backup-date'] != null)
+ str += ' since ' + this.timeAgo(new Date(mt['last-backup-date']));
+
+ metadataItems.push('' + str + '');
+ } else if (mt['last-backup-date'] != null || mt['last-backup-completed-time'] != null) {
+ if (mt['last-backup-completed-time'] != null)
+ metadataItems.push('Last backup: ' + this.timeAgo(new Date(mt['last-backup-completed-time'])) + '');
+ else
+ metadataItems.push('Last backup: ' + this.timeAgo(new Date(mt['last-backup-date'])) + '');
+ }
+
+ for(var ix = 0; ix < metadataItems.length; ix++)
+ {
+ /*if (ix != 0)
+ metadata += ", "; */
+
+ metadata += metadataItems[ix];
+ }
+ }
+
+ if (metadata == '')
+ metadata = 'No backup information was found, run a backup to obtain data'
+
+ return metadata;
+ }
+ }
+})
\ No newline at end of file
diff --git a/Duplicati/Server/webroot/backup-app/Service.js b/Duplicati/Server/webroot/backup-app/Service.js
new file mode 100644
index 000000000..20654d266
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/Service.js
@@ -0,0 +1,228 @@
+Ext.define('BackupApp.Service', {
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ controlUrl: '/control.cgi',
+ pauseCountDownTimer: null,
+ updateRequestParams: {
+ action: 'get-current-state',
+ longpoll: true,
+ lasteventid: '-1',
+ duration: '5m',
+ },
+ updateRequestRunning: false,
+
+ //Bugfix for extjs
+ hasListeners: {},
+
+ //Cache for server responses
+ serverdata: {
+ 'current-state': null,
+ 'installed-backends': null,
+ 'installed-compression-modules': null,
+ 'installed-encryption-modules': null,
+ 'installed-generic-modules': null
+ },
+
+ constructor: function(config) {
+ this.addEvents('current-state-updated', 'count-down-pause-timer');
+ },
+
+ updateCurrentState: function(force) {
+ if (this.updateRequestRunning && !force)
+ return;
+
+ var params = force ? { action: 'get-current-state' } : this.updateRequestParams;
+ var timeout = force ? 5000 : (1000 * ((60 * 5) + 5));
+
+ if (!force)
+ this.updateRequestRunning = true;
+
+ Ext.Ajax.request({
+ url: this.controlUrl,
+ params: params,
+ timeout: timeout,
+ scope: this,
+ success: function(response) {
+ var res = eval('(' + response.responseText + ')');
+ if (res != null)
+ {
+ Ext.log('Got update: ' + res.LastEventID + ', last: ' + this.updateRequestParams.lasteventid);
+ var realUpdate = this.updateRequestParams.lasteventid != res.LastEventID;
+ this.serverdata['current-state'] = res;
+ this.updateRequestParams.lasteventid = res.LastEventID;
+
+ //Register next request
+ if (!force)
+ {
+ this.updateRequestRunning = false;
+ this.updateCurrentState();
+ }
+
+ if (realUpdate)
+ this.fireEvent('current-state-updated', this.serverdata['current-state']);
+ if (this.getExpectedPauseEnd() != null)
+ this.startPauseCountDownTimer();
+ }
+ },
+ failure: function(response, opts) {
+ //TODO: Grey out or similar
+
+
+ //Register next request
+ if (!force)
+ {
+ Ext.log('Failure in longpoll: ' + response.status + ', timeout: ' + response.timedout);
+ this.updateRequestRunning = false;
+ this.updateCurrentState();
+ }
+ }
+ });
+ },
+
+ startPauseCountDownTimer: function() {
+ if (this.pauseCountDownTimer == null)
+ this.pauseCountDownTimer = window.setTimeout('BackupApp.service.onPauseCountDownTimer()', 500);
+ },
+
+ onPauseCountDownTimer: function() {
+ this.pauseCountDownTimer = null;
+ var pauseEnd = this.getExpectedPauseEnd();
+ if (pauseEnd == null) {
+ this.fireEvent('count-down-pause-timer', 0);
+ } else {
+ this.fireEvent('count-down-pause-timer', parseInt((pauseEnd.getTime() - new Date().getTime()) / 1000));
+ this.startPauseCountDownTimer();
+ }
+ },
+
+ getExpectedPauseEnd: function() {
+ var pauseEnd = this.serverdata['current-state'].EstimatedPauseEnd;
+ if (pauseEnd != null) {
+ pauseEnd = BackupApp.Utility.parseJsonDate(pauseEnd);
+ if (pauseEnd != null && pauseEnd.getTime() > new Date().getTime())
+ return pauseEnd;
+ }
+ return null;
+ },
+
+ getSchedulePosition: function(id) {
+ var state = this.serverdata['current-state'];
+ if (state == null)
+ return -1;
+
+ if (id == state.ActiveScheduleId)
+ return 0;
+ else if (state.SchedulerQueueIds != null)
+ for(var i = 0; i < state.SchedulerQueueIds.length; i++)
+ if (state.SchedulerQueueIds[i] == id)
+ return i+1;
+
+ return -1;
+ },
+
+ pause: function(duration) {
+ var self = this;
+ Ext.Ajax.request({
+ url: this.controlUrl,
+ params: {
+ action: 'send-command',
+ command: 'pause',
+ duration: duration
+ },
+ success: function(response) { }
+ });
+ },
+
+ resume: function() {
+ var self = this;
+ Ext.Ajax.request({
+ url: this.controlUrl,
+ params: {
+ action: 'send-command',
+ command: 'resume'
+ },
+ success: function(response) { }
+ });
+ },
+
+ isPaused: function() {
+ var state = this.serverdata['current-state'];
+ if (state != null)
+ return state.ProgramState != 'Running';
+
+ return true;
+ },
+
+ togglePause: function() {
+ if (this.isPaused())
+ this.resume();
+ else
+ this.pause();
+ },
+
+ getInstalledCompressionModules: function(callback) {
+ if (this.serverdata['installed-compression-modules'] == null) {
+ var self = this;
+ Ext.Ajax.request({
+ url: this.controlUrl,
+ params: { action: 'list-installed-compression-modules' },
+ success: function(response) {
+ self.serverdata['installed-compression-modules'] = eval('(' + response.responseText + ')');
+ callback(self.serverdata['installed-compression-modules']);
+ }
+ });
+ } else {
+ callback(this.serverdata['installed-compression-modules']);
+ }
+ },
+
+ getInstalledGenericModules: function(callback) {
+ if (this.serverdata['installed-generic-modules'] == null) {
+ Ext.Ajax.request({
+ url: this.controlUrl,
+ params: { action: 'list-installed-generic-modules' },
+ success: function(response) {
+ self.serverdata['installed-generic-modules'] = eval('(' + response.responseText + ')');
+ callback(self.serverdata['installed-generic-modules']);
+ }
+ });
+ } else {
+ callback(this.serverdata['installed-generic-modules']);
+ }
+ },
+
+ getInstalledEncryptionModules: function(callback) {
+ if (this.serverdata['installed-encryption-modules'] == null) {
+ Ext.Ajax.request({
+ url: this.controlUrl,
+ params: { action: 'list-installed-encryption-modules' },
+ success: function(response) {
+ self.serverdata['installed-encryption-modules'] = eval('(' + response.responseText + ')');
+ callback(self.serverdata['installed-encryption-modules']);
+ }
+ });
+ } else {
+ callback(this.serverdata['installed-encryption-modules']);
+ }
+ },
+
+ runBackup: function(id, full) {
+ var self = this;
+
+ Ext.Ajax.request({
+ url: this.controlUrl,
+ params: {
+ action: 'send-command',
+ command: 'run-backup',
+ id: id,
+ full: full
+ },
+ success: function(response) {
+ }
+ });
+
+ }
+
+});
\ No newline at end of file
diff --git a/Duplicati/Server/webroot/backup-app/Utility.js b/Duplicati/Server/webroot/backup-app/Utility.js
new file mode 100644
index 000000000..c9bc01479
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/Utility.js
@@ -0,0 +1,147 @@
+Ext.define('BackupApp.Utility', {
+ statics: {
+
+ strings: {
+ terabyteShort: 'TB',
+ gigabyteShort: 'GB',
+ megabyteShort: 'MB',
+ kilobyteShort: 'KB',
+ bytesShort: 'bytes',
+
+ prefixAgo: null,
+ prefixFromNow: "in",
+ suffixAgo: "ago",
+ suffixFromNow: null,
+ seconds: "less than a minute",
+ minute: "about a minute",
+ minutes: "%d minutes",
+ hour: "about an hour",
+ hours: "about %d hours",
+ day: "a day",
+ days: "%d days",
+ month: "about a month",
+ months: "%d months",
+ year: "about a year",
+ years: "%d years",
+ wordSeparator: " "
+ },
+
+ timeAgoTimer: window.setInterval('BackupApp.Utility.updateTimeAgoFields()', 1000 * 150),
+
+ //Simplified version of jquery.timeago
+ timeAgo: function(date) {
+ var distanceMillis = (new Date().getTime() - date.getTime());
+ var strings = this.strings;
+
+ var prefix = strings.prefixAgo;
+ var suffix = strings.suffixAgo;
+ if (distanceMillis < 0) {
+ prefix = strings.prefixFromNow;
+ suffix = strings.suffixFromNow;
+ }
+
+ var seconds = Math.abs(distanceMillis) / 1000;
+ var minutes = seconds / 60;
+ var hours = minutes / 60;
+ var days = hours / 24;
+ var years = days / 365;
+
+ var words = seconds < 45 && strings.seconds.replace(/%d/i, Math.round(seconds)) ||
+ seconds < 90 && strings.minute ||
+ minutes < 45 && strings.minutes.replace(/%d/i, Math.round(minutes)) ||
+ minutes < 90 && strings.hour ||
+ hours < 24 && strings.hours.replace(/%d/i, Math.round(hours)) ||
+ hours < 42 && strings.day ||
+ days < 30 && strings.days.replace(/%d/i, Math.round(days)) ||
+ days < 45 && strings.month ||
+ days < 365 && strings.months.replace(/%d/i, Math.round(days / 30)) ||
+ years < 1.5 && strings.year ||
+ strings.years.replace(/%d/i, Math.round(years));
+
+ var separator = strings.wordSeparator === undefined ? " " : strings.wordSeparator;
+ return Ext.String.trim([prefix, words, suffix].join(separator));
+ },
+
+ updateTimeAgoFields: function() {
+ var parent = Ext.get(document);
+ var els = parent.query('.marker-time-ago');
+ for(var i = 0; i < els.length; i++) {
+ var time = this.parseJsonDate(els[i].title);
+ if (els[i].nextSibling.nodeType == 3)
+ els[i].nextSibling.nodeValue = this.timeAgo(time);
+ else if (els[i].nextSibling.innerText != null)
+ els[i].nextSibling.innerText = this.timeAgo(time);
+ }
+ },
+
+
+ parseJsonDate: function(str) {
+ if (str == null)
+ return null;
+
+ if (Ext.String.trim(str).length == 0)
+ return null;
+
+ if (str.indexOf("\/") == 0)
+ {
+ var strtmp = str.replace(/\/Date\((-?\d+(((\+|\-)\d+)|Z|z)?)\)\//gi, "new Date($1)");
+ return eval(strtmp);
+ }
+ else
+ {
+ return new Date(str);
+ }
+ },
+
+ formatSecondsAsTime: function(seconds) {
+ seconds = parseInt(seconds);
+ var hours = parseInt(Math.floor(seconds / (60 * 60)));
+ seconds = seconds - (hours * 60 * 60);
+ var minutes = parseInt(Math.floor(seconds / 60));
+ seconds = seconds - (minutes * 60);
+ var res = '';
+ if (hours > 0) {
+ res += hours + ':';
+ if (minutes < 10)
+ res += '0';
+ }
+
+ res += minutes + ':' ;
+ if (seconds < 10)
+ res += '0';
+ res += seconds;
+
+ return res;
+ },
+
+ formatSizeString: function(size) {
+ size = parseInt(size);
+ if (isNaN(size))
+ return size;
+
+ var strings = this.strings;
+
+ if (size >= 1024 * 1024 * 1024 * 1024)
+ return (size / (1024 * 1024 * 1024 * 1024.0)).toFixed(2) + ' ' + strings.terabyteShort;
+ else if (size >= 1024 * 1024 * 1024)
+ return (size / (1024 * 1024 * 1024.0)).toFixed(2) + ' ' + strings.gigabyteShort;
+ else if (size >= 1024 * 1024)
+ return (size / (1024 * 1024.0)).toFixed(2) + ' ' + strings.megabyteShort;
+ else if (size >= 1024)
+ return (size / (1024.0)).toFixed(2) + ' ' + strings.kilobyteShort;
+ else
+ return size + ' ' + strings.bytesShort;
+ },
+
+ estimatedFreeSpace: function(assignedQuota, freeQuota, backupSize) {
+ if (assignedQuota != null && freeQuota != null && backupSize != null)
+ return Math.min(parseInt(assignedQuota) - parseInt(backupSize), parseInt(freeQuota));
+ else if (assignedQuota != null && backupSize != null)
+ return parseInt(assignedQuota) - parseInt(backupSize);
+ else if (freeQuota != null)
+ return parseInt(freeQuota);
+ else
+ return null;
+ }
+ }
+});
\ No newline at end of file
diff --git a/Duplicati/Server/webroot/backup-app/controller/Schedules.js b/Duplicati/Server/webroot/backup-app/controller/Schedules.js
new file mode 100644
index 000000000..853a4172d
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/controller/Schedules.js
@@ -0,0 +1,36 @@
+Ext.define('BackupApp.controller.Schedules', {
+ extend: 'Ext.app.Controller',
+ stores: [ 'Schedules' ],
+ views: [ 'schedule.List' ],
+
+ refs: [{
+ selector: 'viewport > panel',
+ ref: 'basicPanel'
+ }],
+
+ lastEventID: -1,
+
+ init: function() {
+ this.control({
+ 'viewport > panel': {
+ render: this.onPanelRendered
+ }
+ });
+
+ this.application.on({
+ 'current-state-updated': function(data) {
+
+ //Update the data store if there is new metadata
+ if (data.LastEventID != this.lastEventID)
+ this.getSchedulesStore().load();
+
+ this.lastEventID = data.LastEventID;
+ },
+ scope: this
+ });
+ },
+
+ onPanelRendered: function() {
+ //console.log('The panel was rendered');
+ }
+});
\ No newline at end of file
diff --git a/Duplicati/Server/webroot/backup-app/controller/StatuswindowFooter.js b/Duplicati/Server/webroot/backup-app/controller/StatuswindowFooter.js
new file mode 100644
index 000000000..84e580a0f
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/controller/StatuswindowFooter.js
@@ -0,0 +1,16 @@
+Ext.define('BackupApp.controller.StatuswindowFooter', {
+ extend: 'Ext.app.Controller',
+ views: [ 'StatuswindowFooter' ],
+
+ init: function() {
+ this.control({
+ 'viewport > panel': {
+ render: this.onPanelRendered
+ }
+ });
+ },
+
+ onPanelRendered: function() {
+ //console.log('The footer panel was rendered');
+ }
+});
\ No newline at end of file
diff --git a/Duplicati/Server/webroot/backup-app/controller/StatuswindowHeader.js b/Duplicati/Server/webroot/backup-app/controller/StatuswindowHeader.js
new file mode 100644
index 000000000..048d10d1a
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/controller/StatuswindowHeader.js
@@ -0,0 +1,60 @@
+Ext.define('BackupApp.controller.StatuswindowHeader', {
+ extend: 'Ext.app.Controller',
+ views: [ 'StatuswindowHeader' ],
+
+ refs: [{
+ selector: '#status-window-header-pause-button',
+ ref: 'pauseButton'
+ }, {
+ selector: '#status-window-header-pause-menu',
+ ref: 'pauseMenu',
+ }],
+
+ init: function() {
+ this.application.on({
+ 'current-state-updated': function() {
+ var btn = this.getPauseButton();
+ var mnu = this.getPauseMenu();
+ var srv = this.application.service;
+ var util = BackupApp.Utility;
+
+ if (srv.isPaused()) {
+ if (srv.getExpectedPauseEnd() != null)
+ btn.setText('Resume (' + util.formatSecondsAsTime((srv.getExpectedPauseEnd().getTime() - new Date().getTime()) / 1000) + ')');
+ else
+ btn.setText('Resume');
+ btn.setIconCls('backups-paused');
+ mnu.setText('Resume Duplicati');
+ } else {
+ btn.setText('Pause');
+ btn.setIconCls('backups-running');
+ mnu.setText('Pause Duplicati');
+ }
+ },
+ scope: this
+ });
+
+ this.application.on({
+ 'count-down-pause-timer': function(data) {
+ var btn = this.getPauseButton();
+ var srv = this.application.service;
+ var util = BackupApp.Utility;
+ if (data <= 0)
+ btn.setText('Pause');
+ else
+ btn.setText('Resume (' + util.formatSecondsAsTime(data) + ')');
+ },
+ scope: this
+ });
+
+ this.control({
+ 'viewport > panel': {
+ render: this.onPanelRendered
+ }
+ });
+ },
+
+ onPanelRendered: function() {
+ //console.log('The header panel was rendered');
+ }
+});
\ No newline at end of file
diff --git a/Duplicati/Server/webroot/backup-app/model/Schedule.js b/Duplicati/Server/webroot/backup-app/model/Schedule.js
new file mode 100644
index 000000000..19e63bebf
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/model/Schedule.js
@@ -0,0 +1,4 @@
+Ext.define('BackupApp.model.Schedule', {
+ extend: 'Ext.data.Model',
+ fields: ['ID', 'Name', 'Path', 'MetadataLookup', 'NextScheduledTime', 'When', 'ExistsInDb', 'AllowedWeekdays', 'Repeat', 'Weekdays']
+});
\ No newline at end of file
diff --git a/Duplicati/Server/webroot/backup-app/store/Schedules.js b/Duplicati/Server/webroot/backup-app/store/Schedules.js
new file mode 100644
index 000000000..307f1a3cb
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/store/Schedules.js
@@ -0,0 +1,95 @@
+Ext.define('BackupApp.store.Schedules', {
+ extend: 'Ext.data.Store',
+ model: 'BackupApp.model.Schedule',
+ autoLoad: true,
+
+ /* Sort order,
+ * first by queue position
+ * second by next schedule time,
+ * third by last backup time
+ * */
+ sorters: [{
+ sorterFn: function(a, b) {
+ var schedulePosA = BackupApp.service.getSchedulePosition(a.get("ID"));
+ var schedulePosB = BackupApp.service.getSchedulePosition(b.get("ID"));
+
+ if(schedulePosA >= 0 && schedulePosB >= 0)
+ {
+ if (schedulePosA == schedulePosB)
+ return 0;
+ else
+ return schedulePosA < schedulePosB ? -1 : 1;
+ }
+ else if (schedulePosA >= 0)
+ return -1;
+ else if (schedulePosB >= 0)
+ return 1;
+ else
+ return 0;
+ }
+ }, {
+ sorterFn: function(a, b) {
+ var nextTimeA = BackupApp.Utility.parseJsonDate(a.get('NextScheduledTime'));
+ var nextTimeB = BackupApp.Utility.parseJsonDate(b.get('NextScheduledTime'));
+ var now = new Date().getTime();
+
+ //Scheduled time in past, but not in queue, so it will not run
+ if (nextTimeA != null && nextTimeA < now)
+ nextTimeA = null;
+ if (nextTimeB != null && nextTimeB < now)
+ nextTimeB = null;
+
+ if (nextTimeA != null && nextTimeB != null)
+ {
+ nextTimeA = nextTimeA.getTime();
+ nextTimeB = nextTimeB.getTime();
+ if (nextTimeA == nextTimeB)
+ return 0;
+ else
+ return nextTimeA < nextTimeB ? -1 : 1;
+ }
+ else if (nextTimeA != null)
+ return -1;
+ else if (nextTimeB != null)
+ return 1;
+ else
+ return 0;
+ }
+ }, {
+ sorterFn: function(a, b) {
+ var lastTimeA = BackupApp.Utility.parseJsonDate(a.data.MetadataLookup['last-backup-completed-time']);
+ var lastTimeB = BackupApp.Utility.parseJsonDate(b.data.MetadataLookup['last-backup-completed-time']);
+ if (lastTimeA == null)
+ lastTimeA = BackupApp.Utility.parseJsonDate(a.data.MetadataLookup['last-backup-date']);
+ if (lastTimeB == null)
+ lastTimeB = BackupApp.Utility.parseJsonDate(b.data.MetadataLookup['last-backup-date']);
+
+ if ((lastTimeA != null && lastTimeB != null))
+ {
+ lastTimeA = lastTimeA.getTime();
+ lastTimeB = lastTimeB.getTime();
+
+ if (lastTimeA == lastTimeB)
+ return 0;
+ else
+ return lastTimeA > lastTimeB ? -1 : 1;
+ }
+ else if (lastTimeA != null)
+ return -1;
+ else if (lastTimeB != null)
+ return 1;
+ else
+ return 0;
+ }
+ }, {
+ property: 'Name',
+ }],
+
+ proxy: {
+ type: 'ajax',
+ url: 'control.cgi?action=list-schedules',
+ reader: {
+ type: 'json'
+ }
+ }
+});
\ No newline at end of file
diff --git a/Duplicati/Server/webroot/backup-app/view/AboutWindow.js b/Duplicati/Server/webroot/backup-app/view/AboutWindow.js
new file mode 100644
index 000000000..ac9d12383
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/view/AboutWindow.js
@@ -0,0 +1,32 @@
+Ext.define('BackupApp.view.AboutWindow', {
+ statics: {
+ dialogWindow: null,
+ show: function() {
+ if (this.dialogWindow == null)
+ this.dialogWindow = Ext.create('Ext.window.Window', {
+ title: 'About Duplicati',
+ height: 200,
+ width: 400,
+ modal: true,
+ resizable: false,
+ buttonAlign: 'center',
+ closeAction: 'hide',
+ layout: 'auto',
+ items: [{
+ xtype: 'label',
+ text: 'Duplicati 2.0'
+ }],
+ buttons: [{
+ xtype: 'button',
+ text: 'OK',
+ handler: function() {
+ BackupApp.view.AboutWindow.dialogWindow.hide();
+ }
+ }]
+ });
+
+ this.dialogWindow.show();
+ }
+ }
+});
+
diff --git a/Duplicati/Server/webroot/backup-app/view/StatuswindowFooter.js b/Duplicati/Server/webroot/backup-app/view/StatuswindowFooter.js
new file mode 100644
index 000000000..4e5f58454
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/view/StatuswindowFooter.js
@@ -0,0 +1,37 @@
+Ext.define('BackupApp.view.StatuswindowFooter' ,{
+ extend: 'Ext.panel.Panel',
+ alias : 'widget.statuswindowfooter',
+
+ title : 'Status line',
+ header: false,
+ border: 0,
+ layout: 'column',
+ cls: 'footer-panel',
+
+ items: [{
+ xtype: 'label',
+ html: 'A new version is available',
+ handler: function() { window.open('https://www.facebook.com/pages/Duplicati/105118456272281') },
+ columnWidth: 1,
+ margin: 10
+ },{
+ xtype: 'button',
+ text: 'Facebook',
+ handler: function() { window.open('https://www.facebook.com/pages/Duplicati/105118456272281') },
+ margin: 10
+ },{
+ xtype: 'button',
+ text: 'Google+',
+ handler: function() { window.open('https://plus.google.com/105271984558189185842/posts') },
+ margin: 10
+ },{
+ xtype: 'button',
+ text: 'Donate!',
+ handler: function() { window.open('https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=paypal%40hexad%2edk&item_name=Duplicati%20Donation&no_shipping=2&no_note=1&tax=0¤cy_code=EUR&bn=PP%2dDonationsBF&charset=UTF%2d8') },
+ margin: 10
+ }],
+
+ initComponent: function() {
+ this.callParent(arguments);
+ }
+});
\ No newline at end of file
diff --git a/Duplicati/Server/webroot/backup-app/view/StatuswindowHeader.js b/Duplicati/Server/webroot/backup-app/view/StatuswindowHeader.js
new file mode 100644
index 000000000..43c500982
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/view/StatuswindowHeader.js
@@ -0,0 +1,101 @@
+Ext.define('BackupApp.view.StatuswindowHeader' ,{
+ extend: 'Ext.panel.Panel',
+ alias : 'widget.statuswindowheader',
+
+ title : 'Main App controls',
+ header: false,
+ border: 0,
+ layout: 'column',
+ cls: 'header-panel',
+
+ items: [{
+ xtype: 'splitbutton',
+ cls: 'main-button',
+ text: 'Duplicati',
+ id: 'status-window-header-main-button',
+ margin: 10,
+
+ handler: function() { this.showMenu(); },
+ menu: [{
+ id: 'status-window-header-pause-menu',
+ text: 'Pause Duplicati',
+ handler: function() { BackupApp.service.togglePause(); }
+ },{
+ text: 'Pause for x minutes',
+ menu: [{
+ text: 'Pause for 5 minutes',
+ handler: function() { BackupApp.service.pause('5m'); }
+ },{
+ text: 'Pause for 15 minutes',
+ handler: function() { BackupApp.service.pause('15m'); }
+ },{
+ text: 'Pause for 30 minutes',
+ handler: function() { BackupApp.service.pause('30m'); }
+ },{
+ text: 'Pause for 60 minutes',
+ handler: function() { BackupApp.service.pause('60m'); }
+ }]
+ },{
+ text: '*** Throttle ...'
+ },{
+ xtype: 'menuseparator'
+ },{
+ text: '*** Run backups',
+ menu: [{
+ text: 'All backups'
+ },{
+ text: 'Label: Work'
+ },{
+ text: 'Label: Home'
+ },{
+ text: 'Label: Amazon S3'
+ }]
+ },{
+ xtype: 'menuseparator'
+ },{
+ text: '*** Settings'
+ },{
+ text: 'About',
+ handler: function() { BackupApp.view.AboutWindow.show(); }
+ }]
+ },{
+ xtype: 'label',
+ html: ' ',
+ columnWidth: 1
+ },{
+ xtype: 'panel',
+ layout: 'vbox',
+ align: 'center',
+ margin: 5,
+ border: 0,
+ items: [{
+ xtype: 'label',
+ cls: 'throttle-label',
+ text: '*** Throttle upload'
+ },{
+ xtype: 'slider',
+ width: 200,
+ value: 80,
+ minValue: 0,
+ maxValue: 100
+ }]
+ },{
+ xtype: 'button',
+ cls: 'add-new-backup-button',
+ text: '*** Add new backup',
+ margin: 10
+ },{
+ xtype: 'button',
+ id: 'status-window-header-pause-button',
+ cls: 'pause-backup-button',
+ iconCls: 'backups-paused',
+ text: 'Pause',
+ margin: 10,
+ handler: function() { BackupApp.service.togglePause(); }
+ }],
+
+ initComponent: function() {
+
+ this.callParent(arguments);
+ }
+});
\ No newline at end of file
diff --git a/Duplicati/Server/webroot/backup-app/view/schedule/List.js b/Duplicati/Server/webroot/backup-app/view/schedule/List.js
new file mode 100644
index 000000000..c7983b8d9
--- /dev/null
+++ b/Duplicati/Server/webroot/backup-app/view/schedule/List.js
@@ -0,0 +1,95 @@
+Ext.define('BackupApp.view.schedule.List' ,{
+ extend: 'Ext.grid.Panel',
+ alias : 'widget.schedulelist',
+
+ title : 'All Schedules',
+ header: false,
+ border: 0,
+ id: 'status-window-schedule-list',
+
+ hideHeaders: true,
+ selType: 'rowmodel',
+
+ store: 'Schedules',
+
+ elementTemplateStr:
+ '{Name}' +
+ '' +
+ '' +
+ /* The template for this became un-readable, so now it is a function */
+ '{[BackupApp.ListUtility.formatBackupScheduleDetails(values)]}' +
+ '' +
+ '