diff --git a/Duplicati/Server/Database/Connection.cs b/Duplicati/Server/Database/Connection.cs index 7b4b81437..34b73b891 100644 --- a/Duplicati/Server/Database/Connection.cs +++ b/Duplicati/Server/Database/Connection.cs @@ -835,7 +835,8 @@ namespace Duplicati.Server.Database ), @"SELECT ""Key"", ""Value"" FROM ""UIStorage"" WHERE ""Scheme"" = ?", scheme) - .ToDictionary(x => x.Key, x => x.Value); + .GroupBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.Last().Value); } public void SetUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null) @@ -856,7 +857,28 @@ namespace Duplicati.Server.Database if (tr != null) tr.Commit(); } - } + } + + public void UpdateUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null) + { + lock (m_lock) + using (var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ? AND ""Key"" IN (?)", new object[] { scheme, values.Keys }, + values.Where(x => x.Value != null), + @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)", + (f) => + { + return new object[] { scheme, f.Key ?? "", f.Value ?? "" }; + } + ); + + if (tr != null) + tr.Commit(); + } + } public TempFile[] GetTempFiles() { diff --git a/Duplicati/Server/WebServer/RESTMethods/UISettings.cs b/Duplicati/Server/WebServer/RESTMethods/UISettings.cs index 0cadb8451..ff8e300f5 100644 --- a/Duplicati/Server/WebServer/RESTMethods/UISettings.cs +++ b/Duplicati/Server/WebServer/RESTMethods/UISettings.cs @@ -18,8 +18,45 @@ using System; using System.Collections.Generic; using Duplicati.Server.Serializa namespace Duplicati.Server.WebServer.RESTMethods { - public class UISettings : IRESTMethodGET, IRESTMethodPOST - { public void GET(string key, RequestInfo info) { if (string.IsNullOrWhiteSpace(key)) { info.OutputOK(Program.DataConnection.GetUISettingsSchemes()); } else { info.OutputOK(Program.DataConnection.GetUISettings(key)); } } public void POST(string key, RequestInfo info) { if (string.IsNullOrWhiteSpace(key)) { info.ReportClientError("Scheme is missing"); return; } IDictionary data; try { data = Serializer.Deserialize>(new StreamReader(info.Request.Body)); } catch (Exception ex) { info.ReportClientError(string.Format("Unable to parse settings object: {0}", ex.Message)); return; } if (data == null) { info.ReportClientError(string.Format("Unable to parse settings object")); return; } Program.DataConnection.SetUISettings(key, data); info.OutputOK(); } - } + public class UISettings : IRESTMethodGET, IRESTMethodPOST, IRESTMethodPATCH + { public void GET(string key, RequestInfo info) { if (string.IsNullOrWhiteSpace(key)) { info.OutputOK(Program.DataConnection.GetUISettingsSchemes()); } else { info.OutputOK(Program.DataConnection.GetUISettings(key)); } } + + public void POST(string key, RequestInfo info) + { + PATCH(key, info); + } + + public void PATCH(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + info.ReportClientError("Scheme is missing"); + return; + } + + IDictionary data; + try + { + data = Serializer.Deserialize>(new StreamReader(info.Request.Body)); + } + catch (Exception ex) + { + info.ReportClientError(string.Format("Unable to parse settings object: {0}", ex.Message)); + return; + } + + if (data == null) + { + info.ReportClientError(string.Format("Unable to parse settings object")); + return; + } + + if (info.Request.Method == "POST") + Program.DataConnection.SetUISettings(key, data); + else + Program.DataConnection.UpdateUISettings(key, data); + info.OutputOK(); + } + } } diff --git a/Duplicati/Server/webroot/ngax/index.html b/Duplicati/Server/webroot/ngax/index.html index 18e6bc2b3..380f525d8 100755 --- a/Duplicati/Server/webroot/ngax/index.html +++ b/Duplicati/Server/webroot/ngax/index.html @@ -27,6 +27,7 @@ + @@ -119,7 +120,7 @@ - +
diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/AppController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/AppController.js index d331f1386..f1308aca4 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/AppController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/AppController.js @@ -5,6 +5,12 @@ backupApp.controller('AppController', function($scope, $cookies, $location, AppS $scope.localized = {}; $scope.location = $location; + $scope.saved_theme = $scope.active_theme = $cookies.get('current-theme') || 'default'; + + // If we want the theme settings + // to be persisted on the server, + // set to "true" here + var save_theme_on_server = false; $scope.doReconnect = function() { ServerStatus.reconnect(); @@ -50,6 +56,9 @@ backupApp.controller('AppController', function($scope, $cookies, $location, AppS }; function updateCurrentPage() { + + $scope.active_theme = $scope.saved_theme; + if ($location.$$path == '/' || $location.$$path == '') $scope.current_page = 'home'; else if ($location.$$path == '/addstart' || $location.$$path == '/add' || $location.$$path == '/import') @@ -78,4 +87,58 @@ backupApp.controller('AppController', function($scope, $cookies, $location, AppS $scope.$watch('location.$$path', updateCurrentPage); updateCurrentPage(); + function loadCurrentTheme() { + if (save_theme_on_server) { + AppService.get('/uisettings/ngax').then( + function(data) { + var theme = 'default'; + if (data.data != null && (data.data['theme'] || '').trim().length > 0) + theme = data.data['theme']; + + var now = new Date(); + var exp = new Date(now.getFullYear()+10, now.getMonth(), now.getDate()); + $cookies.put('current-theme', theme, { expires: exp }); + $scope.saved_theme = $scope.active_theme = theme; + }, function() {} + ); + } + }; + + // In case the cookie is out-of-sync + loadCurrentTheme(); + + $scope.$on('update_theme', function(event, args) { + var theme = 'default'; + if (args != null && (args.theme || '').trim().length != 0) + theme = args.theme; + + if (save_theme_on_server) { + // Set it here to avoid flickering when the page changes + $scope.saved_theme = $scope.active_theme = theme; + + AppService.patch('/uisettings/ngax', { 'theme': theme }, {'headers': {'Content-Type': 'application/json'}}).then( + function(data) { + var now = new Date(); + var exp = new Date(now.getFullYear()+10, now.getMonth(), now.getDate()); + $cookies.put('current-theme', theme, { expires: exp }); + $scope.saved_theme = $scope.active_theme = theme; + }, function() {} + ); + } else { + var now = new Date(); + var exp = new Date(now.getFullYear()+10, now.getMonth(), now.getDate()); + $cookies.put('current-theme', theme, { expires: exp }); + $scope.saved_theme = $scope.active_theme = theme; + } + + loadCurrentTheme(); + }); + + $scope.$on('preview_theme', function(event, args) { + if (args == null || (args.theme + '').trim().length == 0) + $scope.active_theme = $scope.saved_theme; + else + $scope.active_theme = args.theme || ''; + + }); }); diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/SystemSettingsController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/SystemSettingsController.js index 24694b9a1..7ec47b24d 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/SystemSettingsController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/SystemSettingsController.js @@ -1,6 +1,11 @@ backupApp.controller('SystemSettingsController', function($rootScope, $scope, $location, $cookies, AppService, AppUtils, SystemInfo, gettextCatalog) { - $scope.SystemInfo = SystemInfo.watch($scope); + $scope.SystemInfo = SystemInfo.watch($scope); + $scope.theme = $scope.$parent.$parent.saved_theme; + if (($scope.theme || '').trim().length == 0) + $scope.theme = 'default'; + + $scope.usageReporterLevel = ''; function reloadOptionsList() { $scope.advancedOptionList = AppUtils.buildOptionList($scope.SystemInfo, false, false, false); @@ -15,12 +20,15 @@ backupApp.controller('SystemSettingsController', function($rootScope, $scope, $l $scope.ServerModules = mods; AppUtils.extractServerModuleOptions($scope.advancedOptions, $scope.ServerModules, $scope.servermodulesettings, 'SupportedGlobalCommands'); - } + }; reloadOptionsList(); - $scope.$on('systeminfochanged', reloadOptionsList); + $scope.$watch('theme', function() { + $rootScope.$broadcast('preview_theme', { theme: $scope.theme }); + }); + $scope.uiLanguage = $cookies.get('ui-locale'); $scope.lang_browser_default = gettextCatalog.getString('Browser default'); $scope.lang_default = gettextCatalog.getString('Default'); @@ -37,7 +45,7 @@ backupApp.controller('SystemSettingsController', function($rootScope, $scope, $l gettextCatalog.setCurrentLanguage($scope.uiLanguage.replace("-", "_")); } $rootScope.$broadcast('ui_language_changed'); - } + }; AppService.get('/serversettings').then(function(data) { @@ -62,7 +70,7 @@ backupApp.controller('SystemSettingsController', function($rootScope, $scope, $l $scope.save = function() { if ($scope.requireRemotePassword && $scope.remotePassword.trim().length == 0) - return AppUtil.notifyInputError('Cannot use empty password'); + return AppUtils.notifyInputError('Cannot use empty password'); var patchdata = { 'server-passphrase': $scope.requireRemotePassword ? $scope.remotePassword : '', @@ -73,7 +81,6 @@ backupApp.controller('SystemSettingsController', function($rootScope, $scope, $l 'usage-reporter-level': $scope.usageReporterLevel }; - if ($scope.requireRemotePassword) { if ($scope.rawdata['server-passphrase'] != $scope.remotePassword) { patchdata['server-passphrase-salt'] = CryptoJS.lib.WordArray.random(256/8).toString(CryptoJS.enc.Base64); @@ -88,6 +95,8 @@ backupApp.controller('SystemSettingsController', function($rootScope, $scope, $l for(var n in $scope.servermodulesettings) patchdata['--' + n] = $scope.servermodulesettings[n]; + $rootScope.$broadcast('update_theme', { theme: $scope.theme } ); + AppService.patch('/serversettings', patchdata, {headers: {'Content-Type': 'application/json; charset=utf-8'}}).then( function() { setUILanguage(); diff --git a/Duplicati/Server/webroot/ngax/styles/themes.css b/Duplicati/Server/webroot/ngax/styles/themes.css new file mode 100644 index 000000000..ae3ce5c60 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/styles/themes.css @@ -0,0 +1,59 @@ +body.theme-dark +{ + background-color: #1a1a1a !important; +} + +body.theme-dark .footer +{ + background-color: #333333 !important; +} + +body.theme-dark .header +{ + background-color: #333333 !important; +} + +body.theme-dark .state +{ + background-color: #1a1a1a !important; +} + +body.theme-dark form.styled .buttons input, body.theme-dark form.styled .buttons a +{ + background: #4a5879; +} + +body.theme-dark form.styled .buttons input:hover, body.theme-dark form.styled .buttons a:hover +{ + background: #6089b5; +} + +body.theme-dark .button +{ + background: #4a5879; +} + +body.theme-dark .button:hover +{ + background: #6089b5; +} + +body.theme-dark .container .body .mainmenu>ul>li>a.active +{ + color: black; +} + +body.theme-dark .container .body .content div.add .steps .step, body.theme-dark .container .body .content div.restore .steps .step +{ + color: #2780b3; +} + +body.theme-dark .step3 source-folder-picker, body.theme-dark #folder_path_picker, body.theme-dark #restore_file_picker +{ + background-color: #ffffff; +} + +body.theme-dark form.styled input, body.theme-dark form.styled textarea, body.theme-dark form.styled select +{ + color: #000000; +} \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/templates/settings.html b/Duplicati/Server/webroot/ngax/templates/settings.html index fde3db23d..3b9856966 100644 --- a/Duplicati/Server/webroot/ngax/templates/settings.html +++ b/Duplicati/Server/webroot/ngax/templates/settings.html @@ -34,7 +34,7 @@
-

User interface language

+

User interface settings

@@ -45,6 +45,14 @@
+
+ + + +

Donation messages