Merge pull request #1360 from dpalou/MOBILE-2431

Mobile 2431
main
Juan Leyva 2018-06-21 09:20:12 +02:00 committed by GitHub
commit 409d1bac3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 142 additions and 49 deletions

View File

@ -35,6 +35,7 @@
<preference name="SplashScreen" value="screen" />
<preference name="SplashScreenDelay" value="15000" />
<preference name="SplashMaintainAspectRatio" value="true" />
<preference name="LoadUrlTimeoutValue" value="60000" />
<feature name="StatusBar">
<param name="ios-package" onload="true" value="CDVStatusBar" />
</feature>
@ -90,8 +91,6 @@
<splash height="960" src="resources/ios/splash/Default@2x~iphone.png" width="640" />
<splash height="480" src="resources/ios/splash/Default~iphone.png" width="320" />
</platform>
<engine name="android" spec="6.1.2" />
<engine name="ios" spec="4.3.1" />
<plugin name="cordova-plugin-file" spec="4.3.3" />
<plugin name="cordova-plugin-file-transfer" spec="1.6.3" />
<plugin name="cordova-plugin-camera" spec="2.4.1">
@ -124,4 +123,7 @@
<plugin name="cordova-plugin-file-opener2" spec="~2.0.19" />
<plugin name="com-darryncampbell-cordova-plugin-intent" spec="~1.0.2" />
<plugin name="cordova-sqlite-evcore-extbuild-free" spec="~0.9.7" />
<plugin name="cordova-plugin-badge" spec="0.8.7" />
<engine name="android" spec="6.1.2" />
<engine name="ios" spec="4.3.1" />
</widget>

View File

@ -82,7 +82,9 @@
"rxjs": "5.5.11",
"sw-toolbox": "3.6.0",
"ts-md5": "^1.2.2",
"zone.js": "0.8.26"
"zone.js": "0.8.26",
"cordova-android": "6.1.2",
"cordova-ios": "4.3.1"
},
"devDependencies": {
"@ionic/app-scripts": "3.1.9",
@ -97,5 +99,11 @@
},
"browser": {
"electron": false
},
"cordova": {
"platforms": [
"android",
"ios"
]
}
}

View File

@ -58,7 +58,7 @@ export class AddonCalendarModule {
return;
}
loginHelper.redirect('AddonCalendarListPage', {eventid: data.eventid}, data.siteId);
loginHelper.redirect('AddonCalendarListPage', {eventId: data.eventid}, data.siteId);
});
});
}

View File

@ -78,7 +78,7 @@ export class AddonCalendarListPage implements OnDestroy {
}, sitesProvider.getCurrentSiteId());
}
this.eventId = navParams.get('eventid') || false;
this.eventId = navParams.get('eventId') || false;
}
/**

View File

@ -52,7 +52,6 @@ export interface AddonModAssignSyncResult {
export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_mod_assign_autom_synced';
static SYNC_TIME = 300000;
protected componentTranslate: string;

View File

@ -42,6 +42,6 @@ export class AddonModAssignSyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return 600000; // 10 minutes.
return this.assignSync.syncInterval;
}
}

View File

@ -42,6 +42,6 @@ export class AddonModChoiceSyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return 600000; // 10 minutes.
return this.choiceSync.syncInterval;
}
}

View File

@ -77,7 +77,7 @@ export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider {
// Sync all responses.
responses.forEach((response) => {
promises.push(this.syncChoice(response.choiceid, response.userid, siteId).then((result) => {
promises.push(this.syncChoiceIfNeeded(response.choiceid, response.userid, siteId).then((result) => {
if (result && result.updated) {
// Sync successful, send event.
this.eventsProvider.trigger(AddonModChoiceSyncProvider.AUTO_SYNCED, {
@ -91,6 +91,24 @@ export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider {
});
}
/**
* Sync an choice only if a certain time has passed since the last time.
*
* @param {number} choiceId Choice ID to be synced.
* @param {number} userId User the answers belong to.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the choice is synced or it doesn't need to be synced.
*/
syncChoiceIfNeeded(choiceId: number, userId: number, siteId?: string): Promise<any> {
const syncId = this.getSyncId(choiceId, userId);
return this.isSyncNeeded(syncId, siteId).then((needed) => {
if (needed) {
return this.syncChoice(choiceId, userId, siteId);
}
});
}
/**
* Synchronize a choice.
*

View File

@ -42,6 +42,6 @@ export class AddonModDataSyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return 600000; // 10 minutes.
return this.dataSync.syncInterval;
}
}

View File

@ -42,6 +42,6 @@ export class AddonModFeedbackSyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return 600000; // 10 minutes.
return this.feedbackSync.syncInterval;
}
}

View File

@ -42,6 +42,6 @@ export class AddonModForumSyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return AddonModForumSyncProvider.SYNC_TIME;
return this.forumSync.syncInterval;
}
}

View File

@ -36,7 +36,6 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_mod_forum_autom_synced';
static MANUAL_SYNCED = 'addon_mod_forum_manual_synced';
static SYNC_TIME = 600000;
protected componentTranslate: string;

View File

@ -42,6 +42,6 @@ export class AddonModGlossarySyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return AddonModGlossarySyncProvider.SYNC_TIME;
return this.glossarySync.syncInterval;
}
}

View File

@ -35,7 +35,6 @@ import { AddonModGlossaryOfflineProvider } from './offline';
export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_mod_glossary_autom_synced';
static SYNC_TIME = 600000; // 10 minutes.
protected componentTranslate: string;

View File

@ -53,7 +53,6 @@ export interface AddonModLessonSyncResult {
export class AddonModLessonSyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_mod_lesson_autom_synced';
static SYNC_TIME = 300000;
protected componentTranslate: string;

View File

@ -42,6 +42,6 @@ export class AddonModLessonSyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return 600000;
return this.lessonSync.syncInterval;
}
}

View File

@ -26,7 +26,7 @@ export class AddonModQuizAccessOfflineAttemptsHandler implements AddonModQuizAcc
name = 'AddonModQuizAccessOfflineAttempts';
ruleName = 'quizaccess_offlineattempts';
constructor() {
constructor(protected quizSync: AddonModQuizSyncProvider) {
// Nothing to do.
}
@ -86,6 +86,6 @@ export class AddonModQuizAccessOfflineAttemptsHandler implements AddonModQuizAcc
}
// Show warning if last sync was a while ago.
return Date.now() - AddonModQuizSyncProvider.SYNC_TIME > quiz.syncTime;
return Date.now() - this.quizSync.syncInterval > quiz.syncTime;
}
}

View File

@ -52,7 +52,6 @@ export interface AddonModQuizSyncResult {
export class AddonModQuizSyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_mod_quiz_autom_synced';
static SYNC_TIME = 300000;
protected componentTranslate: string;

View File

@ -42,6 +42,6 @@ export class AddonModQuizSyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return AddonModQuizSyncProvider.SYNC_TIME;
return this.quizSync.syncInterval;
}
}

View File

@ -57,7 +57,6 @@ export interface AddonModScormSyncResult {
export class AddonModScormSyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_mod_scorm_autom_synced';
static SYNC_TIME = 600000;
protected componentTranslate: string;

View File

@ -42,6 +42,6 @@ export class AddonModScormSyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return AddonModScormSyncProvider.SYNC_TIME;
return this.scormSync.syncInterval;
}
}

View File

@ -42,6 +42,6 @@ export class AddonModSurveySyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return 600000; // 10 minutes.
return this.surveySync.syncInterval;
}
}

View File

@ -42,6 +42,6 @@ export class AddonModWikiSyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return 600000; // 10 minutes.
return this.wikiSync.syncInterval;
}
}

View File

@ -96,7 +96,6 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_mod_wiki_autom_synced';
static MANUAL_SYNCED = 'addon_mod_wiki_manual_synced';
static SYNC_TIME = 300000;
protected componentTranslate: string;

View File

@ -42,6 +42,6 @@ export class AddonModWorkshopSyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return AddonModWorkshopSyncProvider.SYNC_TIME;
return this.workshopSync.syncInterval;
}
}

View File

@ -35,7 +35,6 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_mod_workshop_autom_synced';
static MANUAL_SYNCED = 'addon_mod_workshop_manual_synced';
static SYNC_TIME = 300000;
protected componentTranslate: string;

View File

@ -42,6 +42,6 @@ export class AddonNotesSyncCronHandler implements CoreCronHandler {
* @return {number} Time between consecutive executions (in ms).
*/
getInterval(): number {
return 600000; // 10 minutes.
return 300000; // 5 minutes.
}
}

View File

@ -18,7 +18,7 @@
<!-- Input to enter the answer. -->
<ion-col>
<ion-input padding-left type="text" placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.input.name" [value]="question.input.value" [disabled]="question.input.readOnly" [ngClass]='{"core-question-answer-correct": question.input.isCorrect === 1, "core-question-answer-incorrect": question.input.isCorrect === 0}' autocorrect="off">
<ion-input padding-left type="text" placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.input.name" [value]="question.input.value" [disabled]="question.input.readOnly" [ngClass]="[question.input.correctClass]" autocorrect="off">
</ion-input>
</ion-col>
@ -40,7 +40,7 @@
<ng-template #selectUnits>
<ion-col>
<label *ngIf="question.select.accessibilityLabel" class="accesshide" for="{{question.select.id}}">{{ question.select.accessibilityLabel }}</label>
<ion-select id="{{question.select.id}}" [name]="question.select.name" [(ngModel)]="question.select.selected" interface="popover">
<ion-select id="{{question.select.id}}" [name]="question.select.name" [(ngModel)]="question.select.selected" interface="popover" [disabled]="question.select.disabled">
<ion-option *ngFor="let option of question.select.options" [value]="option.value">{{option.label}}</ion-option>
</ion-select>
@ -56,7 +56,7 @@
<ion-label>
<p>{{option.text}}</p>
</ion-label>
<ion-radio [value]="option.value" [disabled]="option.disabled"></ion-radio>
<ion-radio [value]="option.value" [disabled]="option.disabled || question.input.readOnly"></ion-radio>
</ion-item>
<!-- ion-radio doesn't use an input. Create a hidden input to hold the selected value. -->

View File

@ -2,6 +2,6 @@
<ion-item text-wrap>
<p><core-format-text [component]="component" [componentId]="componentId" [text]="question.text"></core-format-text></p>
</ion-item>
<ion-input padding-left type="text" placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.input.name" [value]="question.input.value" autocorrect="off" [disabled]="question.input.readOnly" [ngClass]='{"core-question-answer-correct": question.input.isCorrect === 1, "core-question-answer-incorrect": question.input.isCorrect === 0}'>
<ion-input padding-left type="text" placeholder="{{ 'core.question.answer' | translate }}" [attr.name]="question.input.name" [value]="question.input.value" autocorrect="off" [disabled]="question.input.readOnly" [ngClass]="[question.input.correctClass]">
</ion-input>
</section>

View File

@ -1,10 +1,10 @@
<ion-item text-wrap>
{{ 'core.maxsizeandattachments' | translate:{$a: {size: maxSizeReadable, attachments: maxSubmissionsReadable} } }}
</ion-item>
<ion-item text-wrap *ngIf="filetypes && filetypes.mimetypes && filetypes.mimetypes.length">
<ion-item text-wrap *ngIf="fileTypes && fileTypes.mimetypes && fileTypes.mimetypes.length">
<p>{{ 'core.fileuploader.filesofthesetypes' | translate }}</p>
<ul class="list-with-style">
<li *ngFor="let typeInfo of filetypes.info">
<li *ngFor="let typeInfo of fileTypes.info">
<strong *ngIf="typeInfo.name">{{typeInfo.name}} </strong>{{typeInfo.extlist}}
</li>
</ul>

View File

@ -47,7 +47,19 @@ export class CoreTabComponent implements OnInit, OnDestroy {
@Input() badge?: string; // A badge to add in the tab.
@Input() badgeStyle?: string; // The badge color.
@Input() enabled = true; // Whether the tab is enabled.
@Input() show = true; // Whether the tab should be shown.
@Input() set show(val: boolean) { // Whether the tab should be shown. Use a setter to detect changes on the value.
if (typeof val != 'undefined') {
const hasChanged = this._show != val;
this._show = val;
if (this.initialized && hasChanged) {
this.tabs.tabVisibilityChanged();
}
}
}
get show(): boolean { // Getter to be able to access "_show" just using .show.
return this._show;
}
@Input() id?: string; // An ID to identify the tab.
@Output() ionSelect: EventEmitter<CoreTabComponent> = new EventEmitter<CoreTabComponent>();
@ -56,6 +68,8 @@ export class CoreTabComponent implements OnInit, OnDestroy {
element: HTMLElement; // The core-tab element.
loaded = false;
initialized = false;
_show = true;
constructor(protected tabs: CoreTabsComponent, element: ElementRef, protected domUtils: CoreDomUtilsProvider) {
this.element = element.nativeElement;
@ -66,6 +80,7 @@ export class CoreTabComponent implements OnInit, OnDestroy {
*/
ngOnInit(): void {
this.tabs.addTab(this);
this.initialized = true;
}
/**

View File

@ -237,10 +237,13 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
}, 0);
this.slidesShown = Math.min(this.maxSlides, this.numTabsShown);
this.slides.update();
this.slides.resize();
this.slideChanged();
setTimeout(() => {
this.slides.update();
this.slides.resize();
});
}
protected calculateMaxSlides(): void {
@ -357,4 +360,11 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
this.tabs = newTabs;
}
}
/**
* Function to call when the visibility of a tab has changed.
*/
tabVisibilityChanged(): void {
this.updateSlides();
}
}

View File

@ -160,11 +160,13 @@ export class CoreLoginHelperProvider {
return false;
}
const modal = this.domUtils.showModalLoading('core.login.authenticating', true);
let siteData: CoreLoginSSOData;
let siteData: CoreLoginSSOData,
modal;
// Wait for app to be ready.
this.initDelegate.ready().then(() => {
modal = this.domUtils.showModalLoading('core.login.authenticating', true);
return this.validateBrowserSSOLogin(url);
}).then((data) => {
siteData = data;

View File

@ -89,8 +89,10 @@ export class CoreMainMenuPage implements OnDestroy {
this.initialTab = 0;
for (let i = 0; i < this.tabs.length; i++) {
if (this.tabs[i].page == this.redirectPage) {
const tab = this.tabs[i];
if (tab.page == this.redirectPage) {
this.initialTab = i + 1;
tab.pageParams = Object.assign(tab.pageParams || {}, this.redirectParams);
break;
}
}

View File

@ -291,10 +291,12 @@ export class CoreQuestionBaseComponent {
};
// Check if question is marked as correct.
if (input.className.indexOf('incorrect') >= 0) {
this.question.input.isCorrect = 0;
} else if (input.className.indexOf('correct') >= 0) {
this.question.input.isCorrect = 1;
if (input.classList.contains('incorrect')) {
this.question.input.correctClass = 'core-question-incorrect';
} else if (input.classList.contains('correct')) {
this.question.input.correctClass = 'core-question-correct';
} else if (input.classList.contains('partiallycorrect')) {
this.question.input.correctClass = 'core-question-partiallycorrect';
}
}

View File

@ -609,7 +609,17 @@ export class CoreFileProvider {
return this.file.moveFile(commonPath, originalPath, commonPath, newPath);
} else {
return this.file.moveFile(this.basePath, originalPath, this.basePath, newPath);
return this.file.moveFile(this.basePath, originalPath, this.basePath, newPath).catch((error) => {
// The move can fail if the path has encoded characters. Try again if that's the case.
const decodedOriginal = decodeURI(originalPath),
decodedNew = decodeURI(newPath);
if (decodedOriginal != originalPath || decodedNew != newPath) {
return this.file.moveFile(this.basePath, decodedOriginal, this.basePath, decodedNew);
} else {
return Promise.reject(error);
}
});
}
});
}
@ -645,7 +655,17 @@ export class CoreFileProvider {
return this.file.copyFile(fromDir, fromFileAndDir.name, toDir, toFileAndDir.name);
} else {
return this.file.copyFile(this.basePath, from, this.basePath, to);
return this.file.copyFile(this.basePath, from, this.basePath, to).catch((error) => {
// The copy can fail if the path has encoded characters. Try again if that's the case.
const decodedFrom = decodeURI(from),
decodedTo = decodeURI(to);
if (from != decodedFrom || to != decodedTo) {
return this.file.copyFile(this.basePath, decodedFrom, this.basePath, decodedTo);
} else {
return Promise.reject(error);
}
});
}
});
}

View File

@ -18,6 +18,7 @@ import { LocalNotifications, ILocalNotification } from '@ionic-native/local-noti
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from './app';
import { CoreConfigProvider } from './config';
import { CoreEventsProvider } from './events';
import { CoreLoggerProvider } from './logger';
import { CoreTextUtilsProvider } from './utils/text';
import { CoreUtilsProvider } from './utils/utils';
@ -117,10 +118,31 @@ export class CoreLocalNotificationsProvider {
constructor(logger: CoreLoggerProvider, private localNotifications: LocalNotifications, private platform: Platform,
private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private configProvider: CoreConfigProvider,
private textUtils: CoreTextUtilsProvider, private translate: TranslateService, private alertCtrl: AlertController) {
private textUtils: CoreTextUtilsProvider, private translate: TranslateService, private alertCtrl: AlertController,
eventsProvider: CoreEventsProvider) {
this.logger = logger.getInstance('CoreLocalNotificationsProvider');
this.appDB = appProvider.getDB();
this.appDB.createTablesFromSchema(this.tablesSchema);
localNotifications.on('trigger', (notification, state) => {
this.trigger(notification);
});
localNotifications.on('click', (notification, state) => {
if (notification && notification.data) {
this.logger.debug('Notification clicked: ', notification.data);
const data = textUtils.parseJSON(notification.data);
this.notifyClick(data);
}
});
eventsProvider.on(CoreEventsProvider.SITE_DELETED, (site) => {
if (site) {
this.cancelSiteNotifications(site.id);
}
});
}
/**