syncthingController.js 98 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503
  1. angular.module('syncthing.core')
  2. .config(function ($locationProvider) {
  3. $locationProvider.html5Mode({ enabled: true, requireBase: false }).hashPrefix('!');
  4. })
  5. .controller('SyncthingController', function ($scope, $http, $location, LocaleService, Events, $filter, $q, $compile, $timeout, $rootScope, $translate) {
  6. 'use strict';
  7. // private/helper definitions
  8. var prevDate = 0;
  9. var navigatingAway = false;
  10. var online = false;
  11. var restarting = false;
  12. function initController() {
  13. LocaleService.autoConfigLocale();
  14. setInterval($scope.refresh, 10000);
  15. Events.start();
  16. }
  17. // public/scope definitions
  18. $scope.completion = {};
  19. $scope.config = {};
  20. $scope.configInSync = true;
  21. $scope.connections = {};
  22. $scope.errors = [];
  23. $scope.model = {};
  24. $scope.myID = '';
  25. $scope.devices = [];
  26. $scope.discoveryCache = {};
  27. $scope.protocolChanged = false;
  28. $scope.reportData = {};
  29. $scope.reportDataPreview = '';
  30. $scope.reportDataPreviewVersion = '';
  31. $scope.reportDataPreviewDiff = false;
  32. $scope.reportPreview = false;
  33. $scope.folders = {};
  34. $scope.seenError = '';
  35. $scope.upgradeInfo = null;
  36. $scope.deviceStats = {};
  37. $scope.folderStats = {};
  38. $scope.progress = {};
  39. $scope.version = {};
  40. $scope.needed = {}
  41. $scope.neededFolder = '';
  42. $scope.failed = {};
  43. $scope.localChanged = {};
  44. $scope.scanProgress = {};
  45. $scope.themes = [];
  46. $scope.globalChangeEvents = {};
  47. $scope.metricRates = false;
  48. $scope.folderPathErrors = {};
  49. $scope.currentFolder = {};
  50. resetRemoteNeed();
  51. try {
  52. $scope.metricRates = (window.localStorage["metricRates"] == "true");
  53. } catch (exception) { }
  54. $scope.folderDefaults = {
  55. sharedDevices: {},
  56. selectedDevices: {},
  57. unrelatedDevices: {},
  58. type: "sendreceive",
  59. rescanIntervalS: 3600,
  60. fsWatcherDelayS: 10,
  61. fsWatcherEnabled: true,
  62. minDiskFree: { value: 1, unit: "%" },
  63. maxConflicts: 10,
  64. fsync: true,
  65. order: "random",
  66. fileVersioningSelector: "none",
  67. trashcanClean: 0,
  68. simpleKeep: 5,
  69. staggeredMaxAge: 365,
  70. staggeredCleanInterval: 3600,
  71. staggeredVersionsPath: "",
  72. externalCommand: "",
  73. autoNormalize: true,
  74. path: "",
  75. };
  76. $scope.localStateTotal = {
  77. bytes: 0,
  78. directories: 0,
  79. files: 0
  80. };
  81. $(window).bind('beforeunload', function () {
  82. navigatingAway = true;
  83. });
  84. $scope.$on("$locationChangeSuccess", function () {
  85. LocaleService.useLocale($location.search().lang);
  86. });
  87. $scope.needActions = {
  88. 'rm': 'Del',
  89. 'rmdir': 'Del (dir)',
  90. 'sync': 'Sync',
  91. 'touch': 'Update'
  92. };
  93. $scope.needIcons = {
  94. 'rm': 'far fa-fw fa-trash-alt',
  95. 'rmdir': 'far fa-fw fa-trash-alt',
  96. 'sync': 'far fa-fw fa-arrow-alt-circle-down',
  97. 'touch': 'fas fa-fw fa-asterisk'
  98. };
  99. $scope.$on(Events.ONLINE, function () {
  100. if (online && !restarting) {
  101. return;
  102. }
  103. console.log('UIOnline');
  104. refreshSystem();
  105. refreshDiscoveryCache();
  106. refreshConfig();
  107. refreshConnectionStats();
  108. refreshDeviceStats();
  109. refreshFolderStats();
  110. refreshGlobalChanges();
  111. refreshThemes();
  112. $http.get(urlbase + '/system/version').success(function (data) {
  113. console.log("version", data);
  114. if ($scope.version.version && $scope.version.version !== data.version) {
  115. // We already have a version response, but it differs from
  116. // the new one. Reload the full GUI in case it's changed.
  117. document.location.reload(true);
  118. }
  119. $scope.version = data;
  120. }).error($scope.emitHTTPError);
  121. $http.get(urlbase + '/svc/report').success(function (data) {
  122. $scope.reportData = data;
  123. if ($scope.system && $scope.config.options.urAccepted > -1 && $scope.config.options.urSeen < $scope.system.urVersionMax && $scope.config.options.urAccepted < $scope.system.urVersionMax) {
  124. // Usage reporting format has changed, prompt the user to re-accept.
  125. $('#ur').modal();
  126. }
  127. }).error($scope.emitHTTPError);
  128. $http.get(urlbase + '/system/upgrade').success(function (data) {
  129. $scope.upgradeInfo = data;
  130. }).error(function () {
  131. $scope.upgradeInfo = null;
  132. });
  133. online = true;
  134. restarting = false;
  135. $('#networkError').modal('hide');
  136. $('#restarting').modal('hide');
  137. $('#shutdown').modal('hide');
  138. });
  139. $scope.$on(Events.OFFLINE, function () {
  140. if (navigatingAway || !online) {
  141. return;
  142. }
  143. console.log('UIOffline');
  144. online = false;
  145. if (!restarting) {
  146. $('#networkError').modal();
  147. }
  148. });
  149. $scope.$on('HTTPError', function (event, arg) {
  150. // Emitted when a HTTP call fails. We use the status code to try
  151. // to figure out what's wrong.
  152. if (navigatingAway || !online) {
  153. return;
  154. }
  155. console.log('HTTPError', arg);
  156. online = false;
  157. if (!restarting) {
  158. if (arg.status === 0) {
  159. // A network error, not an HTTP error
  160. $scope.$emit(Events.OFFLINE);
  161. } else if (arg.status >= 400 && arg.status <= 599) {
  162. // A genuine HTTP error
  163. $('#networkError').modal('hide');
  164. $('#restarting').modal('hide');
  165. $('#shutdown').modal('hide');
  166. $('#httpError').modal();
  167. }
  168. }
  169. });
  170. $scope.$on(Events.STATE_CHANGED, function (event, arg) {
  171. var data = arg.data;
  172. if ($scope.model[data.folder]) {
  173. $scope.model[data.folder].state = data.to;
  174. $scope.model[data.folder].error = data.error;
  175. // If a folder has started scanning, then any scan progress is
  176. // also obsolete.
  177. if (data.to === 'scanning') {
  178. delete $scope.scanProgress[data.folder];
  179. }
  180. // If a folder finished scanning, then refresh folder stats
  181. // to update last scan time.
  182. if (data.from === 'scanning' && data.to === 'idle') {
  183. refreshFolderStats();
  184. }
  185. }
  186. });
  187. $scope.$on(Events.LOCAL_INDEX_UPDATED, function (event, arg) {
  188. refreshFolderStats();
  189. refreshGlobalChanges();
  190. });
  191. $scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
  192. $scope.connections[arg.data.id].connected = false;
  193. refreshDeviceStats();
  194. });
  195. $scope.$on(Events.DEVICE_CONNECTED, function (event, arg) {
  196. if (!$scope.connections[arg.data.id]) {
  197. $scope.connections[arg.data.id] = {
  198. inbps: 0,
  199. outbps: 0,
  200. inBytesTotal: 0,
  201. outBytesTotal: 0,
  202. type: arg.data.type,
  203. address: arg.data.addr
  204. };
  205. $scope.completion[arg.data.id] = {
  206. _total: 100,
  207. _needBytes: 0,
  208. _needItems: 0
  209. };
  210. }
  211. });
  212. $scope.$on('ConfigLoaded', function () {
  213. if ($scope.config.options.urAccepted === 0) {
  214. // If usage reporting has been neither accepted nor declined,
  215. // we want to ask the user to make a choice. But we don't want
  216. // to bug them during initial setup, so we set a cookie with
  217. // the time of the first visit. When that cookie is present
  218. // and the time is more than four hours ago, we ask the
  219. // question.
  220. var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
  221. if (!firstVisit) {
  222. document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600;
  223. } else {
  224. if (+firstVisit < Date.now() - 4 * 3600 * 1000) {
  225. $('#ur').modal();
  226. }
  227. }
  228. }
  229. });
  230. $scope.$on(Events.CONFIG_SAVED, function (event, arg) {
  231. updateLocalConfig(arg.data);
  232. $http.get(urlbase + '/system/config/insync').success(function (data) {
  233. $scope.configInSync = data.configInSync;
  234. }).error($scope.emitHTTPError);
  235. });
  236. $scope.$on(Events.DOWNLOAD_PROGRESS, function (event, arg) {
  237. var stats = arg.data;
  238. var progress = {};
  239. for (var folder in stats) {
  240. progress[folder] = {};
  241. for (var file in stats[folder]) {
  242. var s = stats[folder][file];
  243. var reused = 100 * s.reused / s.total;
  244. var copiedFromOrigin = 100 * s.copiedFromOrigin / s.total;
  245. var copiedFromElsewhere = 100 * s.copiedFromElsewhere / s.total;
  246. var pulled = 100 * s.pulled / s.total;
  247. var pulling = 100 * s.pulling / s.total;
  248. // We try to round up pulling to at least a percent so that it would be at least a bit visible.
  249. if (pulling < 1 && pulled + copiedFromElsewhere + copiedFromOrigin + reused <= 99) {
  250. pulling = 1;
  251. }
  252. progress[folder][file] = {
  253. reused: reused,
  254. copiedFromOrigin: copiedFromOrigin,
  255. copiedFromElsewhere: copiedFromElsewhere,
  256. pulled: pulled,
  257. pulling: pulling,
  258. bytesTotal: s.bytesTotal,
  259. bytesDone: s.bytesDone,
  260. };
  261. }
  262. }
  263. for (var folder in $scope.progress) {
  264. if (!(folder in progress)) {
  265. if ($scope.neededFolder === folder) {
  266. $scope.refreshNeed($scope.needed.page, $scope.needed.perpage);
  267. }
  268. } else if ($scope.neededFolder === folder) {
  269. for (file in $scope.progress[folder]) {
  270. if (!(file in progress[folder])) {
  271. $scope.refreshNeed($scope.needed.page, $scope.needed.perpage);
  272. break;
  273. }
  274. }
  275. }
  276. }
  277. $scope.progress = progress;
  278. console.log("DownloadProgress", $scope.progress);
  279. });
  280. $scope.$on(Events.FOLDER_SUMMARY, function (event, arg) {
  281. var data = arg.data;
  282. $scope.model[data.folder] = data.summary;
  283. recalcLocalStateTotal();
  284. });
  285. $scope.$on(Events.FOLDER_COMPLETION, function (event, arg) {
  286. var data = arg.data;
  287. if (!$scope.completion[data.device]) {
  288. $scope.completion[data.device] = {};
  289. }
  290. $scope.completion[data.device][data.folder] = data;
  291. recalcCompletion(data.device);
  292. });
  293. $scope.$on(Events.FOLDER_ERRORS, function (event, arg) {
  294. $scope.model[arg.data.folder].errors = arg.data.errors.length;
  295. });
  296. $scope.$on(Events.FOLDER_SCAN_PROGRESS, function (event, arg) {
  297. var data = arg.data;
  298. $scope.scanProgress[data.folder] = {
  299. current: data.current,
  300. total: data.total,
  301. rate: data.rate
  302. };
  303. console.log("FolderScanProgress", data);
  304. });
  305. $scope.emitHTTPError = function (data, status, headers, config) {
  306. $scope.$emit('HTTPError', { data: data, status: status, headers: headers, config: config });
  307. };
  308. var debouncedFuncs = {};
  309. function refreshFolder(folder) {
  310. if ($scope.folders[folder].paused) {
  311. return;
  312. }
  313. var key = "refreshFolder" + folder;
  314. if (!debouncedFuncs[key]) {
  315. debouncedFuncs[key] = debounce(function () {
  316. $http.get(urlbase + '/db/status?folder=' + encodeURIComponent(folder)).success(function (data) {
  317. $scope.model[folder] = data;
  318. recalcLocalStateTotal();
  319. console.log("refreshFolder", folder, data);
  320. }).error($scope.emitHTTPError);
  321. }, 1000);
  322. }
  323. debouncedFuncs[key]();
  324. }
  325. function updateLocalConfig(config) {
  326. var hasConfig = !isEmptyObject($scope.config);
  327. $scope.config = config;
  328. $scope.config.options._listenAddressesStr = $scope.config.options.listenAddresses.join(', ');
  329. $scope.config.options._globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
  330. $scope.config.options._urAcceptedStr = "" + $scope.config.options.urAccepted;
  331. $scope.devices = $scope.config.devices;
  332. $scope.devices.forEach(function (deviceCfg) {
  333. $scope.completion[deviceCfg.deviceID] = {
  334. _total: 100,
  335. _needBytes: 0,
  336. _needItems: 0
  337. };
  338. });
  339. $scope.devices.sort(deviceCompare);
  340. $scope.folders = folderMap($scope.config.folders);
  341. Object.keys($scope.folders).forEach(function (folder) {
  342. refreshFolder(folder);
  343. $scope.folders[folder].devices.forEach(function (deviceCfg) {
  344. refreshCompletion(deviceCfg.deviceID, folder);
  345. });
  346. });
  347. refreshNoAuthWarning();
  348. setDefaultTheme();
  349. if (!hasConfig) {
  350. $scope.$emit('ConfigLoaded');
  351. }
  352. }
  353. function refreshSystem() {
  354. $http.get(urlbase + '/system/status').success(function (data) {
  355. $scope.myID = data.myID;
  356. $scope.system = data;
  357. if ($scope.reportDataPreviewVersion === '') {
  358. $scope.reportDataPreviewVersion = $scope.system.urVersionMax;
  359. }
  360. var listenersFailed = [];
  361. for (var address in data.connectionServiceStatus) {
  362. if (data.connectionServiceStatus[address].error) {
  363. listenersFailed.push(address + ": " + data.connectionServiceStatus[address].error);
  364. }
  365. }
  366. $scope.listenersFailed = listenersFailed;
  367. $scope.listenersTotal = $scope.sizeOf(data.connectionServiceStatus);
  368. $scope.discoveryTotal = data.discoveryMethods;
  369. var discoveryFailed = [];
  370. for (var disco in data.discoveryErrors) {
  371. if (data.discoveryErrors[disco]) {
  372. discoveryFailed.push(disco + ": " + data.discoveryErrors[disco]);
  373. }
  374. }
  375. $scope.discoveryFailed = discoveryFailed;
  376. refreshNoAuthWarning();
  377. console.log("refreshSystem", data);
  378. }).error($scope.emitHTTPError);
  379. }
  380. function refreshNoAuthWarning() {
  381. if (!$scope.system || !$scope.config) {
  382. // We need both to be able to determine the state.
  383. return
  384. }
  385. // If we're not listening on localhost, and there is no
  386. // authentication configured, and the magic setting to silence the
  387. // warning isn't set, then yell at the user.
  388. var addr = $scope.system.guiAddressUsed;
  389. var guiCfg = $scope.config.gui;
  390. $scope.openNoAuth = addr.substr(0, 4) !== "127."
  391. && addr.substr(0, 6) !== "[::1]:"
  392. && addr.substr(0, 1) !== "/"
  393. && (!guiCfg.user || !guiCfg.password)
  394. && guiCfg.authMode !== 'ldap'
  395. && !guiCfg.insecureAdminAccess;
  396. }
  397. function refreshDiscoveryCache() {
  398. $http.get(urlbase + '/system/discovery').success(function (data) {
  399. for (var device in data) {
  400. for (var i = 0; i < data[device].addresses.length; i++) {
  401. // Relay addresses are URLs with
  402. // .../?foo=barlongstuff that we strip away here. We
  403. // remove the final slash as well for symmetry with
  404. // tcp://192.0.2.42:1234 type addresses.
  405. data[device].addresses[i] = data[device].addresses[i].replace(/\/\?.*/, '');
  406. }
  407. }
  408. $scope.discoveryCache = data;
  409. console.log("refreshDiscoveryCache", data);
  410. }).error($scope.emitHTTPError);
  411. }
  412. function recalcLocalStateTotal() {
  413. $scope.localStateTotal = {
  414. bytes: 0,
  415. directories: 0,
  416. files: 0
  417. };
  418. for (var f in $scope.model) {
  419. $scope.localStateTotal.bytes += $scope.model[f].localBytes;
  420. $scope.localStateTotal.files += $scope.model[f].localFiles;
  421. $scope.localStateTotal.directories += $scope.model[f].localDirectories;
  422. }
  423. }
  424. function recalcCompletion(device) {
  425. var total = 0, needed = 0, deletes = 0, items = 0;
  426. for (var folder in $scope.completion[device]) {
  427. if (folder === "_total" || folder === '_needBytes' || folder === '_needItems') {
  428. continue;
  429. }
  430. total += $scope.completion[device][folder].globalBytes;
  431. needed += $scope.completion[device][folder].needBytes;
  432. items += $scope.completion[device][folder].needItems;
  433. deletes += $scope.completion[device][folder].needDeletes;
  434. }
  435. if (total == 0) {
  436. $scope.completion[device]._total = 100;
  437. $scope.completion[device]._needBytes = 0;
  438. $scope.completion[device]._needItems = 0;
  439. } else {
  440. $scope.completion[device]._total = Math.floor(100 * (1 - needed / total));
  441. $scope.completion[device]._needBytes = needed;
  442. $scope.completion[device]._needItems = items + deletes;
  443. }
  444. if (needed == 0 && deletes > 0) {
  445. // We don't need any data, but we have deletes that we need
  446. // to do. Drop down the completion percentage to indicate
  447. // that we have stuff to do.
  448. $scope.completion[device]._total = 95;
  449. }
  450. console.log("recalcCompletion", device, $scope.completion[device]);
  451. }
  452. function refreshCompletion(device, folder) {
  453. if (device === $scope.myID) {
  454. return;
  455. }
  456. $http.get(urlbase + '/db/completion?device=' + device + '&folder=' + encodeURIComponent(folder)).success(function (data) {
  457. if (!$scope.completion[device]) {
  458. $scope.completion[device] = {};
  459. }
  460. $scope.completion[device][folder] = data;
  461. recalcCompletion(device);
  462. }).error($scope.emitHTTPError);
  463. }
  464. function refreshConnectionStats() {
  465. $http.get(urlbase + '/system/connections').success(function (data) {
  466. var now = Date.now(),
  467. td = (now - prevDate) / 1000,
  468. id;
  469. prevDate = now;
  470. try {
  471. data.total.inbps = Math.max(0, (data.total.inBytesTotal - $scope.connectionsTotal.inBytesTotal) / td);
  472. data.total.outbps = Math.max(0, (data.total.outBytesTotal - $scope.connectionsTotal.outBytesTotal) / td);
  473. } catch (e) {
  474. data.total.inbps = 0;
  475. data.total.outbps = 0;
  476. }
  477. $scope.connectionsTotal = data.total;
  478. data = data.connections;
  479. for (id in data) {
  480. if (!data.hasOwnProperty(id)) {
  481. continue;
  482. }
  483. try {
  484. data[id].inbps = Math.max(0, (data[id].inBytesTotal - $scope.connections[id].inBytesTotal) / td);
  485. data[id].outbps = Math.max(0, (data[id].outBytesTotal - $scope.connections[id].outBytesTotal) / td);
  486. } catch (e) {
  487. data[id].inbps = 0;
  488. data[id].outbps = 0;
  489. }
  490. }
  491. $scope.connections = data;
  492. console.log("refreshConnections", data);
  493. }).error($scope.emitHTTPError);
  494. }
  495. function refreshErrors() {
  496. $http.get(urlbase + '/system/error').success(function (data) {
  497. $scope.errors = data.errors;
  498. console.log("refreshErrors", data);
  499. }).error($scope.emitHTTPError);
  500. }
  501. function refreshConfig() {
  502. $http.get(urlbase + '/system/config').success(function (data) {
  503. updateLocalConfig(data);
  504. console.log("refreshConfig", data);
  505. }).error($scope.emitHTTPError);
  506. $http.get(urlbase + '/system/config/insync').success(function (data) {
  507. $scope.configInSync = data.configInSync;
  508. }).error($scope.emitHTTPError);
  509. }
  510. $scope.refreshNeed = function (page, perpage) {
  511. if (!$scope.neededFolder) {
  512. return;
  513. }
  514. var url = urlbase + "/db/need?folder=" + encodeURIComponent($scope.neededFolder);
  515. url += "&page=" + page;
  516. url += "&perpage=" + perpage;
  517. $http.get(url).success(function (data) {
  518. console.log("refreshNeed", $scope.neededFolder, data);
  519. parseNeeded(data);
  520. }).error($scope.emitHTTPError);
  521. }
  522. function needAction(file) {
  523. var fDelete = 4096;
  524. var fDirectory = 16384;
  525. if ((file.flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
  526. return 'rmdir';
  527. } else if ((file.flags & fDelete) === fDelete) {
  528. return 'rm';
  529. } else if ((file.flags & fDirectory) === fDirectory) {
  530. return 'touch';
  531. } else {
  532. return 'sync';
  533. }
  534. }
  535. function parseNeeded(data) {
  536. $scope.needed = data;
  537. var merged = [];
  538. data.progress.forEach(function (item) {
  539. item.type = "progress";
  540. item.action = needAction(item);
  541. merged.push(item);
  542. });
  543. data.queued.forEach(function (item) {
  544. item.type = "queued";
  545. item.action = needAction(item);
  546. merged.push(item);
  547. });
  548. data.rest.forEach(function (item) {
  549. item.type = "rest";
  550. item.action = needAction(item);
  551. merged.push(item);
  552. });
  553. $scope.needed.items = merged;
  554. }
  555. function pathJoin(base, name) {
  556. base = expandTilde(base);
  557. if (base[base.length - 1] !== $scope.system.pathSeparator) {
  558. return base + $scope.system.pathSeparator + name;
  559. }
  560. return base + name;
  561. }
  562. function expandTilde(path) {
  563. if (path && path.trim().charAt(0) === '~') {
  564. return $scope.system.tilde + path.trim().substring(1);
  565. }
  566. return path;
  567. }
  568. function shouldSetDefaultFolderPath() {
  569. return $scope.config.options && $scope.config.options.defaultFolderPath && !$scope.editingExisting && $scope.folderEditor.folderPath.$pristine
  570. }
  571. function resetRemoteNeed() {
  572. $scope.remoteNeed = {};
  573. $scope.remoteNeedFolders = [];
  574. $scope.remoteNeedDevice = undefined;
  575. }
  576. function setDefaultTheme() {
  577. if (!document.getElementById("fallback-theme-css")){
  578. // check if no support for prefers-color-scheme
  579. var colorSchemeNotSupported = typeof window.matchMedia === "undefined" || window.matchMedia('(prefers-color-scheme: dark)').media === 'not all';
  580. if ($scope.config.gui.theme === "default" && colorSchemeNotSupported) {
  581. document.documentElement.style.display = 'none';
  582. document.head.insertAdjacentHTML(
  583. 'beforeend',
  584. '<link id="fallback-theme-css" rel="stylesheet" href="theme-assets/light/assets/css/theme.css" onload="document.documentElement.style.display = \'\'">'
  585. );
  586. }
  587. }
  588. }
  589. function saveIgnores(ignores, cb) {
  590. $http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
  591. ignore: ignores
  592. }).success(function () {
  593. if (cb) {
  594. cb();
  595. }
  596. });
  597. };
  598. $scope.refreshFailed = function (page, perpage) {
  599. if (!$scope.failed || !$scope.failed.folder) {
  600. return;
  601. }
  602. var url = urlbase + '/folder/errors?folder=' + encodeURIComponent($scope.failed.folder);
  603. url += "&page=" + page + "&perpage=" + perpage;
  604. $http.get(url).success(function (data) {
  605. $scope.failed = data;
  606. }).error($scope.emitHTTPError);
  607. };
  608. $scope.refreshRemoteNeed = function (folder, page, perpage) {
  609. if (!$scope.remoteNeedDevice) {
  610. return;
  611. }
  612. var url = urlbase + '/db/remoteneed?device=' + $scope.remoteNeedDevice.deviceID;
  613. url += '&folder=' + encodeURIComponent(folder);
  614. url += "&page=" + page + "&perpage=" + perpage;
  615. $http.get(url).success(function (data) {
  616. $scope.remoteNeed[folder] = data;
  617. }).error(function (err) {
  618. $scope.remoteNeed[folder] = undefined;
  619. $scope.emitHTTPError(err);
  620. });
  621. };
  622. $scope.refreshLocalChanged = function (page, perpage) {
  623. if (!$scope.localChangedFolder) {
  624. return;
  625. }
  626. var url = urlbase + '/db/localchanged?folder=';
  627. url += encodeURIComponent($scope.localChangedFolder);
  628. url += "&page=" + page + "&perpage=" + perpage;
  629. $http.get(url).success(function (data) {
  630. $scope.localChanged = data;
  631. }).error($scope.emitHTTPError);
  632. };
  633. var refreshDeviceStats = debounce(function () {
  634. $http.get(urlbase + "/stats/device").success(function (data) {
  635. $scope.deviceStats = data;
  636. for (var device in $scope.deviceStats) {
  637. $scope.deviceStats[device].lastSeen = new Date($scope.deviceStats[device].lastSeen);
  638. $scope.deviceStats[device].lastSeenDays = (new Date() - $scope.deviceStats[device].lastSeen) / 1000 / 86400;
  639. }
  640. console.log("refreshDeviceStats", data);
  641. }).error($scope.emitHTTPError);
  642. }, 2500);
  643. var refreshFolderStats = debounce(function () {
  644. $http.get(urlbase + "/stats/folder").success(function (data) {
  645. $scope.folderStats = data;
  646. for (var folder in $scope.folderStats) {
  647. if ($scope.folderStats[folder].lastFile) {
  648. $scope.folderStats[folder].lastFile.at = new Date($scope.folderStats[folder].lastFile.at);
  649. }
  650. $scope.folderStats[folder].lastScan = new Date($scope.folderStats[folder].lastScan);
  651. $scope.folderStats[folder].lastScanDays = (new Date() - $scope.folderStats[folder].lastScan) / 1000 / 86400;
  652. }
  653. console.log("refreshfolderStats", data);
  654. }).error($scope.emitHTTPError);
  655. }, 2500);
  656. var refreshThemes = debounce(function () {
  657. $http.get("themes.json").success(function (data) { // no urlbase here as this is served by the asset handler
  658. $scope.themes = data.themes;
  659. }).error($scope.emitHTTPError);
  660. }, 2500);
  661. var refreshGlobalChanges = debounce(function () {
  662. $http.get(urlbase + "/events/disk?limit=25").success(function (data) {
  663. if (!data) {
  664. // For reasons unknown this is called with data being the empty
  665. // string on shutdown, causing an error on .reverse().
  666. return;
  667. }
  668. data = data.reverse();
  669. $scope.globalChangeEvents = data;
  670. console.log("refreshGlobalChanges", data);
  671. }).error($scope.emitHTTPError);
  672. }, 2500);
  673. $scope.refresh = function () {
  674. refreshSystem();
  675. refreshDiscoveryCache();
  676. refreshConnectionStats();
  677. refreshErrors();
  678. };
  679. $scope.folderStatus = function (folderCfg) {
  680. if (folderCfg.paused) {
  681. return 'paused';
  682. }
  683. var folderInfo = $scope.model[folderCfg.id];
  684. // after restart syncthing process state may be empty
  685. if (typeof folderInfo === 'undefined' || !folderInfo.state) {
  686. return 'unknown';
  687. }
  688. var state = '' + folderInfo.state;
  689. if (state === 'error') {
  690. return 'stopped'; // legacy, the state is called "stopped" in the GUI
  691. }
  692. if (state !== 'idle') {
  693. return state;
  694. }
  695. if (folderInfo.needTotalItems > 0) {
  696. return 'outofsync';
  697. }
  698. if ($scope.hasFailedFiles(folderCfg.id)) {
  699. return 'faileditems';
  700. }
  701. if (folderInfo.receiveOnlyTotalItems) {
  702. return 'localadditions';
  703. }
  704. if (folderCfg.devices.length <= 1) {
  705. return 'unshared';
  706. }
  707. return state;
  708. };
  709. $scope.folderClass = function (folderCfg) {
  710. var status = $scope.folderStatus(folderCfg);
  711. if (status === 'idle' || status === 'localadditions') {
  712. return 'success';
  713. }
  714. if (status == 'paused') {
  715. return 'default';
  716. }
  717. if (status === 'syncing' || status === 'sync-preparing' || status === 'scanning') {
  718. return 'primary';
  719. }
  720. if (status === 'unknown') {
  721. return 'info';
  722. }
  723. if (status === 'stopped' || status === 'outofsync' || status === 'error' || status === 'faileditems') {
  724. return 'danger';
  725. }
  726. if (status === 'unshared' || status === 'scan-waiting' || status === 'sync-waiting') {
  727. return 'warning';
  728. }
  729. return 'info';
  730. };
  731. $scope.syncPercentage = function (folder) {
  732. if (typeof $scope.model[folder] === 'undefined') {
  733. return 100;
  734. }
  735. if ($scope.model[folder].needTotalItems === 0) {
  736. return 100;
  737. }
  738. if ($scope.model[folder].needBytes == 0 && $scope.model[folder].needDeletes > 0) {
  739. // We don't need any data, but we have deletes that we need
  740. // to do. Drop down the completion percentage to indicate
  741. // that we have stuff to do.
  742. return 95;
  743. }
  744. var pct = 100 * $scope.model[folder].inSyncBytes / $scope.model[folder].globalBytes;
  745. return Math.floor(pct);
  746. };
  747. $scope.scanPercentage = function (folder) {
  748. if (!$scope.scanProgress[folder]) {
  749. return undefined;
  750. }
  751. var pct = 100 * $scope.scanProgress[folder].current / $scope.scanProgress[folder].total;
  752. return Math.floor(pct);
  753. };
  754. $scope.scanRate = function (folder) {
  755. if (!$scope.scanProgress[folder]) {
  756. return 0;
  757. }
  758. return $scope.scanProgress[folder].rate;
  759. };
  760. $scope.scanRemaining = function (folder) {
  761. // Formats the remaining scan time as a string. Includes days and
  762. // hours only when relevant, resulting in time stamps like:
  763. // 00m 40s
  764. // 32m 40s
  765. // 2h 32m
  766. // 4d 2h
  767. // In case remaining scan time appears to be >31d, omit the
  768. // details, i.e.:
  769. // > 1 month
  770. if (!$scope.scanProgress[folder]) {
  771. return "";
  772. }
  773. // Calculate remaining bytes and seconds based on our current
  774. // rate.
  775. var remainingBytes = $scope.scanProgress[folder].total - $scope.scanProgress[folder].current;
  776. var seconds = remainingBytes / $scope.scanProgress[folder].rate;
  777. // Round up to closest ten seconds to avoid flapping too much to
  778. // and fro.
  779. seconds = Math.ceil(seconds / 10) * 10;
  780. // Separate out the number of days.
  781. var days = 0;
  782. var res = [];
  783. if (seconds >= 86400) {
  784. days = Math.floor(seconds / 86400);
  785. if (days > 31) {
  786. return '> 1 month';
  787. }
  788. res.push('' + days + 'd')
  789. seconds = seconds % 86400;
  790. }
  791. // Separate out the number of hours.
  792. var hours = 0;
  793. if (seconds > 3600) {
  794. hours = Math.floor(seconds / 3600);
  795. res.push('' + hours + 'h')
  796. seconds = seconds % 3600;
  797. }
  798. var d = new Date(1970, 0, 1).setSeconds(seconds);
  799. if (days === 0) {
  800. // Format minutes only if we're within a day of completion.
  801. var f = $filter('date')(d, "m'm'");
  802. res.push(f);
  803. }
  804. if (days === 0 && hours === 0) {
  805. // Format seconds only when we're within an hour of completion.
  806. var f = $filter('date')(d, "ss's'");
  807. res.push(f);
  808. }
  809. return res.join(' ');
  810. };
  811. $scope.deviceStatus = function (deviceCfg) {
  812. var status = '';
  813. if ($scope.deviceFolders(deviceCfg).length === 0) {
  814. status = 'unused-';
  815. }
  816. if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
  817. return 'unknown';
  818. }
  819. if (deviceCfg.paused) {
  820. return status + 'paused';
  821. }
  822. if ($scope.connections[deviceCfg.deviceID].connected) {
  823. if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
  824. return status + 'insync';
  825. } else {
  826. return 'syncing';
  827. }
  828. }
  829. // Disconnected
  830. return status + 'disconnected';
  831. };
  832. $scope.deviceClass = function (deviceCfg) {
  833. if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
  834. return 'info';
  835. }
  836. if (deviceCfg.paused) {
  837. return 'default';
  838. }
  839. if ($scope.connections[deviceCfg.deviceID].connected) {
  840. if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
  841. return 'success';
  842. } else {
  843. return 'primary';
  844. }
  845. }
  846. // Disconnected
  847. return 'info';
  848. };
  849. $scope.syncthingStatus = function () {
  850. var syncCount = 0;
  851. var notifyCount = 0;
  852. var pauseCount = 0;
  853. // loop through all folders
  854. var folderListCache = $scope.folderList();
  855. for (var i = 0; i < folderListCache.length; i++) {
  856. var status = $scope.folderStatus(folderListCache[i]);
  857. switch (status) {
  858. case 'sync-preparing':
  859. case 'syncing':
  860. syncCount++;
  861. break;
  862. case 'stopped':
  863. case 'unknown':
  864. case 'outofsync':
  865. case 'error':
  866. notifyCount++;
  867. break;
  868. }
  869. }
  870. // loop through all devices
  871. var deviceCount = $scope.devices.length;
  872. var pendingFolders = 0;
  873. for (var i = 0; i < $scope.devices.length; i++) {
  874. var status = $scope.deviceStatus({
  875. deviceID: $scope.devices[i].deviceID
  876. });
  877. switch (status) {
  878. case 'unknown':
  879. notifyCount++;
  880. break;
  881. case 'paused':
  882. pauseCount++;
  883. break;
  884. case 'unused':
  885. deviceCount--;
  886. break;
  887. }
  888. pendingFolders += $scope.devices[i].pendingFolders.length;
  889. }
  890. // enumerate notifications
  891. if ($scope.openNoAuth || !$scope.configInSync || $scope.errorList().length > 0 || !online || (
  892. !isEmptyObject($scope.config) && ($scope.config.pendingDevices.length > 0 || pendingFolders > 0)
  893. )) {
  894. notifyCount++;
  895. }
  896. // at least one folder is syncing
  897. if (syncCount > 0) {
  898. return 'sync';
  899. }
  900. // a device is unknown or a folder is stopped/unknown/outofsync/error or some other notification is open or gui offline
  901. if (notifyCount > 0) {
  902. return 'notify';
  903. }
  904. // all used devices are paused except (this) one
  905. if (pauseCount === deviceCount - 1) {
  906. return 'pause';
  907. }
  908. return 'default';
  909. };
  910. $scope.deviceAddr = function (deviceCfg) {
  911. var conn = $scope.connections[deviceCfg.deviceID];
  912. if (conn && conn.connected) {
  913. return conn.address;
  914. }
  915. return '?';
  916. };
  917. $scope.friendlyNameFromShort = function (shortID) {
  918. var matches = $scope.devices.filter(function (n) {
  919. return n.deviceID.substr(0, 7) === shortID;
  920. });
  921. if (matches.length !== 1) {
  922. return shortID;
  923. }
  924. return matches[0].name;
  925. };
  926. $scope.friendlyNameFromID = function (deviceID) {
  927. var match = $scope.findDevice(deviceID);
  928. if (match) {
  929. return $scope.deviceName(match);
  930. }
  931. return deviceID.substr(0, 6);
  932. };
  933. $scope.findDevice = function (deviceID) {
  934. var matches = $scope.devices.filter(function (n) {
  935. return n.deviceID === deviceID;
  936. });
  937. if (matches.length !== 1) {
  938. return undefined;
  939. }
  940. return matches[0];
  941. };
  942. $scope.deviceName = function (deviceCfg) {
  943. if (typeof deviceCfg === 'undefined' || typeof deviceCfg.deviceID === 'undefined') {
  944. return "";
  945. }
  946. if (deviceCfg.name) {
  947. return deviceCfg.name;
  948. }
  949. return deviceCfg.deviceID.substr(0, 6);
  950. };
  951. $scope.thisDeviceName = function () {
  952. var device = $scope.thisDevice();
  953. if (typeof device === 'undefined') {
  954. return "(unknown device)";
  955. }
  956. if (device.name) {
  957. return device.name;
  958. }
  959. return device.deviceID.substr(0, 6);
  960. };
  961. $scope.setDevicePause = function (device, pause) {
  962. $scope.devices.forEach(function (cfg) {
  963. if (cfg.deviceID == device) {
  964. cfg.paused = pause;
  965. }
  966. });
  967. $scope.config.devices = $scope.devices;
  968. $scope.saveConfig();
  969. };
  970. $scope.setFolderPause = function (folder, pause) {
  971. var cfg = $scope.folders[folder];
  972. if (cfg) {
  973. cfg.paused = pause;
  974. $scope.config.folders = folderList($scope.folders);
  975. $scope.saveConfig();
  976. }
  977. };
  978. $scope.showDiscoveryFailures = function () {
  979. $('#discovery-failures').modal();
  980. };
  981. $scope.logging = {
  982. facilities: {},
  983. refreshFacilities: function () {
  984. $http.get(urlbase + '/system/debug').success(function (data) {
  985. var facilities = {};
  986. data.enabled = data.enabled || [];
  987. $.each(data.facilities, function (key, value) {
  988. facilities[key] = {
  989. description: value,
  990. enabled: data.enabled.indexOf(key) > -1
  991. }
  992. })
  993. $scope.logging.facilities = facilities;
  994. }).error($scope.emitHTTPError);
  995. },
  996. show: function () {
  997. $scope.logging.refreshFacilities();
  998. $scope.logging.timer = $timeout($scope.logging.fetch);
  999. var textArea = $('#logViewerText');
  1000. textArea.on("scroll", $scope.logging.onScroll);
  1001. $('#logViewer').modal().one('shown.bs.modal', function () {
  1002. // Scroll to bottom.
  1003. textArea.scrollTop(textArea[0].scrollHeight);
  1004. }).one('hidden.bs.modal', function () {
  1005. $timeout.cancel($scope.logging.timer);
  1006. textArea.off("scroll", $scope.logging.onScroll);
  1007. $scope.logging.timer = null;
  1008. $scope.logging.entries = [];
  1009. });
  1010. },
  1011. onFacilityChange: function (facility) {
  1012. var enabled = $scope.logging.facilities[facility].enabled;
  1013. // Disable checkboxes while we're in flight.
  1014. $.each($scope.logging.facilities, function (key) {
  1015. $scope.logging.facilities[key].enabled = null;
  1016. })
  1017. $http.post(urlbase + '/system/debug?' + (enabled ? 'enable=' : 'disable=') + facility)
  1018. .success($scope.logging.refreshFacilities)
  1019. .error($scope.emitHTTPError);
  1020. },
  1021. onScroll: function () {
  1022. var textArea = $('#logViewerText');
  1023. var scrollTop = textArea.prop('scrollTop');
  1024. var scrollHeight = textArea.prop('scrollHeight');
  1025. $scope.logging.paused = scrollHeight > (scrollTop + textArea.outerHeight());
  1026. // Browser events do not cause redraw, trigger manually.
  1027. $scope.$apply();
  1028. },
  1029. timer: null,
  1030. entries: [],
  1031. paused: false,
  1032. content: function () {
  1033. var content = "";
  1034. $.each($scope.logging.entries, function (idx, entry) {
  1035. content += entry.when.split('.')[0].replace('T', ' ') + ' ' + entry.message + "\n";
  1036. });
  1037. return content;
  1038. },
  1039. fetch: function () {
  1040. var textArea = $('#logViewerText');
  1041. if ($scope.logging.paused) {
  1042. if (!$scope.logging.timer) return;
  1043. $scope.logging.timer = $timeout($scope.logging.fetch, 500);
  1044. return;
  1045. }
  1046. var last = null;
  1047. if ($scope.logging.entries.length > 0) {
  1048. last = $scope.logging.entries[$scope.logging.entries.length - 1].when;
  1049. }
  1050. $http.get(urlbase + '/system/log' + (last ? '?since=' + encodeURIComponent(last) : '')).success(function (data) {
  1051. if (!$scope.logging.timer) return;
  1052. $scope.logging.timer = $timeout($scope.logging.fetch, 2000);
  1053. if (!$scope.logging.paused) {
  1054. if (data.messages) {
  1055. $scope.logging.entries.push.apply($scope.logging.entries, data.messages);
  1056. // Wait for the text area to be redrawn, adding new lines, and then scroll to bottom.
  1057. $timeout(function () {
  1058. textArea.scrollTop(textArea[0].scrollHeight);
  1059. });
  1060. }
  1061. }
  1062. });
  1063. }
  1064. };
  1065. $scope.discardChangedSettings = function () {
  1066. $("#discard-changes-confirmation").modal("hide");
  1067. $("#settings").off("hide.bs.modal").modal("hide");
  1068. };
  1069. $scope.showSettings = function () {
  1070. // Make a working copy
  1071. $scope.tmpOptions = angular.copy($scope.config.options);
  1072. $scope.tmpOptions.deviceName = $scope.thisDevice().name;
  1073. $scope.tmpOptions.upgrades = "none";
  1074. if ($scope.tmpOptions.autoUpgradeIntervalH > 0) {
  1075. $scope.tmpOptions.upgrades = "stable";
  1076. }
  1077. if ($scope.tmpOptions.upgradeToPreReleases) {
  1078. $scope.tmpOptions.upgrades = "candidate";
  1079. }
  1080. $scope.tmpGUI = angular.copy($scope.config.gui);
  1081. $scope.tmpRemoteIgnoredDevices = angular.copy($scope.config.remoteIgnoredDevices);
  1082. $scope.tmpDevices = angular.copy($scope.config.devices);
  1083. $('#settings').modal("show");
  1084. $("#settings a[href='#settings-general']").tab("show");
  1085. $("#settings").on('hide.bs.modal', function (event) {
  1086. if ($scope.settingsModified()) {
  1087. event.preventDefault();
  1088. $("#discard-changes-confirmation").modal("show");
  1089. } else {
  1090. $("#settings").off("hide.bs.modal");
  1091. }
  1092. });
  1093. };
  1094. $scope.saveConfig = function (callback) {
  1095. var cfg = JSON.stringify($scope.config);
  1096. var opts = {
  1097. headers: {
  1098. 'Content-Type': 'application/json'
  1099. }
  1100. };
  1101. $http.post(urlbase + '/system/config', cfg, opts).success(function () {
  1102. refreshConfig();
  1103. if (callback) {
  1104. callback();
  1105. }
  1106. }).error(function (data, status, headers, config) {
  1107. refreshConfig();
  1108. $scope.emitHTTPError(data, status, headers, config);
  1109. });
  1110. };
  1111. $scope.urVersions = function () {
  1112. var result = [];
  1113. if ($scope.system) {
  1114. for (var i = $scope.system.urVersionMax; i >= 2; i--) {
  1115. result.push("" + i);
  1116. }
  1117. }
  1118. return result;
  1119. };
  1120. $scope.settingsModified = function () {
  1121. // Options has artificial properties injected into the temp config.
  1122. // Need to recompute them before we can check equality
  1123. var options = angular.copy($scope.config.options);
  1124. options.deviceName = $scope.thisDevice().name;
  1125. options.upgrades = "none";
  1126. if (options.autoUpgradeIntervalH > 0) {
  1127. options.upgrades = "stable";
  1128. }
  1129. if (options.upgradeToPreReleases) {
  1130. options.upgrades = "candidate";
  1131. }
  1132. var optionsEqual = angular.equals(options, $scope.tmpOptions);
  1133. var guiEquals = angular.equals($scope.config.gui, $scope.tmpGUI);
  1134. var ignoredDevicesEquals = angular.equals($scope.config.remoteIgnoredDevices, $scope.tmpRemoteIgnoredDevices);
  1135. var ignoredFoldersEquals = angular.equals($scope.config.devices, $scope.tmpDevices);
  1136. console.log("settings equals - options: " + optionsEqual + " gui: " + guiEquals + " ignDev: " + ignoredDevicesEquals + " ignFol: " + ignoredFoldersEquals);
  1137. return !optionsEqual || !guiEquals || !ignoredDevicesEquals || !ignoredFoldersEquals;
  1138. };
  1139. $scope.saveSettings = function () {
  1140. // Make sure something changed
  1141. if ($scope.settingsModified()) {
  1142. var themeChanged = $scope.config.gui.theme !== $scope.tmpGUI.theme;
  1143. // Angular has issues with selects with numeric values, so we handle strings here.
  1144. $scope.tmpOptions.urAccepted = parseInt($scope.tmpOptions._urAcceptedStr);
  1145. // Check if auto-upgrade has been enabled or disabled. This
  1146. // also has an effect on usage reporting, so do the check
  1147. // for that later.
  1148. if ($scope.tmpOptions.upgrades == "candidate") {
  1149. $scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12;
  1150. $scope.tmpOptions.upgradeToPreReleases = true;
  1151. $scope.tmpOptions.urAccepted = $scope.system.urVersionMax;
  1152. $scope.tmpOptions.urSeen = $scope.system.urVersionMax;
  1153. } else if ($scope.tmpOptions.upgrades == "stable") {
  1154. $scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12;
  1155. $scope.tmpOptions.upgradeToPreReleases = false;
  1156. } else {
  1157. $scope.tmpOptions.autoUpgradeIntervalH = 0;
  1158. $scope.tmpOptions.upgradeToPreReleases = false;
  1159. }
  1160. // Check if protocol will need to be changed on restart
  1161. if ($scope.config.gui.useTLS !== $scope.tmpGUI.useTLS) {
  1162. $scope.protocolChanged = true;
  1163. }
  1164. // Parse strings to arrays before copying over
  1165. ['listenAddresses', 'globalAnnounceServers'].forEach(function (key) {
  1166. $scope.tmpOptions[key] = $scope.tmpOptions["_" + key + "Str"].split(/[ ,]+/).map(function (x) {
  1167. return x.trim();
  1168. });
  1169. });
  1170. // Apply new settings locally
  1171. $scope.thisDeviceIn($scope.tmpDevices).name = $scope.tmpOptions.deviceName;
  1172. $scope.config.options = angular.copy($scope.tmpOptions);
  1173. $scope.config.gui = angular.copy($scope.tmpGUI);
  1174. $scope.config.remoteIgnoredDevices = angular.copy($scope.tmpRemoteIgnoredDevices);
  1175. $scope.config.devices = angular.copy($scope.tmpDevices);
  1176. // $scope.devices is updated by updateLocalConfig based on
  1177. // the config changed event, but settingsModified will look
  1178. // at it before that and conclude that the settings are
  1179. // modified (even though we just saved) unless we update
  1180. // here as well...
  1181. $scope.devices = $scope.config.devices;
  1182. $scope.saveConfig(function () {
  1183. if (themeChanged) {
  1184. document.location.reload(true);
  1185. }
  1186. });
  1187. }
  1188. $("#settings").off("hide.bs.modal").modal("hide");
  1189. };
  1190. $scope.saveAdvanced = function () {
  1191. $scope.config = $scope.advancedConfig;
  1192. $scope.saveConfig();
  1193. $('#advanced').modal("hide");
  1194. };
  1195. $scope.restart = function () {
  1196. restarting = true;
  1197. $('#restarting').modal();
  1198. $http.post(urlbase + '/system/restart');
  1199. $scope.configInSync = true;
  1200. // Switch webpage protocol if needed
  1201. if ($scope.protocolChanged) {
  1202. var protocol = 'http';
  1203. if ($scope.config.gui.useTLS) {
  1204. protocol = 'https';
  1205. }
  1206. setTimeout(function () {
  1207. window.location.protocol = protocol;
  1208. }, 2500);
  1209. $scope.protocolChanged = false;
  1210. }
  1211. };
  1212. $scope.upgrade = function () {
  1213. restarting = true;
  1214. $('#upgrade').modal('hide');
  1215. $('#majorUpgrade').modal('hide');
  1216. $('#upgrading').modal();
  1217. $http.post(urlbase + '/system/upgrade').success(function () {
  1218. $('#restarting').modal();
  1219. $('#upgrading').modal('hide');
  1220. }).error(function () {
  1221. $('#upgrading').modal('hide');
  1222. });
  1223. };
  1224. $scope.shutdown = function () {
  1225. restarting = true;
  1226. $http.post(urlbase + '/system/shutdown').success(function () {
  1227. $('#shutdown').modal();
  1228. }).error($scope.emitHTTPError);
  1229. $scope.configInSync = true;
  1230. };
  1231. $scope.editDevice = function (deviceCfg) {
  1232. $scope.currentDevice = $.extend({}, deviceCfg);
  1233. $scope.editingExisting = true;
  1234. $scope.willBeReintroducedBy = undefined;
  1235. if (deviceCfg.introducedBy) {
  1236. var introducerDevice = $scope.findDevice(deviceCfg.introducedBy);
  1237. if (introducerDevice && introducerDevice.introducer) {
  1238. $scope.willBeReintroducedBy = $scope.deviceName(introducerDevice);
  1239. }
  1240. }
  1241. $scope.currentDevice._addressesStr = deviceCfg.addresses.join(', ');
  1242. $scope.currentDevice.selectedFolders = {};
  1243. $scope.deviceFolders($scope.currentDevice).forEach(function (folder) {
  1244. $scope.currentDevice.selectedFolders[folder] = true;
  1245. });
  1246. $scope.deviceEditor.$setPristine();
  1247. $('#editDevice').modal();
  1248. };
  1249. $scope.selectAllFolders = function () {
  1250. angular.forEach($scope.folders, function (_, id) {
  1251. $scope.currentDevice.selectedFolders[id] = true;
  1252. });
  1253. };
  1254. $scope.deSelectAllFolders = function () {
  1255. angular.forEach($scope.folders, function (_, id) {
  1256. $scope.currentDevice.selectedFolders[id] = false;
  1257. });
  1258. };
  1259. $scope.addDevice = function (deviceID, name) {
  1260. return $http.get(urlbase + '/system/discovery')
  1261. .success(function (registry) {
  1262. $scope.discovery = [];
  1263. outer:
  1264. for (var id in registry) {
  1265. if ($scope.discovery.length === 5) {
  1266. break;
  1267. }
  1268. for (var i = 0; i < $scope.devices.length; i++) {
  1269. if ($scope.devices[i].deviceID === id) {
  1270. continue outer;
  1271. }
  1272. }
  1273. $scope.discovery.push(id);
  1274. }
  1275. })
  1276. .then(function () {
  1277. $scope.currentDevice = {
  1278. name: name,
  1279. deviceID: deviceID,
  1280. _addressesStr: 'dynamic',
  1281. compression: 'metadata',
  1282. introducer: false,
  1283. selectedFolders: {},
  1284. pendingFolders: [],
  1285. ignoredFolders: []
  1286. };
  1287. $scope.editingExisting = false;
  1288. $scope.deviceEditor.$setPristine();
  1289. $('#editDevice').modal();
  1290. });
  1291. };
  1292. $scope.deleteDevice = function () {
  1293. $('#editDevice').modal('hide');
  1294. if (!$scope.editingExisting) {
  1295. return;
  1296. }
  1297. $scope.devices = $scope.devices.filter(function (n) {
  1298. return n.deviceID !== $scope.currentDevice.deviceID;
  1299. });
  1300. $scope.config.devices = $scope.devices;
  1301. for (var id in $scope.folders) {
  1302. $scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
  1303. return n.deviceID !== $scope.currentDevice.deviceID;
  1304. });
  1305. }
  1306. $scope.saveConfig();
  1307. };
  1308. $scope.saveDevice = function () {
  1309. $('#editDevice').modal('hide');
  1310. $scope.saveDeviceConfig($scope.currentDevice);
  1311. };
  1312. $scope.saveDeviceConfig = function (deviceCfg) {
  1313. deviceCfg.addresses = deviceCfg._addressesStr.split(',').map(function (x) {
  1314. return x.trim();
  1315. });
  1316. var done = false;
  1317. for (var i = 0; i < $scope.devices.length && !done; i++) {
  1318. if ($scope.devices[i].deviceID === deviceCfg.deviceID) {
  1319. $scope.devices[i] = deviceCfg;
  1320. done = true;
  1321. }
  1322. }
  1323. if (!done) {
  1324. $scope.devices.push(deviceCfg);
  1325. }
  1326. $scope.devices.sort(deviceCompare);
  1327. $scope.config.devices = $scope.devices;
  1328. for (var id in deviceCfg.selectedFolders) {
  1329. if (deviceCfg.selectedFolders[id]) {
  1330. var found = false;
  1331. for (i = 0; i < $scope.folders[id].devices.length; i++) {
  1332. if ($scope.folders[id].devices[i].deviceID === deviceCfg.deviceID) {
  1333. found = true;
  1334. break;
  1335. }
  1336. }
  1337. if (!found) {
  1338. $scope.folders[id].devices.push({
  1339. deviceID: deviceCfg.deviceID
  1340. });
  1341. }
  1342. } else {
  1343. $scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
  1344. return n.deviceID !== deviceCfg.deviceID;
  1345. });
  1346. }
  1347. }
  1348. $scope.saveConfig();
  1349. };
  1350. $scope.ignoreDevice = function (pendingDevice) {
  1351. pendingDevice = angular.copy(pendingDevice);
  1352. // Bump time
  1353. pendingDevice.time = (new Date()).toISOString();
  1354. $scope.config.remoteIgnoredDevices.push(pendingDevice);
  1355. $scope.saveConfig();
  1356. };
  1357. $scope.unignoreDeviceFromTemporaryConfig = function (ignoredDevice) {
  1358. $scope.tmpRemoteIgnoredDevices = $scope.tmpRemoteIgnoredDevices.filter(function (existingIgnoredDevice) {
  1359. return ignoredDevice.deviceID !== existingIgnoredDevice.deviceID;
  1360. });
  1361. };
  1362. $scope.ignoredFoldersCountTmpConfig = function () {
  1363. var count = 0;
  1364. ($scope.tmpDevices || []).forEach(function (deviceCfg) {
  1365. count += deviceCfg.ignoredFolders.length;
  1366. });
  1367. return count;
  1368. };
  1369. $scope.unignoreFolderFromTemporaryConfig = function (device, ignoredFolderID) {
  1370. for (var i = 0; i < $scope.tmpDevices.length; i++) {
  1371. if ($scope.tmpDevices[i].deviceID == device) {
  1372. $scope.tmpDevices[i].ignoredFolders = $scope.tmpDevices[i].ignoredFolders.filter(function (existingIgnoredFolder) {
  1373. return existingIgnoredFolder.id !== ignoredFolderID;
  1374. });
  1375. return;
  1376. }
  1377. }
  1378. };
  1379. $scope.otherDevices = function () {
  1380. return $scope.devices.filter(function (n) {
  1381. return n.deviceID !== $scope.myID;
  1382. });
  1383. };
  1384. $scope.thisDevice = function () {
  1385. return $scope.thisDeviceIn($scope.devices);
  1386. };
  1387. $scope.thisDeviceIn = function (l) {
  1388. for (var i = 0; i < l.length; i++) {
  1389. var n = l[i];
  1390. if (n.deviceID === $scope.myID) {
  1391. return n;
  1392. }
  1393. }
  1394. };
  1395. $scope.allDevices = function () {
  1396. var devices = $scope.otherDevices();
  1397. devices.push($scope.thisDevice());
  1398. return devices;
  1399. };
  1400. $scope.setAllDevicesPause = function (pause) {
  1401. $scope.devices.forEach(function (cfg) {
  1402. cfg.paused = pause;
  1403. });
  1404. $scope.config.devices = $scope.devices;
  1405. $scope.saveConfig();
  1406. }
  1407. $scope.isAtleastOneDevicePausedStateSetTo = function (pause) {
  1408. for (var i = 0; i < $scope.devices.length; i++) {
  1409. if ($scope.devices[i].paused == pause) {
  1410. return true;
  1411. }
  1412. }
  1413. return false
  1414. }
  1415. $scope.errorList = function () {
  1416. if (!$scope.errors) {
  1417. return [];
  1418. }
  1419. return $scope.errors.filter(function (e) {
  1420. return e.when > $scope.seenError;
  1421. });
  1422. };
  1423. $scope.clearErrors = function () {
  1424. $scope.seenError = $scope.errors[$scope.errors.length - 1].when;
  1425. $http.post(urlbase + '/system/error/clear');
  1426. };
  1427. $scope.fsWatcherErrorMap = function () {
  1428. var errs = {}
  1429. $.each($scope.folders, function (id, cfg) {
  1430. if (cfg.fsWatcherEnabled && $scope.model[cfg.id] && $scope.model[id].watchError && !cfg.paused && $scope.folderStatus(cfg) !== 'stopped') {
  1431. errs[id] = $scope.model[id].watchError;
  1432. }
  1433. });
  1434. return errs;
  1435. };
  1436. $scope.friendlyDevices = function (str) {
  1437. for (var i = 0; i < $scope.devices.length; i++) {
  1438. var cfg = $scope.devices[i];
  1439. str = str.replace(cfg.deviceID, $scope.deviceName(cfg));
  1440. }
  1441. return str;
  1442. };
  1443. $scope.folderList = function () {
  1444. return folderList($scope.folders);
  1445. };
  1446. $scope.directoryList = [];
  1447. $scope.$watch('currentFolder.path', function (newvalue) {
  1448. if (!newvalue) {
  1449. return;
  1450. }
  1451. $scope.currentFolder.path = expandTilde(newvalue);
  1452. $http.get(urlbase + '/system/browse', {
  1453. params: { current: newvalue }
  1454. }).success(function (data) {
  1455. $scope.directoryList = data;
  1456. }).error($scope.emitHTTPError);
  1457. });
  1458. $scope.$watch('currentFolder.label', function (newvalue) {
  1459. if (!newvalue || !shouldSetDefaultFolderPath()) {
  1460. return;
  1461. }
  1462. $scope.currentFolder.path = pathJoin($scope.config.options.defaultFolderPath, newvalue);
  1463. });
  1464. $scope.$watch('currentFolder.id', function (newvalue) {
  1465. if (!newvalue || !shouldSetDefaultFolderPath() || $scope.currentFolder.label) {
  1466. return;
  1467. }
  1468. $scope.currentFolder.path = pathJoin($scope.config.options.defaultFolderPath, newvalue);
  1469. });
  1470. $scope.fsWatcherToggled = function () {
  1471. if ($scope.currentFolder.fsWatcherEnabled) {
  1472. $scope.currentFolder.rescanIntervalS = 3600;
  1473. } else {
  1474. $scope.currentFolder.rescanIntervalS = 60;
  1475. }
  1476. };
  1477. $scope.loadFormIntoScope = function (form) {
  1478. console.log('loadFormIntoScope', form.$name);
  1479. switch (form.$name) {
  1480. case 'deviceEditor':
  1481. $scope.deviceEditor = form;
  1482. break;
  1483. case 'folderEditor':
  1484. $scope.folderEditor = form;
  1485. break;
  1486. }
  1487. };
  1488. $scope.globalChanges = function () {
  1489. $('#globalChanges').modal();
  1490. };
  1491. $scope.editFolderModal = function () {
  1492. $scope.folderPathErrors = {};
  1493. $scope.folderEditor.$setPristine();
  1494. $('#editFolder').modal().one('shown.bs.tab', function (e) {
  1495. if (e.target.attributes.href.value === "#folder-ignores") {
  1496. $('#folder-ignores textarea').focus();
  1497. }
  1498. }).one('hidden.bs.modal', function () {
  1499. $('.nav-tabs a[href="#folder-general"]').tab('show');
  1500. window.location.hash = "";
  1501. });
  1502. };
  1503. $scope.editFolder = function (folderCfg) {
  1504. $scope.editingExisting = true;
  1505. $scope.currentFolder = angular.copy(folderCfg);
  1506. if ($scope.currentFolder.path.length > 1 && $scope.currentFolder.path.slice(-1) === $scope.system.pathSeparator) {
  1507. $scope.currentFolder.path = $scope.currentFolder.path.slice(0, -1);
  1508. }
  1509. // Cache complete device objects indexed by ID for lookups
  1510. var devMap = deviceMap($scope.devices)
  1511. $scope.currentFolder.sharedDevices = [];
  1512. $scope.currentFolder.selectedDevices = {};
  1513. $scope.currentFolder.devices.forEach(function (n) {
  1514. if (n.deviceID !== $scope.myID) {
  1515. $scope.currentFolder.sharedDevices.push(devMap[n.deviceID]);
  1516. }
  1517. $scope.currentFolder.selectedDevices[n.deviceID] = true;
  1518. });
  1519. $scope.currentFolder.unrelatedDevices = $scope.devices.filter(function (n) {
  1520. return n.deviceID !== $scope.myID
  1521. && ! $scope.currentFolder.selectedDevices[n.deviceID]
  1522. });
  1523. if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "trashcan") {
  1524. $scope.currentFolder.trashcanFileVersioning = true;
  1525. $scope.currentFolder.fileVersioningSelector = "trashcan";
  1526. $scope.currentFolder.trashcanClean = +$scope.currentFolder.versioning.params.cleanoutDays;
  1527. } else if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "simple") {
  1528. $scope.currentFolder.simpleFileVersioning = true;
  1529. $scope.currentFolder.fileVersioningSelector = "simple";
  1530. $scope.currentFolder.simpleKeep = +$scope.currentFolder.versioning.params.keep;
  1531. } else if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "staggered") {
  1532. $scope.currentFolder.staggeredFileVersioning = true;
  1533. $scope.currentFolder.fileVersioningSelector = "staggered";
  1534. $scope.currentFolder.staggeredMaxAge = Math.floor(+$scope.currentFolder.versioning.params.maxAge / 86400);
  1535. $scope.currentFolder.staggeredCleanInterval = +$scope.currentFolder.versioning.params.cleanInterval;
  1536. $scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.versioning.params.versionsPath;
  1537. } else if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "external") {
  1538. $scope.currentFolder.externalFileVersioning = true;
  1539. $scope.currentFolder.fileVersioningSelector = "external";
  1540. $scope.currentFolder.externalCommand = $scope.currentFolder.versioning.params.command;
  1541. } else {
  1542. $scope.currentFolder.fileVersioningSelector = "none";
  1543. }
  1544. $scope.currentFolder.trashcanClean = $scope.currentFolder.trashcanClean || 0; // weeds out nulls and undefineds
  1545. $scope.currentFolder.simpleKeep = $scope.currentFolder.simpleKeep || 5;
  1546. $scope.currentFolder.staggeredCleanInterval = $scope.currentFolder.staggeredCleanInterval || 3600;
  1547. $scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.staggeredVersionsPath || "";
  1548. // staggeredMaxAge can validly be zero, which we should not replace
  1549. // with the default value of 365. So only set the default if it's
  1550. // actually undefined.
  1551. if (typeof $scope.currentFolder.staggeredMaxAge === 'undefined') {
  1552. $scope.currentFolder.staggeredMaxAge = 365;
  1553. }
  1554. $scope.currentFolder.externalCommand = $scope.currentFolder.externalCommand || "";
  1555. $('#folder-ignores textarea').val($translate.instant("Loading..."));
  1556. $('#folder-ignores textarea').attr('disabled', 'disabled');
  1557. $http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
  1558. .success(function (data) {
  1559. $scope.currentFolder.ignores = data.ignore || [];
  1560. $('#folder-ignores textarea').val($scope.currentFolder.ignores.join('\n'));
  1561. $('#folder-ignores textarea').removeAttr('disabled');
  1562. })
  1563. .error(function (err) {
  1564. $('#folder-ignores textarea').val($translate.instant("Failed to load ignore patterns."));
  1565. $scope.emitHTTPError(err);
  1566. });
  1567. $scope.editFolderModal();
  1568. };
  1569. $scope.selectAllSharedDevices = function (state) {
  1570. var devices = $scope.currentFolder.sharedDevices;
  1571. for (var i = 0; i < devices.length; i++) {
  1572. $scope.currentFolder.selectedDevices[devices[i].deviceID] = !!state;
  1573. }
  1574. };
  1575. $scope.selectAllUnrelatedDevices = function (state) {
  1576. var devices = $scope.currentFolder.unrelatedDevices;
  1577. for (var i = 0; i < devices.length; i++) {
  1578. $scope.currentFolder.selectedDevices[devices[i].deviceID] = !!state;
  1579. }
  1580. };
  1581. $scope.addFolder = function () {
  1582. $http.get(urlbase + '/svc/random/string?length=10').success(function (data) {
  1583. $scope.editingExisting = false;
  1584. $scope.currentFolder = angular.copy($scope.folderDefaults);
  1585. $scope.currentFolder.id = (data.random.substr(0, 5) + '-' + data.random.substr(5, 5)).toLowerCase();
  1586. $scope.currentFolder.unrelatedDevices = $scope.otherDevices();
  1587. $('#folder-ignores textarea').val("");
  1588. $('#folder-ignores textarea').removeAttr('disabled');
  1589. $scope.editFolderModal();
  1590. });
  1591. };
  1592. $scope.addFolderAndShare = function (folder, folderLabel, device) {
  1593. $scope.editingExisting = false;
  1594. $scope.currentFolder = angular.copy($scope.folderDefaults);
  1595. $scope.currentFolder.id = folder;
  1596. $scope.currentFolder.label = folderLabel;
  1597. $scope.currentFolder.viewFlags = {
  1598. importFromOtherDevice: true
  1599. };
  1600. $scope.currentFolder.selectedDevices[device] = true;
  1601. $scope.currentFolder.unrelatedDevices = $scope.otherDevices();
  1602. $('#folder-ignores textarea').val("");
  1603. $('#folder-ignores textarea').removeAttr('disabled');
  1604. $scope.editFolderModal();
  1605. };
  1606. $scope.shareFolderWithDevice = function (folder, device) {
  1607. $scope.folders[folder].devices.push({
  1608. deviceID: device
  1609. });
  1610. $scope.config.folders = folderList($scope.folders);
  1611. $scope.saveConfig();
  1612. };
  1613. $scope.saveFolder = function () {
  1614. $('#editFolder').modal('hide');
  1615. var folderCfg = angular.copy($scope.currentFolder);
  1616. folderCfg.devices = [];
  1617. folderCfg.selectedDevices[$scope.myID] = true;
  1618. for (var deviceID in folderCfg.selectedDevices) {
  1619. if (folderCfg.selectedDevices[deviceID] === true) {
  1620. folderCfg.devices.push({
  1621. deviceID: deviceID
  1622. });
  1623. }
  1624. }
  1625. delete folderCfg.sharedDevices;
  1626. delete folderCfg.selectedDevices;
  1627. delete folderCfg.unrelatedDevices;
  1628. if (folderCfg.fileVersioningSelector === "trashcan") {
  1629. folderCfg.versioning = {
  1630. 'Type': 'trashcan',
  1631. 'Params': {
  1632. 'cleanoutDays': '' + folderCfg.trashcanClean
  1633. }
  1634. };
  1635. delete folderCfg.trashcanFileVersioning;
  1636. delete folderCfg.trashcanClean;
  1637. } else if (folderCfg.fileVersioningSelector === "simple") {
  1638. folderCfg.versioning = {
  1639. 'Type': 'simple',
  1640. 'Params': {
  1641. 'keep': '' + folderCfg.simpleKeep
  1642. }
  1643. };
  1644. delete folderCfg.simpleFileVersioning;
  1645. delete folderCfg.simpleKeep;
  1646. } else if (folderCfg.fileVersioningSelector === "staggered") {
  1647. folderCfg.versioning = {
  1648. 'type': 'staggered',
  1649. 'params': {
  1650. 'maxAge': '' + (folderCfg.staggeredMaxAge * 86400),
  1651. 'cleanInterval': '' + folderCfg.staggeredCleanInterval,
  1652. 'versionsPath': '' + folderCfg.staggeredVersionsPath
  1653. }
  1654. };
  1655. delete folderCfg.staggeredFileVersioning;
  1656. delete folderCfg.staggeredMaxAge;
  1657. delete folderCfg.staggeredCleanInterval;
  1658. delete folderCfg.staggeredVersionsPath;
  1659. } else if (folderCfg.fileVersioningSelector === "external") {
  1660. folderCfg.versioning = {
  1661. 'Type': 'external',
  1662. 'Params': {
  1663. 'command': '' + folderCfg.externalCommand
  1664. }
  1665. };
  1666. delete folderCfg.externalFileVersioning;
  1667. delete folderCfg.externalCommand;
  1668. } else {
  1669. delete folderCfg.versioning;
  1670. }
  1671. var ignoresLoaded = !$('#folder-ignores textarea').is(':disabled');
  1672. var ignores = $('#folder-ignores textarea').val().split('\n');
  1673. // Split always returns a minimum 1-length array even for no patterns
  1674. if (ignores.length === 1 && ignores[0] === "") {
  1675. ignores = [];
  1676. }
  1677. if (!$scope.editingExisting && ignores.length) {
  1678. folderCfg.paused = true;
  1679. };
  1680. $scope.folders[folderCfg.id] = folderCfg;
  1681. $scope.config.folders = folderList($scope.folders);
  1682. if (ignoresLoaded && $scope.editingExisting && ignores !== folderCfg.ignores) {
  1683. saveIgnores(ignores);
  1684. };
  1685. $scope.saveConfig(function () {
  1686. if (!$scope.editingExisting && ignores.length) {
  1687. saveIgnores(ignores, function () {
  1688. $scope.setFolderPause(folderCfg.id, false);
  1689. });
  1690. }
  1691. });
  1692. };
  1693. $scope.ignoreFolder = function (device, pendingFolder) {
  1694. pendingFolder = angular.copy(pendingFolder);
  1695. // Bump time
  1696. pendingFolder.time = (new Date()).toISOString();
  1697. for (var i = 0; i < $scope.devices.length; i++) {
  1698. if ($scope.devices[i].deviceID == device) {
  1699. $scope.devices[i].ignoredFolders.push(pendingFolder);
  1700. $scope.saveConfig();
  1701. return;
  1702. }
  1703. }
  1704. };
  1705. $scope.sharesFolder = function (folderCfg) {
  1706. var names = [];
  1707. folderCfg.devices.forEach(function (device) {
  1708. if (device.deviceID !== $scope.myID) {
  1709. names.push($scope.deviceName($scope.findDevice(device.deviceID)));
  1710. }
  1711. });
  1712. names.sort();
  1713. return names.join(", ");
  1714. };
  1715. $scope.deviceFolders = function (deviceCfg) {
  1716. var folders = [];
  1717. $scope.folderList().forEach(function (folder) {
  1718. for (var i = 0; i < folder.devices.length; i++) {
  1719. if (folder.devices[i].deviceID === deviceCfg.deviceID) {
  1720. folders.push(folder.id);
  1721. break;
  1722. }
  1723. }
  1724. });
  1725. return folders;
  1726. };
  1727. $scope.folderLabel = function (folderID) {
  1728. if (!$scope.folders[folderID]) {
  1729. return folderID;
  1730. }
  1731. var label = $scope.folders[folderID].label;
  1732. return label && label.length > 0 ? label : folderID;
  1733. };
  1734. $scope.deleteFolder = function (id) {
  1735. $('#editFolder').modal('hide');
  1736. if (!$scope.editingExisting) {
  1737. return;
  1738. }
  1739. delete $scope.folders[id];
  1740. delete $scope.model[id];
  1741. $scope.config.folders = folderList($scope.folders);
  1742. recalcLocalStateTotal();
  1743. $scope.saveConfig();
  1744. };
  1745. function resetRestoreVersions() {
  1746. $scope.restoreVersions = {
  1747. folder: null,
  1748. selections: {},
  1749. versions: null,
  1750. tree: null,
  1751. errors: null,
  1752. filters: {},
  1753. massAction: function (name, action) {
  1754. $.each($scope.restoreVersions.versions, function (key) {
  1755. if (key.startsWith(name + '/') && (!$scope.restoreVersions.filters.text || key.indexOf($scope.restoreVersions.filters.text) > -1)) {
  1756. if (action == 'unset') {
  1757. delete $scope.restoreVersions.selections[key];
  1758. return;
  1759. }
  1760. var availableVersions = [];
  1761. $.each($scope.restoreVersions.filterVersions($scope.restoreVersions.versions[key]), function (idx, version) {
  1762. availableVersions.push(version.versionTime);
  1763. })
  1764. if (availableVersions.length) {
  1765. availableVersions.sort(function (a, b) { return a - b; });
  1766. if (action == 'latest') {
  1767. $scope.restoreVersions.selections[key] = availableVersions.pop();
  1768. } else if (action == 'oldest') {
  1769. $scope.restoreVersions.selections[key] = availableVersions.shift();
  1770. }
  1771. }
  1772. }
  1773. });
  1774. },
  1775. filterVersions: function (versions) {
  1776. var filteredVersions = [];
  1777. $.each(versions, function (idx, version) {
  1778. if (moment(version.versionTime).isBetween($scope.restoreVersions.filters['start'], $scope.restoreVersions.filters['end'], null, '[]')) {
  1779. filteredVersions.push(version);
  1780. }
  1781. });
  1782. return filteredVersions;
  1783. },
  1784. selectionCount: function () {
  1785. var count = 0;
  1786. $.each($scope.restoreVersions.selections, function (key, value) {
  1787. if (value) {
  1788. count++;
  1789. }
  1790. });
  1791. return count;
  1792. },
  1793. restore: function () {
  1794. $scope.restoreVersions.tree.clear();
  1795. $scope.restoreVersions.tree = null;
  1796. $scope.restoreVersions.versions = null;
  1797. var selections = {};
  1798. $.each($scope.restoreVersions.selections, function (key, value) {
  1799. if (value) {
  1800. selections[key] = value;
  1801. }
  1802. });
  1803. $scope.restoreVersions.selections = {};
  1804. $http.post(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder), selections).success(function (data) {
  1805. if (Object.keys(data).length == 0) {
  1806. $('#restoreVersions').modal('hide');
  1807. } else {
  1808. $scope.restoreVersions.errors = data;
  1809. }
  1810. });
  1811. },
  1812. show: function (folder) {
  1813. $scope.restoreVersions.folder = folder;
  1814. var closed = false;
  1815. var modalShown = $q.defer();
  1816. $('#restoreVersions').modal().one('hidden.bs.modal', function () {
  1817. closed = true;
  1818. resetRestoreVersions();
  1819. }).one('shown.bs.modal', function () {
  1820. modalShown.resolve();
  1821. });
  1822. var dataReceived = $http.get(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder))
  1823. .success(function (data) {
  1824. $.each(data, function (key, values) {
  1825. $.each(values, function (idx, value) {
  1826. value.modTime = new Date(value.modTime);
  1827. value.versionTime = new Date(value.versionTime);
  1828. });
  1829. values.sort(function (a, b) {
  1830. return b.versionTime - a.versionTime;
  1831. });
  1832. });
  1833. if (closed) return;
  1834. $scope.restoreVersions.versions = data;
  1835. });
  1836. $q.all([dataReceived, modalShown.promise]).then(function () {
  1837. $timeout(function () {
  1838. if (closed) {
  1839. resetRestoreVersions();
  1840. return;
  1841. }
  1842. $scope.restoreVersions.tree = $("#restoreTree").fancytree({
  1843. extensions: ["table", "filter"],
  1844. quicksearch: true,
  1845. filter: {
  1846. autoApply: true,
  1847. counter: true,
  1848. hideExpandedCounter: true,
  1849. hideExpanders: true,
  1850. highlight: true,
  1851. leavesOnly: false,
  1852. nodata: true,
  1853. mode: "hide"
  1854. },
  1855. table: {
  1856. indentation: 20,
  1857. nodeColumnIdx: 0,
  1858. },
  1859. debugLevel: 2,
  1860. source: buildTree($scope.restoreVersions.versions),
  1861. renderColumns: function (event, data) {
  1862. var node = data.node,
  1863. $tdList = $(node.tr).find(">td"),
  1864. template;
  1865. if (node.folder) {
  1866. template = '<div ng-include="\'syncthing/folder/restoreVersionsMassActions.html\'" class="pull-right"/>';
  1867. } else {
  1868. template = '<div ng-include="\'syncthing/folder/restoreVersionsVersionSelector.html\'" class="pull-right"/>';
  1869. }
  1870. var scope = $rootScope.$new(true);
  1871. scope.key = node.key;
  1872. scope.restoreVersions = $scope.restoreVersions;
  1873. $tdList.eq(1).html(
  1874. $compile(template)(scope)
  1875. );
  1876. // Force angular to redraw.
  1877. $timeout(function () {
  1878. $scope.$apply();
  1879. });
  1880. }
  1881. }).fancytree("getTree");
  1882. var minDate = moment(),
  1883. maxDate = moment(0, 'X'),
  1884. date;
  1885. // Find version window.
  1886. $.each($scope.restoreVersions.versions, function (key) {
  1887. $.each($scope.restoreVersions.versions[key], function (idx, version) {
  1888. date = moment(version.versionTime);
  1889. if (date.isBefore(minDate)) {
  1890. minDate = date;
  1891. }
  1892. if (date.isAfter(maxDate)) {
  1893. maxDate = date;
  1894. }
  1895. });
  1896. });
  1897. $scope.restoreVersions.filters['start'] = minDate;
  1898. $scope.restoreVersions.filters['end'] = maxDate;
  1899. var ranges = {
  1900. 'All time': [minDate, maxDate],
  1901. 'Today': [moment(), moment()],
  1902. 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
  1903. 'Last 7 Days': [moment().subtract(6, 'days'), moment()],
  1904. 'Last 30 Days': [moment().subtract(29, 'days'), moment()],
  1905. 'This Month': [moment().startOf('month'), moment().endOf('month')],
  1906. 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
  1907. };
  1908. // Filter out invalid ranges.
  1909. $.each(ranges, function (key, range) {
  1910. if (!range[0].isBetween(minDate, maxDate, null, '[]') && !range[1].isBetween(minDate, maxDate, null, '[]')) {
  1911. delete ranges[key];
  1912. }
  1913. });
  1914. $("#restoreVersionDateRange").daterangepicker({
  1915. timePicker: true,
  1916. timePicker24Hour: true,
  1917. timePickerSeconds: true,
  1918. autoUpdateInput: true,
  1919. opens: "left",
  1920. drops: "up",
  1921. startDate: minDate,
  1922. endDate: maxDate,
  1923. minDate: minDate,
  1924. maxDate: maxDate,
  1925. ranges: ranges,
  1926. locale: {
  1927. format: 'YYYY/MM/DD HH:mm:ss',
  1928. }
  1929. }).on('apply.daterangepicker', function (ev, picker) {
  1930. $scope.restoreVersions.filters['start'] = picker.startDate;
  1931. $scope.restoreVersions.filters['end'] = picker.endDate;
  1932. // Events for this UI element are not managed by angular.
  1933. // Force angular to wake up.
  1934. $timeout(function () {
  1935. $scope.$apply();
  1936. });
  1937. });
  1938. });
  1939. });
  1940. }
  1941. };
  1942. }
  1943. resetRestoreVersions();
  1944. $scope.$watchCollection('restoreVersions.filters', function () {
  1945. if (!$scope.restoreVersions.tree) return;
  1946. $scope.restoreVersions.tree.filterNodes(function (node) {
  1947. if (node.folder) return false;
  1948. if ($scope.restoreVersions.filters.text && node.key.indexOf($scope.restoreVersions.filters.text) < 0) {
  1949. return false;
  1950. }
  1951. if ($scope.restoreVersions.filterVersions(node.data.versions).length == 0) {
  1952. return false;
  1953. }
  1954. return true;
  1955. });
  1956. });
  1957. $scope.setAPIKey = function (cfg) {
  1958. $http.get(urlbase + '/svc/random/string?length=32').success(function (data) {
  1959. cfg.apiKey = data.random;
  1960. });
  1961. };
  1962. $scope.acceptUR = function () {
  1963. $scope.config.options.urAccepted = $scope.system.urVersionMax;
  1964. $scope.config.options.urSeen = $scope.system.urVersionMax;
  1965. $scope.saveConfig();
  1966. $('#ur').modal('hide');
  1967. };
  1968. $scope.declineUR = function () {
  1969. if ($scope.config.options.urAccepted === 0) {
  1970. $scope.config.options.urAccepted = -1;
  1971. }
  1972. $scope.config.options.urSeen = $scope.system.urVersionMax;
  1973. $scope.saveConfig();
  1974. $('#ur').modal('hide');
  1975. };
  1976. $scope.showNeed = function (folder) {
  1977. $scope.neededFolder = folder;
  1978. $scope.refreshNeed(1, 10);
  1979. $('#needed').modal().one('hidden.bs.modal', function () {
  1980. $scope.needed = undefined;
  1981. $scope.neededFolder = '';
  1982. });
  1983. };
  1984. $scope.showRemoteNeed = function (device) {
  1985. resetRemoteNeed();
  1986. $scope.remoteNeedDevice = device;
  1987. $scope.deviceFolders(device).forEach(function (folder) {
  1988. var comp = $scope.completion[device.deviceID][folder];
  1989. if (comp !== undefined && comp.needItems + comp.needDeletes === 0) {
  1990. return;
  1991. }
  1992. $scope.remoteNeedFolders.push(folder);
  1993. $scope.refreshRemoteNeed(folder, 1, 10);
  1994. });
  1995. $('#remoteNeed').modal().one('hidden.bs.modal', function () {
  1996. resetRemoteNeed();
  1997. });
  1998. };
  1999. $scope.showFailed = function (folder) {
  2000. $scope.failed.folder = folder;
  2001. $scope.failed = $scope.refreshFailed(1, 10);
  2002. $('#failed').modal().one('hidden.bs.modal', function () {
  2003. $scope.failed = {};
  2004. });
  2005. };
  2006. $scope.hasFailedFiles = function (folder) {
  2007. if (!$scope.model[folder]) {
  2008. return false;
  2009. }
  2010. return $scope.model[folder].errors !== 0;
  2011. };
  2012. $scope.override = function (folder) {
  2013. $http.post(urlbase + "/db/override?folder=" + encodeURIComponent(folder));
  2014. };
  2015. $scope.showLocalChanged = function (folder) {
  2016. $scope.localChangedFolder = folder;
  2017. $scope.localChanged = $scope.refreshLocalChanged(1, 10);
  2018. $('#localChanged').modal().one('hidden.bs.modal', function () {
  2019. $scope.localChanged = {};
  2020. $scope.localChangedFolder = undefined;
  2021. });
  2022. };
  2023. $scope.revert = function (folder) {
  2024. $http.post(urlbase + "/db/revert?folder=" + encodeURIComponent(folder));
  2025. };
  2026. $scope.canRevert = function (folder) {
  2027. var f = $scope.model[folder];
  2028. if (!f) {
  2029. return false;
  2030. }
  2031. return $scope.model[folder].receiveOnlyTotalItems > 0;
  2032. };
  2033. $scope.advanced = function () {
  2034. $scope.advancedConfig = angular.copy($scope.config);
  2035. $('#advanced').modal('show');
  2036. };
  2037. $scope.showReportPreview = function () {
  2038. $scope.reportPreview = true;
  2039. };
  2040. $scope.refreshReportDataPreview = function () {
  2041. $scope.reportDataPreview = '';
  2042. if (!$scope.reportDataPreviewVersion) {
  2043. return;
  2044. }
  2045. var version = parseInt($scope.reportDataPreviewVersion);
  2046. if ($scope.reportDataPreviewDiff && version > 2) {
  2047. $q.all([
  2048. $http.get(urlbase + '/svc/report?version=' + version),
  2049. $http.get(urlbase + '/svc/report?version=' + (version - 1)),
  2050. ]).then(function (responses) {
  2051. var newReport = responses[0].data;
  2052. var oldReport = responses[1].data;
  2053. angular.forEach(oldReport, function (_, key) {
  2054. delete newReport[key];
  2055. });
  2056. $scope.reportDataPreview = newReport;
  2057. });
  2058. } else {
  2059. $http.get(urlbase + '/svc/report?version=' + version).success(function (data) {
  2060. $scope.reportDataPreview = data;
  2061. }).error($scope.emitHTTPError);
  2062. }
  2063. };
  2064. $scope.rescanAllFolders = function () {
  2065. $http.post(urlbase + "/db/scan");
  2066. };
  2067. $scope.rescanFolder = function (folder) {
  2068. $http.post(urlbase + "/db/scan?folder=" + encodeURIComponent(folder));
  2069. };
  2070. $scope.setAllFoldersPause = function (pause) {
  2071. var folderListCache = $scope.folderList();
  2072. for (var i = 0; i < folderListCache.length; i++) {
  2073. folderListCache[i].paused = pause;
  2074. }
  2075. $scope.config.folders = folderList(folderListCache);
  2076. $scope.saveConfig();
  2077. };
  2078. $scope.isAtleastOneFolderPausedStateSetTo = function (pause) {
  2079. var folderListCache = $scope.folderList();
  2080. for (var i = 0; i < folderListCache.length; i++) {
  2081. if (folderListCache[i].paused == pause) {
  2082. return true;
  2083. }
  2084. }
  2085. return false;
  2086. };
  2087. $scope.activateAllFsWatchers = function () {
  2088. var folders = $scope.folderList();
  2089. $.each(folders, function (i) {
  2090. if (folders[i].fsWatcherEnabled) {
  2091. return;
  2092. }
  2093. folders[i].fsWatcherEnabled = true;
  2094. if (folders[i].rescanIntervalS === 0) {
  2095. return;
  2096. }
  2097. // Delay full scans, but scan at least once per day
  2098. folders[i].rescanIntervalS *= 60;
  2099. if (folders[i].rescanIntervalS > 86400) {
  2100. folders[i].rescanIntervalS = 86400;
  2101. }
  2102. });
  2103. $scope.config.folders = folders;
  2104. $scope.saveConfig();
  2105. };
  2106. $scope.bumpFile = function (folder, file) {
  2107. var url = urlbase + "/db/prio?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file);
  2108. // In order to get the right view of data in the response.
  2109. url += "&page=" + $scope.needed.page;
  2110. url += "&perpage=" + $scope.needed.perpage;
  2111. $http.post(url).success(function (data) {
  2112. if ($scope.neededFolder === folder) {
  2113. console.log("bumpFile", folder, data);
  2114. parseNeeded(data);
  2115. }
  2116. }).error($scope.emitHTTPError);
  2117. };
  2118. $scope.versionString = function () {
  2119. if (!$scope.version.version) {
  2120. return '';
  2121. }
  2122. var os = {
  2123. 'darwin': 'macOS',
  2124. 'dragonfly': 'DragonFly BSD',
  2125. 'freebsd': 'FreeBSD',
  2126. 'openbsd': 'OpenBSD',
  2127. 'netbsd': 'NetBSD',
  2128. 'linux': 'Linux',
  2129. 'windows': 'Windows',
  2130. 'solaris': 'Solaris'
  2131. }[$scope.version.os] || $scope.version.os;
  2132. var arch = {
  2133. '386': '32 bit',
  2134. 'amd64': '64 bit',
  2135. 'arm': 'ARM',
  2136. 'arm64': 'AArch64',
  2137. 'ppc64': 'PowerPC',
  2138. 'ppc64le': 'PowerPC (LE)'
  2139. }[$scope.version.arch] || $scope.version.arch;
  2140. return $scope.version.version + ', ' + os + ' (' + arch + ')';
  2141. };
  2142. $scope.inputTypeFor = function (key, value) {
  2143. if (key.substr(0, 1) === '_') {
  2144. return 'skip';
  2145. }
  2146. if (value === null) {
  2147. return 'null';
  2148. }
  2149. if (typeof value === 'number') {
  2150. return 'number';
  2151. }
  2152. if (typeof value === 'boolean') {
  2153. return 'checkbox';
  2154. }
  2155. if (value instanceof Array) {
  2156. return 'list';
  2157. }
  2158. if (typeof value === 'object') {
  2159. return 'skip';
  2160. }
  2161. return 'text';
  2162. };
  2163. $scope.themeName = function (theme) {
  2164. return theme.replace('-', ' ').replace(/(?:^|\s)\S/g, function (a) {
  2165. return a.toUpperCase();
  2166. });
  2167. };
  2168. $scope.modalLoaded = function () {
  2169. // once all modal elements have been processed
  2170. if ($('modal').length === 0) {
  2171. // pseudo main. called on all definitions assigned
  2172. initController();
  2173. }
  2174. };
  2175. $scope.toggleUnits = function () {
  2176. $scope.metricRates = !$scope.metricRates;
  2177. try {
  2178. window.localStorage["metricRates"] = $scope.metricRates;
  2179. } catch (exception) { }
  2180. };
  2181. $scope.sizeOf = function (dict) {
  2182. if (dict === undefined) {
  2183. return 0;
  2184. }
  2185. return Object.keys(dict).length;
  2186. };
  2187. $scope.dismissNotification = function (id) {
  2188. var idx = $scope.config.options.unackedNotificationIDs.indexOf(id);
  2189. if (idx > -1) {
  2190. $scope.config.options.unackedNotificationIDs.splice(idx, 1);
  2191. $scope.saveConfig();
  2192. }
  2193. };
  2194. $scope.abbreviatedError = function (addr) {
  2195. var status = $scope.system.lastDialStatus[addr];
  2196. if (!status || !status.error) {
  2197. return null;
  2198. }
  2199. var time = $filter('date')(status.when, "HH:mm:ss")
  2200. var err = status.error.replace(/.+: /, '');
  2201. return err + " (" + time + ")";
  2202. }
  2203. $scope.setCrashReportingEnabled = function (enabled) {
  2204. $scope.config.options.crashReportingEnabled = enabled;
  2205. $scope.saveConfig();
  2206. };
  2207. $scope.isUnixAddress = function (address) {
  2208. return address != null &&
  2209. (address.startsWith('/') ||
  2210. address.startsWith('unix://') ||
  2211. address.startsWith('unixs://'));
  2212. }
  2213. });