From 99af9178169c38d608d0c0bb46fe1ef1ca59a425 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Mon, 9 Apr 2018 13:20:45 +0200 Subject: [PATCH] MOBILE-2329 settings: Migrate preferences --- src/classes/sqlitedb.ts | 2 +- src/core/constants.ts | 1 + src/core/emulator/providers/file.ts | 2 +- src/core/login/pages/sites/sites.ts | 22 +-- src/core/settings/lang/en.json | 39 +++- src/core/settings/pages/about/about.html | 109 +++++++++++ src/core/settings/pages/about/about.module.ts | 33 ++++ src/core/settings/pages/about/about.ts | 105 ++++++++++ src/core/settings/pages/general/general.html | 24 +++ .../settings/pages/general/general.module.ts | 33 ++++ src/core/settings/pages/general/general.ts | 93 +++++++++ src/core/settings/pages/list/list.html | 2 +- .../pages/space-usage/space-usage.html | 28 +++ .../pages/space-usage/space-usage.module.ts | 35 ++++ .../settings/pages/space-usage/space-usage.ts | 180 ++++++++++++++++++ .../synchronization/synchronization.html | 29 +++ .../synchronization/synchronization.module.ts | 35 ++++ .../pages/synchronization/synchronization.ts | 113 +++++++++++ src/core/settings/providers/helper.ts | 86 ++++++++- src/providers/sites.ts | 30 +++ 20 files changed, 976 insertions(+), 25 deletions(-) create mode 100644 src/core/settings/pages/about/about.html create mode 100644 src/core/settings/pages/about/about.module.ts create mode 100644 src/core/settings/pages/about/about.ts create mode 100644 src/core/settings/pages/general/general.html create mode 100644 src/core/settings/pages/general/general.module.ts create mode 100644 src/core/settings/pages/general/general.ts create mode 100644 src/core/settings/pages/space-usage/space-usage.html create mode 100644 src/core/settings/pages/space-usage/space-usage.module.ts create mode 100644 src/core/settings/pages/space-usage/space-usage.ts create mode 100644 src/core/settings/pages/synchronization/synchronization.html create mode 100644 src/core/settings/pages/synchronization/synchronization.module.ts create mode 100644 src/core/settings/pages/synchronization/synchronization.ts diff --git a/src/classes/sqlitedb.ts b/src/classes/sqlitedb.ts index fdf17f59b..c60833ce2 100644 --- a/src/classes/sqlitedb.ts +++ b/src/classes/sqlitedb.ts @@ -270,7 +270,7 @@ export class SQLiteDB { deleteRecords(table: string, conditions?: object): Promise { if (conditions === null || typeof conditions == 'undefined') { // No conditions, delete the whole table. - return this.execute(`DELETE FROM TABLE ${table}`); + return this.execute(`DELETE FROM ${table}`); } const selectAndParams = this.whereClause(conditions); diff --git a/src/core/constants.ts b/src/core/constants.ts index dfff32e96..ac25f07b6 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -30,6 +30,7 @@ export class CoreConstants { static SETTINGS_RICH_TEXT_EDITOR = 'CoreSettingsRichTextEditor'; static SETTINGS_NOTIFICATION_SOUND = 'CoreSettingsNotificationSound'; static SETTINGS_SYNC_ONLY_ON_WIFI = 'CoreSettingsSyncOnlyOnWifi'; + static SETTINGS_REPORT_IN_BACKGROUND = 'CoreSettingsReportInBackground'; // WS constants. static WS_TIMEOUT = 30000; diff --git a/src/core/emulator/providers/file.ts b/src/core/emulator/providers/file.ts index cf6c77e8c..d25c69b7a 100644 --- a/src/core/emulator/providers/file.ts +++ b/src/core/emulator/providers/file.ts @@ -279,7 +279,7 @@ export class FileMock extends File { maxIterations = 10; // More accurate. Factor is 1.1. calculateByRequest(size, 1.1).then((size: number) => { - return size / 1024; // Return size in KB. + resolve(size / 1024); // Return size in KB. }); }); diff --git a/src/core/login/pages/sites/sites.ts b/src/core/login/pages/sites/sites.ts index 77dfb0d73..7e659e9c6 100644 --- a/src/core/login/pages/sites/sites.ts +++ b/src/core/login/pages/sites/sites.ts @@ -45,9 +45,9 @@ export class CoreLoginSitesPage { * View loaded. */ ionViewDidLoad(): void { - this.sitesProvider.getSites().then((sites) => { + this.sitesProvider.getSortedSites().then((sites) => { // Remove protocol from the url to show more url text. - sites = sites.map((site) => { + this.sites = sites.map((site) => { site.siteUrl = site.siteUrl.replace(/^https?:\/\//, ''); site.badge = 0; this.pushNotificationsProvider.getSiteCounter(site.id).then((counter) => { @@ -57,24 +57,6 @@ export class CoreLoginSitesPage { return site; }); - // Sort sites by url and fullname. - this.sites = sites.sort((a, b) => { - // First compare by site url without the protocol. - let compareA = a.siteUrl.toLowerCase(), - compareB = b.siteUrl.toLowerCase(); - const compare = compareA.localeCompare(compareB); - - if (compare !== 0) { - return compare; - } - - // If site url is the same, use fullname instead. - compareA = a.fullName.toLowerCase().trim(); - compareB = b.fullName.toLowerCase().trim(); - - return compareA.localeCompare(compareB); - }); - this.showDelete = false; }).catch(() => { // Shouldn't happen. diff --git a/src/core/settings/lang/en.json b/src/core/settings/lang/en.json index 0999dce8c..00ac951bd 100644 --- a/src/core/settings/lang/en.json +++ b/src/core/settings/lang/en.json @@ -1,12 +1,49 @@ { "about": "About", + "appready": "App ready", + "cannotsyncoffline": "Cannot synchronise offline.", + "cannotsyncwithoutwifi": "Cannot synchronise because the current settings only allow to synchronise when connected to Wi-Fi. Please connect to a Wi-Fi network.", + "cordovadevicemodel": "Cordova device model", + "cordovadeviceosversion": "Cordova device OS version", + "cordovadeviceplatform": "Cordova device platform", + "cordovadeviceuuid": "Cordova device UUID", + "cordovaversion": "Cordova version", + "currentlanguage": "Current language", + "deletesitefiles": "Are you sure that you want to delete the downloaded files from the site '{{sitename}}'?", + "deletesitefilestitle": "Delete site files", + "deviceinfo": "Device info", + "deviceos": "Device OS", + "devicewebworkers": "Device web workers supported", "disableall": "Disable notifications", "disabled": "Disabled", + "displayformat": "Display format", + "enablerichtexteditor": "Enable text editor", + "enablerichtexteditordescription": "If enabled, a text editor will be available when entering content.", + "enablesyncwifi": "Allow sync only when on Wi-Fi", + "errordeletesitefiles": "Error deleting site files.", + "errorsyncsite": "Error synchronising site data. Please check your Internet connection and try again.", + "estimatedfreespace": "Estimated free space", + "filesystemroot": "File system root", "general": "General", + "language": "Language", + "license": "License", + "localnotifavailable": "Local notifications available", + "locationhref": "Web view URL", "loggedin": "Online", "loggedoff": "Offline", + "navigatorlanguage": "Navigator language", + "navigatoruseragent": "Navigator userAgent", + "networkstatus": "Internet connection status", + "privacypolicy": "Privacy policy", + "reportinbackground": "Report errors automatically", "settings": "Settings", "sites": "Sites", "spaceusage": "Space usage", - "synchronization": "Synchronisation" + "synchronization": "Synchronisation", + "synchronizenow": "Synchronise now", + "syncsettings": "Synchronisation settings", + "total": "Total", + "versioncode": "Version code", + "versionname": "Version name", + "wificonnection": "Wi-Fi connection" } diff --git a/src/core/settings/pages/about/about.html b/src/core/settings/pages/about/about.html new file mode 100644 index 000000000..b05a037ec --- /dev/null +++ b/src/core/settings/pages/about/about.html @@ -0,0 +1,109 @@ + + + {{ 'core.settings.about' | translate }} + + + + +

{{ appName }} {{ versionName }}

+
+ + + {{ 'core.settings.license' | translate }} + + +

Apache 2.0

+

http://www.apache.org/licenses/LICENSE-2.0

+
+
+ + + {{ 'core.settings.privacypolicy' | translate }} + + +

{{ privacyPolicy }}

+
+
+ + + {{ 'core.settings.deviceinfo' | translate }} + + +

{{ 'core.settings.versionname' | translate}}

+

{{ versionName }}

+
+ +

{{ 'core.settings.versioncode' | translate}}

+

{{ versionCode }}

+
+ +

{{ 'core.settings.filesystemroot' | translate}}

+

{{ filesystemroot }}

+

{{ fileSystemRoot }}

+
+ +

{{ 'core.settings.navigatoruseragent' | translate}}

+

{{ navigator.userAgent }}

+
+ +

{{ 'core.settings.navigatorlanguage' | translate}}

+

{{ navigator.language }}

+
+ +

{{ 'core.settings.locationhref' | translate}}

+

{{ locationHref }}

+
+ +

{{ 'core.settings.appready' | translate}}

+

{{ appReady | translate }}

+
+ +

{{ 'core.settings.displayformat' | translate}}

+

{{ deviceType | translate }}

+
+ +

{{ 'core.settings.deviceos' | translate}}

+

{{ deviceOs | translate }}

+
+ +

{{ 'core.settings.currentlanguage' | translate}}

+

{{ currentLanguage }}

+
+ +

{{ 'core.settings.networkstatus' | translate}}

+

{{ networkStatus | translate }}

+
+ +

{{ 'core.settings.wificonnection' | translate}}

+

{{ wifiConnection | translate }}

+
+ +

{{ 'core.settings.devicewebworkers' | translate}}

+

{{ deviceWebWorkers | translate }}

+
+ +

{{ 'core.settings.cordovaversion' | translate}}

+

{{ device.cordova }}

+
+ +

{{ 'core.settings.cordovadeviceplatform' | translate}}

+

{{ device.platform }}

+
+ +

{{ 'core.settings.cordovadeviceosversion' | translate}}

+

{{ device.version }}

+
+ +

{{ 'core.settings.cordovadevicemodel' | translate}}

+

{{ device.model }}

+
+ +

{{ 'core.settings.cordovadeviceuuid' | translate}}

+

{{ device.uuid }}

+
+ +

{{ 'core.settings.localnotifavailable' | translate}}

+

{{ localNotifAvailable | translate }}

+
+
+
diff --git a/src/core/settings/pages/about/about.module.ts b/src/core/settings/pages/about/about.module.ts new file mode 100644 index 000000000..1daf53329 --- /dev/null +++ b/src/core/settings/pages/about/about.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreSettingsAboutPage } from './about'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + CoreSettingsAboutPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreSettingsAboutPage), + TranslateModule.forChild() + ], +}) +export class CoreSettingsAboutPageModule {} diff --git a/src/core/settings/pages/about/about.ts b/src/core/settings/pages/about/about.ts new file mode 100644 index 000000000..3dd531962 --- /dev/null +++ b/src/core/settings/pages/about/about.ts @@ -0,0 +1,105 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, } from '@angular/core'; +import { IonicPage, Platform } from 'ionic-angular'; +import { Device } from '@ionic-native/device'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFileProvider } from '@providers/file'; +import { CoreInitDelegate } from '@providers/init'; +import { CoreLangProvider } from '@providers/lang'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreConfigConstants } from '../../../../configconstants'; + +/** + * Page that displays the about settings. + */ +@IonicPage({segment: 'core-settings-about'}) +@Component({ + selector: 'page-core-settings-about', + templateUrl: 'about.html', +}) +export class CoreSettingsAboutPage { + + appName: string; + versionName: string; + versionCode: number; + privacyPolicy: string; + navigator: Navigator; + locationHref: string; + appReady: string; + deviceType: string; + deviceOs: string; + currentLanguage: string; + networkStatus: string; + wifiConnection: string; + deviceWebWorkers: string; + device: Device; + fileSystemRoot: string; + fsClickable: boolean; + storageType: string; + localNotifAvailable: string; + + constructor(platform: Platform, device: Device, appProvider: CoreAppProvider, fileProvider: CoreFileProvider, + initDelegate: CoreInitDelegate, langProvider: CoreLangProvider, + localNotificationsProvider: CoreLocalNotificationsProvider) { + + this.appName = appProvider.isDesktop() ? CoreConfigConstants.desktopappname : CoreConfigConstants.appname; + this.versionName = CoreConfigConstants.versionname; + this.versionCode = CoreConfigConstants.versioncode; + this.privacyPolicy = CoreConfigConstants.privacypolicy; + + this.navigator = window.navigator; + if (window.location && window.location.href) { + const url = window.location.href; + this.locationHref = url.substr(0, url.indexOf('#')); + } + + this.appReady = initDelegate.isReady() ? 'core.yes' : 'core.no'; + this.deviceType = platform.is('tablet') ? 'core.tablet' : 'core.phone'; + + if (platform.is('android')) { + this.deviceOs = 'core.android'; + } else if (platform.is('ios')) { + this.deviceOs = 'core.ios'; + } else if (platform.is('windows')) { + this.deviceOs = 'core.windowsphone'; + } else { + const matches = navigator.userAgent.match(/\(([^\)]*)\)/); + if (matches && matches.length > 1) { + this.deviceOs = matches[1]; + } else { + this.deviceOs = 'core.unknown'; + } + } + + langProvider.getCurrentLanguage().then((lang) => { + this.currentLanguage = lang; + }); + + this.networkStatus = appProvider.isOnline() ? 'core.online' : 'core.offline'; + this.wifiConnection = appProvider.isNetworkAccessLimited() ? 'core.no' : 'core.yes'; + this.deviceWebWorkers = !!window['Worker'] && !!window['URL'] ? 'core.yes' : 'core.no'; + this.device = device; + + if (fileProvider.isAvailable()) { + fileProvider.getBasePath().then((basepath) => { + this.fileSystemRoot = basepath; + this.fsClickable = fileProvider.usesHTMLAPI(); + }); + } + + this.localNotifAvailable = localNotificationsProvider.isAvailable() ? 'core.yes' : 'core.no'; + } +} diff --git a/src/core/settings/pages/general/general.html b/src/core/settings/pages/general/general.html new file mode 100644 index 000000000..ea82aa063 --- /dev/null +++ b/src/core/settings/pages/general/general.html @@ -0,0 +1,24 @@ + + + {{ 'core.settings.general' | translate }} + + + + +

{{ 'core.settings.language' | translate }}

+ + {{ languages[code] }} + +
+ + +

{{ 'core.settings.enablerichtexteditor' | translate }}

+

{{ 'core.settings.enablerichtexteditordescription' | translate }}

+
+ +
+ +

{{ 'core.settings.reportinbackground' | translate }}

+ +
+
diff --git a/src/core/settings/pages/general/general.module.ts b/src/core/settings/pages/general/general.module.ts new file mode 100644 index 000000000..3a3125219 --- /dev/null +++ b/src/core/settings/pages/general/general.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreSettingsGeneralPage } from './general'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + CoreSettingsGeneralPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreSettingsGeneralPage), + TranslateModule.forChild() + ], +}) +export class CoreSettingsGeneralPageModule {} diff --git a/src/core/settings/pages/general/general.ts b/src/core/settings/pages/general/general.ts new file mode 100644 index 000000000..27c7d1e7e --- /dev/null +++ b/src/core/settings/pages/general/general.ts @@ -0,0 +1,93 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreConstants } from '@core/constants'; +import { CoreConfigProvider } from '@providers/config'; +import { CoreFileProvider } from '@providers/file'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLangProvider } from '@providers/lang'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; +import { CoreConfigConstants } from '../../../../configconstants'; + +/** + * Page that displays the general settings. + */ +@IonicPage({segment: 'core-settings-general'}) +@Component({ + selector: 'page-core-settings-general', + templateUrl: 'general.html', +}) +export class CoreSettingsGeneralPage { + + languages = {}; + languageCodes = []; + selectedLanguage: string; + rteSupported: boolean; + richTextEditor: boolean; + showReport: boolean; + reportInBackground: boolean; + + constructor(appProvider: CoreAppProvider, private configProvider: CoreConfigProvider, fileProvider: CoreFileProvider, + private eventsProvider: CoreEventsProvider, private langProvider: CoreLangProvider, + private domUtils: CoreDomUtilsProvider, + localNotificationsProvider: CoreLocalNotificationsProvider) { + + this.languages = CoreConfigConstants.languages; + this.languageCodes = Object.keys(this.languages); + langProvider.getCurrentLanguage().then((currentLanguage) => { + this.selectedLanguage = currentLanguage; + }); + + this.rteSupported = this.domUtils.isRichTextEditorSupported(); + if (this.rteSupported) { + this.configProvider.get(CoreConstants.SETTINGS_RICH_TEXT_EDITOR, true).then((richTextEditorEnabled) => { + this.richTextEditor = richTextEditorEnabled; + }); + } + + if (localStorage && localStorage.getItem && localStorage.setItem) { + this.showReport = true; + this.reportInBackground = parseInt(localStorage.getItem(CoreConstants.SETTINGS_REPORT_IN_BACKGROUND), 10) === 1; + } else { + this.showReport = false; + } + } + + /** + * Called when a new language is selected. + */ + languageChanged(): void { + this.langProvider.changeCurrentLanguage(this.selectedLanguage).finally(() => { + this.eventsProvider.trigger(CoreEventsProvider.LANGUAGE_CHANGED); + }); + } + + /** + * Called when the rich text editor is enabled or disabled. + */ + richTextEditorChanged(): void { + this.configProvider.set(CoreConstants.SETTINGS_RICH_TEXT_EDITOR, this.richTextEditor); + } + + /** + * Called when the report in background setting is enabled or disabled. + */ + reportInBackgroundChanged(): void { + localStorage.setItem(CoreConstants.SETTINGS_REPORT_IN_BACKGROUND, this.reportInBackground ? '1' : '0'); + } +} diff --git a/src/core/settings/pages/list/list.html b/src/core/settings/pages/list/list.html index d24df5cb8..6081ea1e7 100644 --- a/src/core/settings/pages/list/list.html +++ b/src/core/settings/pages/list/list.html @@ -16,7 +16,7 @@

{{ 'core.settings.spaceusage' | translate }}

- +

{{ 'core.settings.synchronization' | translate }}

diff --git a/src/core/settings/pages/space-usage/space-usage.html b/src/core/settings/pages/space-usage/space-usage.html new file mode 100644 index 000000000..bf32f7e80 --- /dev/null +++ b/src/core/settings/pages/space-usage/space-usage.html @@ -0,0 +1,28 @@ + + + {{ 'core.settings.spaceusage' | translate }} + + + + + + + + +

+

{{ site.fullName }}

+

{{ site.spaceUsage | coreBytesToSize }}

+ +
+ +

{{ 'core.settings.total' | translate }}

+

{{ totalUsage | coreBytesToSize }}

+
+ +

{{ 'core.settings.estimatedfreespace' | translate }}

+

{{ freeSpace | coreBytesToSize }}

+
+
+
diff --git a/src/core/settings/pages/space-usage/space-usage.module.ts b/src/core/settings/pages/space-usage/space-usage.module.ts new file mode 100644 index 000000000..07e590a91 --- /dev/null +++ b/src/core/settings/pages/space-usage/space-usage.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreSettingsSpaceUsagePage } from './space-usage'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; + +@NgModule({ + declarations: [ + CoreSettingsSpaceUsagePage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(CoreSettingsSpaceUsagePage), + TranslateModule.forChild() + ], +}) +export class CoreSettingsSpaceUsagePageModule {} diff --git a/src/core/settings/pages/space-usage/space-usage.ts b/src/core/settings/pages/space-usage/space-usage.ts new file mode 100644 index 000000000..c24ba88b6 --- /dev/null +++ b/src/core/settings/pages/space-usage/space-usage.ts @@ -0,0 +1,180 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreFileProvider } from '@providers/file'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; + +/** + * Page that displays the space usage settings. + */ +@IonicPage({segment: 'core-settings-space-usage'}) +@Component({ + selector: 'page-core-settings-space-usage', + templateUrl: 'space-usage.html', +}) +export class CoreSettingsSpaceUsagePage { + + usageLoaded = false; + sites = []; + currentSiteId = ''; + totalUsage = 0; + freeSpace = 0; + + constructor(private fileProvider: CoreFileProvider, private filePoolProvider: CoreFilepoolProvider, + private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, + private translate: TranslateService, private domUtils: CoreDomUtilsProvider) { + this.currentSiteId = this.sitesProvider.getCurrentSiteId(); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.fetchData().finally(() => { + this.usageLoaded = true; + }); + } + + /** + * Convenience function to calculate each site's usage, and the total usage. + * + * @return {Promise} Resolved when done. + */ + protected calculateSizeUsage(): Promise { + return this.sitesProvider.getSortedSites().then((sites) => { + this.sites = sites; + + // Get space usage. + const promises = this.sites.map((siteEntry) => { + return this.sitesProvider.getSite(siteEntry.id).then((site) => { + return site.getSpaceUsage().then((size) => { + siteEntry.spaceUsage = size; + }); + }); + }); + + return Promise.all(promises); + }); + } + + /** + * Convenience function to calculate total usage. + */ + protected calculateTotalUsage(): void { + let total = 0; + this.sites.forEach((site) => { + if (site.spaceUsage) { + total += parseInt(site.spaceUsage, 10); + } + }); + this.totalUsage = total; + } + + /** + * Convenience function to calculate free space in the device. + * + * @return {Promise} Resolved when done. + */ + protected calculateFreeSpace(): Promise { + if (this.fileProvider.isAvailable()) { + return this.fileProvider.calculateFreeSpace().then((freeSpace) => { + this.freeSpace = freeSpace; + }).catch(() => { + this.freeSpace = 0; + }); + } else { + this.freeSpace = 0; + + return Promise.resolve(null); + } + } + + /** + * Convenience function to calculate space usage and free space in the device. + * + * @return {Promise} Resolved when done. + */ + protected fetchData(): Promise { + return Promise.all([ + this.calculateSizeUsage().then(() => this.calculateTotalUsage()), + this.calculateFreeSpace(), + ]); + } + + /** + * Refresh the data. + * + * @param {any} refresher Refresher. + */ + refreshData(refresher: any): void { + this.fetchData().finally(() => { + refresher.complete(); + }); + } + + /** + * Convenience function to update site size, along with total usage and free space. + * + * @param {any} site Site object with space usage. + * @param {number} newUsage New space usage of the site in bytes. + */ + protected updateSiteUsage(site: any, newUsage: number): void { + const oldUsage = site.spaceUsage; + site.spaceUsage = newUsage; + this.totalUsage -= oldUsage - newUsage; + this.freeSpace += oldUsage - newUsage; + } + + /** + * Deletes files of a site. + * + * @param {any} siteData Site object with space usage. + */ + deleteSiteFiles(siteData: any): void { + this.textUtils.formatText(siteData.siteName).then((siteName) => { + const title = this.translate.instant('core.settings.deletesitefilestitle'); + const message = this.translate.instant('core.settings.deletesitefiles', {sitename: siteName}); + + this.domUtils.showConfirm(message, title).then(() => { + return this.sitesProvider.getSite(siteData.id); + }).then((site) => { + site.deleteFolder().then(() => { + this.filePoolProvider.clearAllPackagesStatus(site.id); + this.filePoolProvider.clearFilepool(site.id); + this.updateSiteUsage(siteData, 0); + }).catch((error) => { + if (error && error.code === FileError.NOT_FOUND_ERR) { + // Not found, set size 0. + this.filePoolProvider.clearAllPackagesStatus(site.id); + this.updateSiteUsage(siteData, 0); + } else { + // Error, recalculate the site usage. + this.domUtils.showErrorModal('core.settings.errordeletesitefiles', true); + site.getSpaceUsage().then((size) => { + this.updateSiteUsage(siteData, size); + }); + } + }); + }).catch(() => { + // Ignore cancelled confirmation modal. + }); + }); + } +} diff --git a/src/core/settings/pages/synchronization/synchronization.html b/src/core/settings/pages/synchronization/synchronization.html new file mode 100644 index 000000000..b3a5338f5 --- /dev/null +++ b/src/core/settings/pages/synchronization/synchronization.html @@ -0,0 +1,29 @@ + + + {{ 'core.settings.synchronization' | translate }} + + + + + +

{{ 'core.settings.syncsettings' | translate }}

+
+ + {{ 'core.settings.enablesyncwifi' | translate }} + + + + +

{{ 'core.settings.sites' | translate }}

+
+ +

+

{{ site.fullName }}

+

{{ site.siteUrl }}

+ + +
+
+
diff --git a/src/core/settings/pages/synchronization/synchronization.module.ts b/src/core/settings/pages/synchronization/synchronization.module.ts new file mode 100644 index 000000000..e159f4800 --- /dev/null +++ b/src/core/settings/pages/synchronization/synchronization.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreSettingsSynchronizationPage } from './synchronization'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; + +@NgModule({ + declarations: [ + CoreSettingsSynchronizationPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(CoreSettingsSynchronizationPage), + TranslateModule.forChild() + ], +}) +export class CoreSettingsSynchronizationPageModule {} diff --git a/src/core/settings/pages/synchronization/synchronization.ts b/src/core/settings/pages/synchronization/synchronization.ts new file mode 100644 index 000000000..19be97a5d --- /dev/null +++ b/src/core/settings/pages/synchronization/synchronization.ts @@ -0,0 +1,113 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnDestroy } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreConstants } from '@core/constants'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider, CoreSiteBasicInfo } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreConfigProvider } from '@providers/config'; +import { CoreSettingsHelper } from '@core/settings/providers/helper'; + +/** + * Page that displays the synchronization settings. + */ +@IonicPage({segment: 'core-settings-synchronization'}) +@Component({ + selector: 'page-core-settings-synchronization', + templateUrl: 'synchronization.html', +}) +export class CoreSettingsSynchronizationPage implements OnDestroy { + + sites: CoreSiteBasicInfo[] = []; + sitesLoaded = false; + sitesObserver: any; + currentSiteId = ''; + syncOnlyOnWifi = false; + isDestroyed = false; + + constructor(private configProvider: CoreConfigProvider, private eventsProvider: CoreEventsProvider, + private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, + private settingsHelper: CoreSettingsHelper) { + + this.currentSiteId = this.sitesProvider.getCurrentSiteId(); + + this.sitesObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, (data) => { + this.sitesProvider.getSite(data.siteId).then((site) => { + const siteInfo = site.getInfo(); + const siteEntry = this.sites.find((siteEntry) => siteEntry.id == site.id); + if (siteEntry) { + siteEntry.siteUrl = siteInfo.siteurl; + siteEntry.siteName = siteInfo.sitename; + siteEntry.fullName = siteInfo.fullname; + } + }); + }); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.sitesProvider.getSortedSites().then((sites) => { + this.sites = sites; + }).finally(() => { + this.sitesLoaded = true; + }); + + this.configProvider.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, true).then((syncOnlyOnWifi) => { + this.syncOnlyOnWifi = syncOnlyOnWifi; + }); + } + + /** + * Called when sync only on wifi setting is enabled or disabled. + */ + syncOnlyOnWifiChanged(): void { + this.configProvider.set(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, this.syncOnlyOnWifi); + } + + /** + * Syncrhonizes a site. + * + * @param {string} siteId Site ID. + */ + synchronize(siteId: string): void { + this.settingsHelper.synchronizeSite(this.syncOnlyOnWifi, siteId).catch((error) => { + if (this.isDestroyed) { + return; + } + this.domUtils.showErrorModalDefault(error, 'core.settings.errorsyncsite', true); + }); + } + + /** + * Returns true if site is beeing synchronized. + * + * @param {string} siteId Site ID. + * @return {boolean} True if site is beeing synchronized, false otherwise. + */ + isSynchronizing(siteId: string): boolean { + return !!this.settingsHelper.getSiteSyncPromise(siteId); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + this.sitesObserver && this.sitesObserver.off(); + } +} diff --git a/src/core/settings/providers/helper.ts b/src/core/settings/providers/helper.ts index c4fb77b71..0a2d5c32f 100644 --- a/src/core/settings/providers/helper.ts +++ b/src/core/settings/providers/helper.ts @@ -13,8 +13,14 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreCronDelegate } from '@providers/cron'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { TranslateService } from '@ngx-translate/core'; /** * Settings helper service. @@ -22,8 +28,11 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; @Injectable() export class CoreSettingsHelper { protected logger; + protected syncPromises = {}; - constructor(loggerProvider: CoreLoggerProvider, private utils: CoreUtilsProvider) { + constructor(loggerProvider: CoreLoggerProvider, private appProvider: CoreAppProvider, private cronDelegate: CoreCronDelegate, + private eventsProvider: CoreEventsProvider, private filePoolProvider: CoreFilepoolProvider, + private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private translate: TranslateService) { this.logger = loggerProvider.getInstance('CoreSettingsHelper'); } @@ -91,4 +100,79 @@ export class CoreSettingsHelper { return result; } + + /** + * Get the synchronization promise of a site. + * + * @param {string} siteId ID of the site. + * @return {Promise | null} Sync promise or null if site is not being syncrhonized. + */ + getSiteSyncPromise(siteId: string): Promise { + if (this.syncPromises[siteId]) { + return this.syncPromises[siteId]; + } else { + return null; + } + } + + /** + * Synchronize a site. + * + * @param {boolean} syncOnlyOnWifi True to sync only on wifi, false otherwise. + * @param {string} siteId ID of the site to synchronize. + * @return {Promise} Promise resolved when synchronized, rejected if failure. + */ + synchronizeSite(syncOnlyOnWifi: boolean, siteId: string): Promise { + if (this.syncPromises[siteId]) { + // There's already a sync ongoing for this site, return the promise. + return this.syncPromises[siteId]; + } + + const promises = []; + const hasSyncHandlers = this.cronDelegate.hasManualSyncHandlers(); + + if (hasSyncHandlers && !this.appProvider.isOnline()) { + // We need connection to execute sync. + return Promise.reject(this.translate.instant('core.settings.cannotsyncoffline')); + } else if (hasSyncHandlers && syncOnlyOnWifi && this.appProvider.isNetworkAccessLimited()) { + return Promise.reject(this.translate.instant('core.settings.cannotsyncwithoutwifi')); + } + + // Invalidate all the site files so they are re-downloaded. + promises.push(this.filePoolProvider.invalidateAllFiles(siteId).catch(() => { + // Ignore errors. + })); + + // Get the site to invalidate data. + promises.push(this.sitesProvider.getSite(siteId).then((site) => { + // Invalidate the WS cache. + return site.invalidateWsCache().then(() => { + const subPromises = []; + + // Check if local_mobile was installed in Moodle. + subPromises.push(site.checkIfLocalMobileInstalledAndNotUsed().then(() => { + // Local mobile was added. Throw invalid session to force reconnect and create a new token. + this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, {}, siteId); + + return Promise.reject(this.translate.instant('core.lostconnection')); + }, () => { + // Update site info. + return this.sitesProvider.updateSiteInfo(siteId); + })); + + // Execute cron if needed. + subPromises.push(this.cronDelegate.forceSyncExecution(siteId)); + + return Promise.all(subPromises); + }); + })); + + const syncPromise = Promise.all(promises); + this.syncPromises[siteId] = syncPromise; + syncPromise.finally(() => { + delete this.syncPromises[siteId]; + }); + + return syncPromise; + } } diff --git a/src/providers/sites.ts b/src/providers/sites.ts index ac30cd79f..8b9694b15 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -870,6 +870,36 @@ export class CoreSitesProvider { }); } + /** + * Get the list of sites stored, sorted by URL and full name. + * + * @param {String[]} [ids] IDs of the sites to get. If not defined, return all sites. + * @return {Promise} Promise resolved when the sites are retrieved. + */ + getSortedSites(ids?: string[]): Promise { + return this.getSites(ids).then((sites) => { + // Sort sites by url and ful lname. + sites.sort((a, b) => { + // First compare by site url without the protocol. + let compareA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase(), + compareB = b.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); + const compare = compareA.localeCompare(compareB); + + if (compare !== 0) { + return compare; + } + + // If site url is the same, use fullname instead. + compareA = a.fullName.toLowerCase().trim(); + compareB = b.fullName.toLowerCase().trim(); + + return compareA.localeCompare(compareB); + }); + + return sites; + }); + } + /** * Get the list of IDs of sites stored. *