Merge pull request #2556 from crazyserver/MOBILE-3565

Mobile 3565
main
Dani Palou 2020-10-14 13:06:16 +02:00 committed by GitHub
commit f4681c1c64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1973 additions and 1527 deletions

5
package-lock.json generated
View File

@ -8574,6 +8574,11 @@
"integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==",
"dev": true
},
"font-awesome": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
"integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
},
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",

View File

@ -97,6 +97,7 @@
"cordova-support-google-services": "^1.2.1",
"cordova.plugins.diagnostic": "^6.0.2",
"es6-promise-plugin": "^4.2.2",
"font-awesome": "^4.7.0",
"moment": "^2.29.0",
"nl.kingsquare.cordova.background-audio": "^1.0.1",
"phonegap-plugin-multidex": "^1.0.0",

View File

@ -63,7 +63,7 @@ export class CoreDelegate {
/**
* Set of promises to update a handler, to prevent doing the same operation twice.
*/
protected updatePromises: {[siteId: string]: {[name: string]: Promise<any>}} = {};
protected updatePromises: {[siteId: string]: {[name: string]: Promise<void>}} = {};
/**
* Whether handlers have been initialized.
@ -73,7 +73,7 @@ export class CoreDelegate {
/**
* Promise to wait for handlers to be initialized.
*/
protected handlersInitPromise: Promise<any>;
protected handlersInitPromise: Promise<void>;
/**
* Function to resolve the handlers init promise.
@ -136,7 +136,7 @@ export class CoreDelegate {
* @param params Parameters to pass to the function.
* @return Function returned value or default value.
*/
private execute(handler: any, fnName: string, params?: any[]): any {
private execute(handler: CoreDelegateHandler, fnName: string, params?: any[]): any {
if (handler && handler[fnName]) {
return handler[fnName].apply(handler, params);
} else if (this.defaultHandler && this.defaultHandler[fnName]) {
@ -243,7 +243,7 @@ export class CoreDelegate {
protected updateHandler(handler: CoreDelegateHandler, time: number): Promise<void> {
const siteId = CoreSites.instance.getCurrentSiteId();
const currentSite = CoreSites.instance.getCurrentSite();
let promise;
let promise: Promise<boolean>;
if (this.updatePromises[siteId] && this.updatePromises[siteId][handler.name]) {
// There's already an update ongoing for this handler, return the promise.
@ -252,18 +252,14 @@ export class CoreDelegate {
this.updatePromises[siteId] = {};
}
if (!CoreSites.instance.isLoggedIn()) {
promise = Promise.reject(null);
} else if (this.isFeatureDisabled(handler, currentSite)) {
if (!CoreSites.instance.isLoggedIn() || this.isFeatureDisabled(handler, currentSite)) {
promise = Promise.resolve(false);
} else {
promise = Promise.resolve(handler.isEnabled());
promise = handler.isEnabled().catch(() => false);
}
// Checks if the handler is enabled.
this.updatePromises[siteId][handler.name] = promise.catch(() => {
return false;
}).then((enabled: boolean) => {
this.updatePromises[siteId][handler.name] = promise.then((enabled: boolean) => {
// Check that site hasn't changed since the check started.
if (CoreSites.instance.getCurrentSiteId() === siteId) {
const key = handler[this.handlerNameProperty] || handler.name;
@ -298,9 +294,9 @@ export class CoreDelegate {
*
* @return Resolved when done.
*/
protected updateHandlers(): Promise<void> {
const promises = [],
now = Date.now();
protected async updateHandlers(): Promise<void> {
const promises = [];
const now = Date.now();
this.logger.debug('Updating handlers for current site.');
@ -311,21 +307,19 @@ export class CoreDelegate {
promises.push(this.updateHandler(this.handlers[name], now));
}
return Promise.all(promises).then(() => {
return true;
}, () => {
// Never reject.
return true;
}).then(() => {
try {
await Promise.all(promises);
} catch (e) {
// Never reject
}
// Verify that this call is the last one that was started.
if (this.isLastUpdateCall(now)) {
this.handlersInitialized = true;
this.handlersInitResolve();
// Verify that this call is the last one that was started.
if (this.isLastUpdateCall(now)) {
this.handlersInitialized = true;
this.handlersInitResolve();
this.updateData();
}
});
this.updateData();
}
}
/**
@ -335,6 +329,7 @@ export class CoreDelegate {
updateData(): any {
// To be overridden.
}
}
export interface CoreDelegateHandler {
@ -346,7 +341,8 @@ export interface CoreDelegateHandler {
/**
* Whether or not the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean>;
isEnabled(): Promise<boolean>;
}

View File

@ -32,7 +32,7 @@ export class CoreInterceptor implements HttpInterceptor {
*/
static serialize(obj: any, addNull?: boolean): string {
let query = '';
let fullSubName;
let fullSubName: string;
let subValue;
let innerObj;
@ -68,10 +68,11 @@ export class CoreInterceptor implements HttpInterceptor {
const newReq = req.clone({
headers: req.headers.set('Content-Type', 'application/x-www-form-urlencoded'),
body: typeof req.body == 'object' && String(req.body) != '[object File]' ?
CoreInterceptor.serialize(req.body) : req.body
CoreInterceptor.serialize(req.body) : req.body,
});
// Pass on the cloned request instead of the original request.
return next.handle(newReq);
}
}

View File

@ -92,7 +92,8 @@ export class CoreNativeToAngularHttpResponse<T> extends AngularHttpResponse<T> {
headers: new HttpHeaders(nativeResponse.headers),
status: nativeResponse.status,
statusText: HTTP_STATUS_MESSAGES[nativeResponse.status] || '',
url: nativeResponse.url || ''
url: nativeResponse.url || '',
});
}
}

View File

@ -53,6 +53,7 @@ export type CoreQueueRunnerAddOptions = {
* A queue to prevent having too many concurrent executions.
*/
export class CoreQueueRunner {
protected queue: {[id: string]: CoreQueueRunnerItem} = {};
protected orderedQueue: CoreQueueRunnerItem[] = [];
protected numberRunning = 0;
@ -140,4 +141,5 @@ export class CoreQueueRunner {
return item.deferred.promise;
}
}

View File

@ -80,4 +80,5 @@ export class CoreSingletonsFactory {
};
}
}

View File

@ -131,6 +131,7 @@ export interface SQLiteDBForeignKeySchema {
* this.db = new SQLiteDB('MyDB');
*/
export class SQLiteDB {
db: SQLiteObject;
promise: Promise<void>;
@ -240,10 +241,10 @@ export class SQLiteDB {
*
* @return Promise resolved when done.
*/
async close(): Promise<any> {
async close(): Promise<void> {
await this.ready();
return this.db.close();
await this.db.close();
}
/**
@ -253,7 +254,7 @@ export class SQLiteDB {
* @param conditions The conditions to build the where clause. Must not contain numeric indexes.
* @return Promise resolved with the count of records returned from the specified criteria.
*/
countRecords(table: string, conditions?: object): Promise<number> {
async countRecords(table: string, conditions?: SQLiteDBRecordValues): Promise<number> {
const selectAndParams = this.whereClause(conditions);
return this.countRecordsSelect(table, selectAndParams[0], selectAndParams[1]);
@ -268,7 +269,8 @@ export class SQLiteDB {
* @param countItem The count string to be used in the SQL call. Default is COUNT('x').
* @return Promise resolved with the count of records returned from the specified criteria.
*/
countRecordsSelect(table: string, select: string = '', params?: any, countItem: string = 'COUNT(\'x\')'): Promise<number> {
async countRecordsSelect(table: string, select: string = '', params?: SQLiteDBRecordValue[],
countItem: string = 'COUNT(\'x\')'): Promise<number> {
if (select) {
select = 'WHERE ' + select;
}
@ -285,14 +287,13 @@ export class SQLiteDB {
* @param params An array of sql parameters.
* @return Promise resolved with the count.
*/
countRecordsSql(sql: string, params?: any): Promise<number> {
return this.getFieldSql(sql, params).then((count) => {
if (typeof count != 'number' || count < 0) {
return 0;
}
async countRecordsSql(sql: string, params?: SQLiteDBRecordValue[]): Promise<number> {
const count = await this.getFieldSql(sql, params);
if (typeof count != 'number' || count < 0) {
return 0;
}
return count;
});
return count;
}
/**
@ -306,11 +307,11 @@ export class SQLiteDB {
* @param tableCheck Check constraint for the table.
* @return Promise resolved when success.
*/
createTable(name: string, columns: SQLiteDBColumnSchema[], primaryKeys?: string[], uniqueKeys?: string[][],
foreignKeys?: SQLiteDBForeignKeySchema[], tableCheck?: string): Promise<any> {
async createTable(name: string, columns: SQLiteDBColumnSchema[], primaryKeys?: string[], uniqueKeys?: string[][],
foreignKeys?: SQLiteDBForeignKeySchema[], tableCheck?: string): Promise<void> {
const sql = this.buildCreateTableSql(name, columns, primaryKeys, uniqueKeys, foreignKeys, tableCheck);
return this.execute(sql);
await this.execute(sql);
}
/**
@ -319,9 +320,8 @@ export class SQLiteDB {
* @param table Table schema.
* @return Promise resolved when success.
*/
createTableFromSchema(table: SQLiteDBTableSchema): Promise<any> {
return this.createTable(table.name, table.columns, table.primaryKeys, table.uniqueKeys,
table.foreignKeys, table.tableCheck);
async createTableFromSchema(table: SQLiteDBTableSchema): Promise<void> {
await this.createTable(table.name, table.columns, table.primaryKeys, table.uniqueKeys, table.foreignKeys, table.tableCheck);
}
/**
@ -330,13 +330,13 @@ export class SQLiteDB {
* @param tables List of table schema.
* @return Promise resolved when success.
*/
createTablesFromSchema(tables: SQLiteDBTableSchema[]): Promise<any> {
async createTablesFromSchema(tables: SQLiteDBTableSchema[]): Promise<void> {
const promises = [];
tables.forEach((table) => {
promises.push(this.createTableFromSchema(table));
});
return Promise.all(promises);
await Promise.all(promises);
}
/**
@ -345,12 +345,14 @@ export class SQLiteDB {
*
* @param table The table to delete from.
* @param conditions The conditions to build the where clause. Must not contain numeric indexes.
* @return Promise resolved when done.
* @return Promise resolved with the number of affected rows.
*/
deleteRecords(table: string, conditions?: object): Promise<any> {
async deleteRecords(table: string, conditions?: SQLiteDBRecordValues): Promise<number> {
if (conditions === null || typeof conditions == 'undefined') {
// No conditions, delete the whole table.
return this.execute(`DELETE FROM ${table}`);
const result = await this.execute(`DELETE FROM ${table}`);
return result.rowsAffected;
}
const selectAndParams = this.whereClause(conditions);
@ -364,9 +366,9 @@ export class SQLiteDB {
* @param table The table to delete from.
* @param field The name of a field.
* @param values The values field might take.
* @return Promise resolved when done.
* @return Promise resolved with the number of affected rows.
*/
deleteRecordsList(table: string, field: string, values: any[]): Promise<any> {
async deleteRecordsList(table: string, field: string, values: SQLiteDBRecordValue[]): Promise<number> {
const selectAndParams = this.whereClauseList(field, values);
return this.deleteRecordsSelect(table, selectAndParams[0], selectAndParams[1]);
@ -378,14 +380,16 @@ export class SQLiteDB {
* @param table The table to delete from.
* @param select A fragment of SQL to be used in a where clause in the SQL call.
* @param params Array of sql parameters.
* @return Promise resolved when done.
* @return Promise resolved with the number of affected rows.
*/
deleteRecordsSelect(table: string, select: string = '', params?: any[]): Promise<any> {
async deleteRecordsSelect(table: string, select: string = '', params?: SQLiteDBRecordValue[]): Promise<number> {
if (select) {
select = 'WHERE ' + select;
}
return this.execute(`DELETE FROM ${table} ${select}`, params);
const result = await this.execute(`DELETE FROM ${table} ${select}`, params);
return result.rowsAffected;
}
/**
@ -394,8 +398,8 @@ export class SQLiteDB {
* @param name The table name.
* @return Promise resolved when success.
*/
dropTable(name: string): Promise<any> {
return this.execute(`DROP TABLE IF EXISTS ${name}`);
async dropTable(name: string): Promise<void> {
await this.execute(`DROP TABLE IF EXISTS ${name}`);
}
/**
@ -407,7 +411,7 @@ export class SQLiteDB {
* @param params Query parameters.
* @return Promise resolved with the result.
*/
async execute(sql: string, params?: any[]): Promise<any> {
async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise<any> {
await this.ready();
return this.db.executeSql(sql, params);
@ -421,10 +425,10 @@ export class SQLiteDB {
* @param sqlStatements SQL statements to execute.
* @return Promise resolved with the result.
*/
async executeBatch(sqlStatements: any[]): Promise<any> {
async executeBatch(sqlStatements: (string | SQLiteDBRecordValue[])[][]): Promise<void> {
await this.ready();
return this.db.sqlBatch(sqlStatements);
await this.db.sqlBatch(sqlStatements);
}
/**
@ -432,27 +436,35 @@ export class SQLiteDB {
*
* @param data Data to insert.
*/
protected formatDataToInsert(data: object): void {
protected formatDataToInsert(data: SQLiteDBRecordValues): void {
if (!data) {
return;
}
// Remove undefined entries and convert null to "NULL".
for (const name in data) {
const value = data[name];
if (typeof value == 'undefined') {
if (typeof data[name] == 'undefined') {
delete data[name];
}
}
}
/**
* Format the data to where params.
*
* @param data Object data.
*/
protected formatDataToSQLParams(data: SQLiteDBRecordValues): SQLiteDBRecordValue[] {
return Object.keys(data).map((key) => data[key]);
}
/**
* Get all the records from a table.
*
* @param table The table to query.
* @return Promise resolved with the records.
*/
getAllRecords(table: string): Promise<any> {
async getAllRecords(table: string): Promise<SQLiteDBRecordValues[]> {
return this.getRecords(table);
}
@ -464,7 +476,7 @@ export class SQLiteDB {
* @param conditions The conditions to build the where clause. Must not contain numeric indexes.
* @return Promise resolved with the field's value.
*/
getField(table: string, field: string, conditions?: object): Promise<any> {
async getField(table: string, field: string, conditions?: SQLiteDBRecordValues): Promise<SQLiteDBRecordValue> {
const selectAndParams = this.whereClause(conditions);
return this.getFieldSelect(table, field, selectAndParams[0], selectAndParams[1]);
@ -479,7 +491,8 @@ export class SQLiteDB {
* @param params Array of sql parameters.
* @return Promise resolved with the field's value.
*/
getFieldSelect(table: string, field: string, select: string = '', params?: any[]): Promise<any> {
async getFieldSelect(table: string, field: string, select: string = '', params?: SQLiteDBRecordValue[]):
Promise<SQLiteDBRecordValue> {
if (select) {
select = 'WHERE ' + select;
}
@ -494,10 +507,10 @@ export class SQLiteDB {
* @param params An array of sql parameters.
* @return Promise resolved with the field's value.
*/
async getFieldSql(sql: string, params?: any[]): Promise<any> {
async getFieldSql(sql: string, params?: SQLiteDBRecordValue[]): Promise<SQLiteDBRecordValue> {
const record = await this.getRecordSql(sql, params);
if (!record) {
return Promise.reject(null);
throw null;
}
return record[Object.keys(record)[0]];
@ -512,17 +525,14 @@ export class SQLiteDB {
* meaning return empty. Other values will become part of the returned SQL fragment.
* @return A list containing the constructed sql fragment and an array of parameters.
*/
getInOrEqual(items: any, equal: boolean = true, onEmptyItems?: any): any[] {
let sql;
let params;
if (typeof onEmptyItems == 'undefined') {
onEmptyItems = false;
}
getInOrEqual(items: SQLiteDBRecordValue | SQLiteDBRecordValue[], equal: boolean = true, onEmptyItems?: SQLiteDBRecordValue):
SQLiteDBQueryParams {
let sql = '';
let params: SQLiteDBRecordValue[];
// Default behavior, return empty data on empty array.
if (Array.isArray(items) && !items.length && onEmptyItems === false) {
return ['', []];
if (Array.isArray(items) && !items.length && typeof onEmptyItems == 'undefined') {
return { sql: '', params: [] };
}
// Handle onEmptyItems on empty array of items.
@ -530,7 +540,7 @@ export class SQLiteDB {
if (onEmptyItems === null) { // Special case, NULL value.
sql = equal ? ' IS NULL' : ' IS NOT NULL';
return [sql, []];
return { sql, params: [] };
} else {
items = [onEmptyItems]; // Rest of cases, prepare items for processing.
}
@ -544,7 +554,7 @@ export class SQLiteDB {
params = items;
}
return [sql, params];
return { sql, params };
}
/**
@ -564,7 +574,7 @@ export class SQLiteDB {
* @param fields A comma separated list of fields to return.
* @return Promise resolved with the record, rejected if not found.
*/
getRecord(table: string, conditions?: object, fields: string = '*'): Promise<any> {
getRecord(table: string, conditions?: SQLiteDBRecordValues, fields: string = '*'): Promise<SQLiteDBRecordValues> {
const selectAndParams = this.whereClause(conditions);
return this.getRecordSelect(table, selectAndParams[0], selectAndParams[1], fields);
@ -579,7 +589,8 @@ export class SQLiteDB {
* @param fields A comma separated list of fields to return.
* @return Promise resolved with the record, rejected if not found.
*/
getRecordSelect(table: string, select: string = '', params: any[] = [], fields: string = '*'): Promise<any> {
getRecordSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], fields: string = '*'):
Promise<SQLiteDBRecordValues> {
if (select) {
select = ' WHERE ' + select;
}
@ -597,11 +608,11 @@ export class SQLiteDB {
* @param params List of sql parameters
* @return Promise resolved with the records.
*/
async getRecordSql(sql: string, params?: any[]): Promise<any> {
async getRecordSql(sql: string, params?: SQLiteDBRecordValue[]): Promise<SQLiteDBRecordValues> {
const result = await this.getRecordsSql(sql, params, 0, 1);
if (!result || !result.length) {
// Not found, reject.
return Promise.reject(null);
throw null;
}
return result[0];
@ -618,8 +629,8 @@ export class SQLiteDB {
* @param limitNum Return a subset comprising this many records in total.
* @return Promise resolved with the records.
*/
getRecords(table: string, conditions?: object, sort: string = '', fields: string = '*', limitFrom: number = 0,
limitNum: number = 0): Promise<any> {
getRecords(table: string, conditions?: SQLiteDBRecordValues, sort: string = '', fields: string = '*', limitFrom: number = 0,
limitNum: number = 0): Promise<SQLiteDBRecordValues[]> {
const selectAndParams = this.whereClause(conditions);
return this.getRecordsSelect(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum);
@ -637,8 +648,8 @@ export class SQLiteDB {
* @param limitNum Return a subset comprising this many records in total.
* @return Promise resolved with the records.
*/
getRecordsList(table: string, field: string, values: any[], sort: string = '', fields: string = '*', limitFrom: number = 0,
limitNum: number = 0): Promise<any> {
getRecordsList(table: string, field: string, values: SQLiteDBRecordValue[], sort: string = '', fields: string = '*',
limitFrom: number = 0, limitNum: number = 0): Promise<SQLiteDBRecordValues[]> {
const selectAndParams = this.whereClauseList(field, values);
return this.getRecordsSelect(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum);
@ -656,8 +667,8 @@ export class SQLiteDB {
* @param limitNum Return a subset comprising this many records in total.
* @return Promise resolved with the records.
*/
getRecordsSelect(table: string, select: string = '', params: any[] = [], sort: string = '', fields: string = '*',
limitFrom: number = 0, limitNum: number = 0): Promise<any> {
getRecordsSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], sort: string = '',
fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise<SQLiteDBRecordValues[]> {
if (select) {
select = ' WHERE ' + select;
}
@ -679,7 +690,8 @@ export class SQLiteDB {
* @param limitNum Return a subset comprising this many records.
* @return Promise resolved with the records.
*/
async getRecordsSql(sql: string, params?: any[], limitFrom?: number, limitNum?: number): Promise<any> {
async getRecordsSql(sql: string, params?: SQLiteDBRecordValue[], limitFrom?: number, limitNum?: number):
Promise<SQLiteDBRecordValues[]> {
const limits = this.normaliseLimitFromNum(limitFrom, limitNum);
if (limits[0] || limits[1]) {
@ -706,31 +718,31 @@ export class SQLiteDB {
* @param data A data object with values for one or more fields in the record.
* @return Array with the SQL query and the params.
*/
protected getSqlInsertQuery(table: string, data: object): any[] {
protected getSqlInsertQuery(table: string, data: SQLiteDBRecordValues): SQLiteDBQueryParams {
this.formatDataToInsert(data);
const keys = Object.keys(data);
const fields = keys.join(',');
const questionMarks = ',?'.repeat(keys.length).substr(1);
return [
`INSERT OR REPLACE INTO ${table} (${fields}) VALUES (${questionMarks})`,
keys.map((key) => data[key])
];
return {
sql: `INSERT OR REPLACE INTO ${table} (${fields}) VALUES (${questionMarks})`,
params: this.formatDataToSQLParams(data),
};
}
/**
* Initialize the database.
*/
init(): void {
this.promise = Platform.instance.ready().then(() => {
return SQLite.instance.create({
this.promise = Platform.instance.ready()
.then(() => SQLite.instance.create({
name: this.name,
location: 'default'
location: 'default',
}))
.then((db: SQLiteObject) => {
this.db = db;
});
}).then((db: SQLiteObject) => {
this.db = db;
});
}
/**
@ -740,7 +752,7 @@ export class SQLiteDB {
* @param data A data object with values for one or more fields in the record.
* @return Promise resolved with new rowId. Please notice this rowId is internal from SQLite.
*/
async insertRecord(table: string, data: object): Promise<number> {
async insertRecord(table: string, data: SQLiteDBRecordValues): Promise<number> {
const sqlAndParams = this.getSqlInsertQuery(table, data);
const result = await this.execute(sqlAndParams[0], sqlAndParams[1]);
@ -754,18 +766,18 @@ export class SQLiteDB {
* @param dataObjects List of objects to be inserted.
* @return Promise resolved when done.
*/
insertRecords(table: string, dataObjects: object[]): Promise<any> {
async insertRecords(table: string, dataObjects: SQLiteDBRecordValues[]): Promise<void> {
if (!Array.isArray(dataObjects)) {
return Promise.reject(null);
throw null;
}
const statements = [];
const statements = dataObjects.map((dataObject) => {
const statement = this.getSqlInsertQuery(table, dataObject);
dataObjects.forEach((dataObject) => {
statements.push(this.getSqlInsertQuery(table, dataObject));
return [statement.sql, statement.params];
});
return this.executeBatch(statements);
await this.executeBatch(statements);
}
/**
@ -777,12 +789,13 @@ export class SQLiteDB {
* @param fields A comma separated list of fields to return.
* @return Promise resolved when done.
*/
insertRecordsFrom(table: string, source: string, conditions?: object, fields: string = '*'): Promise<any> {
async insertRecordsFrom(table: string, source: string, conditions?: SQLiteDBRecordValues, fields: string = '*'):
Promise<void> {
const selectAndParams = this.whereClause(conditions);
const select = selectAndParams[0] ? 'WHERE ' + selectAndParams[0] : '';
const params = selectAndParams[1];
return this.execute(`INSERT INTO ${table} SELECT ${fields} FROM ${source} ${select}`, params);
await this.execute(`INSERT INTO ${table} SELECT ${fields} FROM ${source} ${select}`, params);
}
/**
@ -794,19 +807,19 @@ export class SQLiteDB {
* @param limitNum How many results to return.
* @return Normalised limit params in array: [limitFrom, limitNum].
*/
normaliseLimitFromNum(limitFrom: any, limitNum: any): number[] {
normaliseLimitFromNum(limitFrom: number, limitNum: number): number[] {
// We explicilty treat these cases as 0.
if (typeof limitFrom == 'undefined' || limitFrom === null || limitFrom === '' || limitFrom === -1) {
if (!limitFrom || limitFrom === -1) {
limitFrom = 0;
}
if (typeof limitNum == 'undefined' || limitNum === null || limitNum === '' || limitNum === -1) {
limitNum = 0;
} else {
limitFrom = Math.max(0, limitFrom);
}
limitFrom = parseInt(limitFrom, 10);
limitNum = parseInt(limitNum, 10);
limitFrom = Math.max(0, limitFrom);
limitNum = Math.max(0, limitNum);
if (!limitNum || limitNum === -1) {
limitNum = 0;
} else {
limitNum = Math.max(0, limitNum);
}
return [limitFrom, limitNum];
}
@ -816,10 +829,10 @@ export class SQLiteDB {
*
* @return Promise resolved when open.
*/
async open(): Promise<any> {
async open(): Promise<void> {
await this.ready();
return this.db.open();
await this.db.open();
}
/**
@ -838,10 +851,10 @@ export class SQLiteDB {
* @param conditions The conditions to build the where clause. Must not contain numeric indexes.
* @return Promise resolved if exists, rejected otherwise.
*/
async recordExists(table: string, conditions?: object): Promise<void> {
async recordExists(table: string, conditions?: SQLiteDBRecordValues): Promise<void> {
const record = await this.getRecord(table, conditions);
if (!record) {
return Promise.reject(null);
throw null;
}
}
@ -853,10 +866,10 @@ export class SQLiteDB {
* @param params An array of sql parameters.
* @return Promise resolved if exists, rejected otherwise.
*/
async recordExistsSelect(table: string, select: string = '', params: any[] = []): Promise<void> {
async recordExistsSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = []): Promise<void> {
const record = await this.getRecordSelect(table, select, params);
if (!record) {
return Promise.reject(null);
throw null;
}
}
@ -867,10 +880,10 @@ export class SQLiteDB {
* @param params An array of sql parameters.
* @return Promise resolved if exists, rejected otherwise.
*/
async recordExistsSql(sql: string, params?: any[]): Promise<void> {
async recordExistsSql(sql: string, params?: SQLiteDBRecordValue[]): Promise<void> {
const record = await this.getRecordSql(sql, params);
if (!record) {
return Promise.reject(null);
throw null;
}
}
@ -881,7 +894,8 @@ export class SQLiteDB {
* @return Promise resolved if exists, rejected otherwise.
*/
async tableExists(name: string): Promise<void> {
await this.recordExists('sqlite_master', {type: 'table', tbl_name: name});
// eslint-disable-next-line @typescript-eslint/naming-convention
await this.recordExists('sqlite_master', { type: 'table', tbl_name: name });
}
/**
@ -890,31 +904,12 @@ export class SQLiteDB {
* @param string table The database table to update.
* @param data An object with the fields to update: fieldname=>fieldvalue.
* @param conditions The conditions to build the where clause. Must not contain numeric indexes.
* @return Promise resolved when updated.
* @return Promise resolved with the number of affected rows.
*/
updateRecords(table: string, data: any, conditions?: any): Promise<any> {
this.formatDataToInsert(data);
if (!data || !Object.keys(data).length) {
// No fields to update, consider it's done.
return Promise.resolve();
}
async updateRecords(table: string, data: SQLiteDBRecordValues, conditions?: SQLiteDBRecordValues): Promise<number> {
const whereAndParams = this.whereClause(conditions);
const sets = [];
let sql;
let params;
for (const key in data) {
sets.push(`${key} = ?`);
}
sql = `UPDATE ${table} SET ${sets.join(', ')} WHERE ${whereAndParams[0]}`;
// Create the list of params using the "data" object and the params for the where clause.
params = Object.keys(data).map((key) => data[key]).concat(whereAndParams[1]);
return this.execute(sql, params);
return this.updateRecordsWhere(table, data, whereAndParams[0], whereAndParams[1]);
}
/**
@ -924,34 +919,35 @@ export class SQLiteDB {
* @param data An object with the fields to update: fieldname=>fieldvalue.
* @param where Where clause. Must not include the "WHERE" word.
* @param whereParams Params for the where clause.
* @return Promise resolved when updated.
* @return Promise resolved with the number of affected rows.
*/
updateRecordsWhere(table: string, data: any, where?: string, whereParams?: any[]): Promise<any> {
async updateRecordsWhere(table: string, data: SQLiteDBRecordValues, where?: string, whereParams?: SQLiteDBRecordValue[]):
Promise<number> {
this.formatDataToInsert(data);
if (!data || !Object.keys(data).length) {
// No fields to update, consider it's done.
return Promise.resolve();
return 0;
}
const sets = [];
let sql;
let params;
for (const key in data) {
sets.push(`${key} = ?`);
}
sql = `UPDATE ${table} SET ${sets.join(', ')}`;
let sql = `UPDATE ${table} SET ${sets.join(', ')}`;
if (where) {
sql += ` WHERE ${where}`;
}
// Create the list of params using the "data" object and the params for the where clause.
params = Object.keys(data).map((key) => data[key]);
let params = this.formatDataToSQLParams(data);
if (where && whereParams) {
params = params.concat(whereParams);
}
return this.execute(sql, params);
const result = await this.execute(sql, params);
return result.rowsAffected;
}
/**
@ -960,9 +956,12 @@ export class SQLiteDB {
* @param conditions The conditions to build the where clause. Must not contain numeric indexes.
* @return An array list containing sql 'where' part and 'params'.
*/
whereClause(conditions: any = {}): any[] {
whereClause(conditions: SQLiteDBRecordValues = {}): SQLiteDBQueryParams {
if (!conditions || !Object.keys(conditions).length) {
return ['1 = 1', []];
return {
sql: '1 = 1',
params: [],
};
}
const where = [];
@ -979,7 +978,10 @@ export class SQLiteDB {
}
}
return [where.join(' AND '), params];
return {
sql: where.join(' AND '),
params,
};
}
/**
@ -989,39 +991,50 @@ export class SQLiteDB {
* @param values The values field might take.
* @return An array containing sql 'where' part and 'params'.
*/
whereClauseList(field: string, values: any[]): any[] {
whereClauseList(field: string, values: SQLiteDBRecordValue[]): SQLiteDBQueryParams {
if (!values || !values.length) {
return ['1 = 2', []]; // Fake condition, won't return rows ever.
return {
sql: '1 = 2', // Fake condition, won't return rows ever.
params: [],
};
}
const params = [];
let select = '';
let sql = '';
values.forEach((value) => {
if (typeof value == 'boolean') {
value = Number(value);
}
if (typeof value == 'undefined' || value === null) {
select = field + ' IS NULL';
sql = field + ' IS NULL';
} else {
params.push(value);
}
});
if (params && params.length) {
if (select !== '') {
select = select + ' OR ';
if (sql !== '') {
sql = sql + ' OR ';
}
if (params.length == 1) {
select = select + field + ' = ?';
sql = sql + field + ' = ?';
} else {
const questionMarks = ',?'.repeat(params.length).substr(1);
select = select + field + ' IN (' + questionMarks + ')';
sql = sql + field + ' IN (' + questionMarks + ')';
}
}
return [select, params];
return { sql, params };
}
}
export type SQLiteDBRecordValues = {
[key in string ]: SQLiteDBRecordValue;
};
export type SQLiteDBQueryParams = {
sql: string;
params: SQLiteDBRecordValue[];
};
type SQLiteDBRecordValue = number | string;

View File

@ -0,0 +1,27 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreIconComponent } from './icon/icon';
@NgModule({
declarations: [
CoreIconComponent,
],
imports: [],
exports: [
CoreIconComponent,
]
})
export class CoreComponentsModule {}

View File

@ -0,0 +1 @@
<div></div>

View File

@ -0,0 +1,52 @@
// TODO ionic 5
:host-context([dir=rtl]) ion-icon {
&.core-icon-dir-flip,
&.fa-caret-right,
&.ion-md-send, &.ion-ios-send {
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}
// Slash
@font-face {
font-family: "Moodle Slash Icon";
font-style: normal;
font-weight: 400;
src: url("/assets/fonts/slash-icon.woff") format("woff");
}
:host {
&.fa {
font-size: 24px;
}
// Center font awesome icons
&.fa::before {
width: 1em;
height: 1em;
text-align: center;
}
&.icon-slash {
position: relative;
&::after {
content: "/";
font-family: "Moodle Slash Icon";
font-size: 0.75em;
margin-top: 0.125em;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
text-align: center;
color: var(--ion-color-danger);
}
&.fa::after {
font-size: 1em;
margin-top: 0;
}
}
}

View File

@ -0,0 +1,135 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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, Input, OnChanges, OnDestroy, ElementRef, SimpleChange } from '@angular/core';
/**
* Core Icon is a component that enables the posibility to add fontawesome icon to the html. It's recommended if both fontawesome
* or ionicons can be used in the name attribute. To use fontawesome just place the full icon name with the fa- prefix and
* the component will detect it.
* Check available icons at https://fontawesome.com/v4.7.0/icons/.
*/
@Component({
selector: 'core-icon',
templateUrl: 'core-icon.html',
styleUrls: ['icon.scss'],
})
export class CoreIconComponent implements OnChanges, OnDestroy {
// Common params.
@Input() name: string;
@Input('color') color?: string;
@Input('slash') slash?: boolean; // Display a red slash over the icon.
// Ionicons params.
@Input('isActive') isActive?: boolean;
@Input('md') md?: string;
@Input('ios') ios?: string;
// FontAwesome params.
@Input('fixed-width') fixedWidth: string;
@Input('label') ariaLabel?: string;
@Input() flipRtl?: boolean; // Whether to flip the icon in RTL. Defaults to false.
protected element: HTMLElement;
protected newElement: HTMLElement;
constructor(el: ElementRef) {
this.element = el.nativeElement;
}
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
if (!changes.name || !this.name) {
return;
}
const oldElement = this.newElement ? this.newElement : this.element;
// Use a new created element to avoid ion-icon working.
// This is necessary to make the FontAwesome stuff work.
// It is also required to stop Ionic overriding the aria-label attribute.
this.newElement = document.createElement('ion-icon');
if (this.name.startsWith('fa-')) {
this.newElement.classList.add('fa');
this.newElement.classList.add(this.name);
if (this.isTrueProperty(this.fixedWidth)) {
this.newElement.classList.add('fa-fw');
}
if (this.color) {
this.newElement.classList.add('fa-' + this.color);
}
}
!this.ariaLabel && this.newElement.setAttribute('aria-hidden', 'true');
!this.ariaLabel && this.newElement.setAttribute('role', 'presentation');
this.ariaLabel && this.newElement.setAttribute('aria-label', this.ariaLabel);
this.ariaLabel && this.newElement.setAttribute('title', this.ariaLabel);
const attrs = this.element.attributes;
for (let i = attrs.length - 1; i >= 0; i--) {
if (attrs[i].name == 'class') {
// We don't want to override the classes we already added. Add them one by one.
if (attrs[i].value) {
const classes = attrs[i].value.split(' ');
for (let j = 0; j < classes.length; j++) {
if (classes[j]) {
this.newElement.classList.add(classes[j]);
}
}
}
} else {
this.newElement.setAttribute(attrs[i].name, attrs[i].value);
}
}
if (this.slash) {
this.newElement.classList.add('icon-slash');
}
if (this.flipRtl) {
this.newElement.classList.add('core-icon-dir-flip');
}
oldElement.parentElement.replaceChild(this.newElement, oldElement);
}
/**
* Check if the value is true or on.
*
* @param val value to be checked.
* @return If has a value equivalent to true.
*/
isTrueProperty(val: any): boolean {
if (typeof val === 'string') {
val = val.toLowerCase().trim();
return (val === 'true' || val === 'on' || val === '');
}
return !!val;
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
if (this.newElement) {
this.newElement.remove();
}
}
}

View File

@ -0,0 +1,35 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { Pipe, PipeTransform } from '@angular/core';
/**
* Pipe to search URLs that are not inside <a> tags and add the corresponding <a> tags.
*/
@Pipe({
name: 'coreCreateLinks',
})
export class CoreCreateLinksPipe implements PipeTransform {
protected static replacePattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])(?![^<]*>|[^<>]*<\/)/gim;
/**
* Takes some text and adds anchor tags to all links that aren't inside anchors.
*
* @param text Text to treat.
* @return Treated text.
*/
transform(text: string): string {
return text.replace(CoreCreateLinksPipe.replacePattern, '<a href="$1">$1</a>');
}
}

View File

@ -0,0 +1,34 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { Pipe, PipeTransform } from '@angular/core';
/**
* Pipe to remove HTML tags.
*/
@Pipe({
name: 'coreNoTags',
})
export class CoreNoTagsPipe implements PipeTransform {
/**
* Takes a text and removes HTML tags.
*
* @param text The text to treat.
* @return Treated text.
*/
transform(text: string): string {
return text.replace(/(<([^>]+)>)/ig, '');
}
}

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreCreateLinksPipe } from './create-links.pipe';
import { CoreNoTagsPipe } from './no-tags.pipe';
import { CoreTimeAgoPipe } from './time-ago.pipe';
@NgModule({
declarations: [
CoreCreateLinksPipe,
CoreNoTagsPipe,
CoreTimeAgoPipe,
],
imports: [],
exports: [
CoreCreateLinksPipe,
CoreNoTagsPipe,
CoreTimeAgoPipe,
]
})
export class CorePipesModule {}

View File

@ -0,0 +1,53 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { Pipe, PipeTransform } from '@angular/core';
import { Translate } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
import moment from 'moment';
/**
* Pipe to turn a UNIX timestamp to "time ago".
*/
@Pipe({
name: 'coreTimeAgo',
})
export class CoreTimeAgoPipe implements PipeTransform {
private logger: CoreLogger;
constructor() {
this.logger = CoreLogger.getInstance('CoreTimeAgoPipe');
}
/**
* Turn a UNIX timestamp to "time ago".
*
* @param timestamp The UNIX timestamp (without milliseconds).
* @return Formatted time.
*/
transform(timestamp: string | number): string {
if (typeof timestamp == 'string') {
// Convert the value to a number.
const numberTimestamp = parseInt(timestamp, 10);
if (isNaN(numberTimestamp)) {
this.logger.error('Invalid value received', timestamp);
return timestamp;
}
timestamp = numberTimestamp;
}
return Translate.instance.instant('core.ago', {$a: moment(timestamp * 1000).fromNow(true)});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -70,7 +70,7 @@ export class CoreGeolocationProvider {
}
if (!CoreApp.instance.isIOS()) {
await Diagnostic.instance.switchToLocationSettings();
Diagnostic.instance.switchToLocationSettings();
await CoreApp.instance.waitForResume(30000);
locationEnabled = await Diagnostic.instance.isLocationEnabled();
@ -91,8 +91,7 @@ export class CoreGeolocationProvider {
const authorizationStatus = await Diagnostic.instance.getLocationAuthorizationStatus();
switch (authorizationStatus) {
// This constant is hard-coded because it is not declared in @ionic-native/diagnostic v4.
case 'DENIED_ONCE':
case Diagnostic.instance.permissionStatus.DENIED_ONCE:
if (failOnDeniedOnce) {
throw new CoreGeolocationError(CoreGeolocationErrorReason.PermissionDenied);
}
@ -107,7 +106,6 @@ export class CoreGeolocationProvider {
case Diagnostic.instance.permissionStatus.GRANTED_WHEN_IN_USE:
// Location is authorized.
return;
case Diagnostic.instance.permissionStatus.DENIED:
default:
throw new CoreGeolocationError(CoreGeolocationErrorReason.PermissionDenied);
}
@ -133,7 +131,7 @@ export enum CoreGeolocationErrorReason {
export class CoreGeolocationError extends CoreError {
readonly reason: CoreGeolocationErrorReason;
reason: CoreGeolocationErrorReason;
constructor(reason: CoreGeolocationErrorReason) {
super(`GeolocationError: ${reason}`);

View File

@ -17,19 +17,20 @@ import { Injectable } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreWSExternalWarning } from '@services/ws';
import { CoreCourseBase } from '@/types/global';
/*
* Service to handle groups.
*/
@Injectable()
export class CoreGroupsProvider {
// Group mode constants.
static NOGROUPS = 0;
static SEPARATEGROUPS = 1;
static VISIBLEGROUPS = 2;
protected ROOT_CACHE_KEY = 'mmGroups:';
constructor() { }
// Group mode constants.
static readonly NOGROUPS = 0;
static readonly SEPARATEGROUPS = 1;
static readonly VISIBLEGROUPS = 2;
protected readonly ROOT_CACHE_KEY = 'mmGroups:';
/**
* Check if group mode of an activity is enabled.
@ -39,12 +40,14 @@ export class CoreGroupsProvider {
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
* @return Promise resolved with true if the activity has groups, resolved with false otherwise.
*/
activityHasGroups(cmId: number, siteId?: string, ignoreCache?: boolean): Promise<boolean> {
return this.getActivityGroupMode(cmId, siteId, ignoreCache).then((groupmode) => {
async activityHasGroups(cmId: number, siteId?: string, ignoreCache?: boolean): Promise<boolean> {
try {
const groupmode = await this.getActivityGroupMode(cmId, siteId, ignoreCache);
return groupmode === CoreGroupsProvider.SEPARATEGROUPS || groupmode === CoreGroupsProvider.VISIBLEGROUPS;
}).catch(() => {
} catch (error) {
return false;
});
}
}
/**
@ -56,32 +59,32 @@ export class CoreGroupsProvider {
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
* @return Promise resolved when the groups are retrieved.
*/
getActivityAllowedGroups(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean): Promise<any> {
return CoreSites.instance.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
async getActivityAllowedGroups(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean):
Promise<CoreGroupGetActivityAllowedGroupsResponse> {
const site = await CoreSites.instance.getSite(siteId);
const params = {
cmid: cmId,
userid: userId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getActivityAllowedGroupsCacheKey(cmId, userId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
userId = userId || site.getUserId();
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
const params = {
cmid: cmId,
userid: userId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getActivityAllowedGroupsCacheKey(cmId, userId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
return site.read('core_group_get_activity_allowed_groups', params, preSets).then((response) => {
if (!response || !response.groups) {
return Promise.reject(null);
}
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return response;
});
});
const response = await site.read('core_group_get_activity_allowed_groups', params, preSets);
if (!response || !response.groups) {
throw null;
}
return response;
}
/**
@ -104,20 +107,20 @@ export class CoreGroupsProvider {
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
* @return Promise resolved when the groups are retrieved. If not allowed, empty array will be returned.
*/
getActivityAllowedGroupsIfEnabled(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean): Promise<any[]> {
async getActivityAllowedGroupsIfEnabled(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean):
Promise<CoreGroupGetActivityAllowedGroupsResponse> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
// Get real groupmode, in case it's forced by the course.
return this.activityHasGroups(cmId, siteId, ignoreCache).then((hasGroups) => {
if (hasGroups) {
// Get the groups available for the user.
return this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache);
}
const hasGroups = await this.activityHasGroups(cmId, siteId, ignoreCache);
if (hasGroups) {
// Get the groups available for the user.
return this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache);
}
return {
groups: []
};
});
return {
groups: [],
};
}
/**
@ -130,44 +133,43 @@ export class CoreGroupsProvider {
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
* @return Promise resolved with the group info.
*/
getActivityGroupInfo(cmId: number, addAllParts?: boolean, userId?: number, siteId?: string, ignoreCache?: boolean)
: Promise<CoreGroupInfo> {
async getActivityGroupInfo(cmId: number, addAllParts?: boolean, userId?: number, siteId?: string, ignoreCache?: boolean):
Promise<CoreGroupInfo> {
const groupInfo: CoreGroupInfo = {
groups: []
groups: [],
};
return this.getActivityGroupMode(cmId, siteId, ignoreCache).then((groupMode) => {
groupInfo.separateGroups = groupMode === CoreGroupsProvider.SEPARATEGROUPS;
groupInfo.visibleGroups = groupMode === CoreGroupsProvider.VISIBLEGROUPS;
const groupMode = await this.getActivityGroupMode(cmId, siteId, ignoreCache);
if (groupInfo.separateGroups || groupInfo.visibleGroups) {
return this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache);
}
groupInfo.separateGroups = groupMode === CoreGroupsProvider.SEPARATEGROUPS;
groupInfo.visibleGroups = groupMode === CoreGroupsProvider.VISIBLEGROUPS;
return {
let result: CoreGroupGetActivityAllowedGroupsResponse;
if (groupInfo.separateGroups || groupInfo.visibleGroups) {
result = await this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache);
} else {
result = {
groups: [],
canaccessallgroups: false
};
}).then((result) => {
if (result.groups.length <= 0) {
groupInfo.separateGroups = false;
groupInfo.visibleGroups = false;
}
if (result.groups.length <= 0) {
groupInfo.separateGroups = false;
groupInfo.visibleGroups = false;
groupInfo.defaultGroupId = 0;
} else {
// The "canaccessallgroups" field was added in 3.4. Add all participants for visible groups in previous versions.
if (result.canaccessallgroups || (typeof result.canaccessallgroups == 'undefined' && groupInfo.visibleGroups)) {
groupInfo.groups.push({ id: 0, name: Translate.instance.instant('core.allparticipants') });
groupInfo.defaultGroupId = 0;
} else {
// The "canaccessallgroups" field was added in 3.4. Add all participants for visible groups in previous versions.
if (result.canaccessallgroups || (typeof result.canaccessallgroups == 'undefined' && groupInfo.visibleGroups)) {
groupInfo.groups.push({ id: 0, name: Translate.instance.instant('core.allparticipants') });
groupInfo.defaultGroupId = 0;
} else {
groupInfo.defaultGroupId = result.groups[0].id;
}
groupInfo.groups = groupInfo.groups.concat(result.groups);
groupInfo.defaultGroupId = result.groups[0].id;
}
return groupInfo;
});
groupInfo.groups = groupInfo.groups.concat(result.groups);
}
return groupInfo;
}
/**
@ -178,29 +180,27 @@ export class CoreGroupsProvider {
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
* @return Promise resolved when the group mode is retrieved.
*/
getActivityGroupMode(cmId: number, siteId?: string, ignoreCache?: boolean): Promise<number> {
return CoreSites.instance.getSite(siteId).then((site) => {
const params = {
cmid: cmId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getActivityGroupModeCacheKey(cmId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
async getActivityGroupMode(cmId: number, siteId?: string, ignoreCache?: boolean): Promise<number> {
const site = await CoreSites.instance.getSite(siteId);
const params = {
cmid: cmId,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getActivityGroupModeCacheKey(cmId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return site.read('core_group_get_activity_groupmode', params, preSets).then((response) => {
if (!response || typeof response.groupmode == 'undefined') {
return Promise.reject(null);
}
const response = await site.read('core_group_get_activity_groupmode', params, preSets);
if (!response || typeof response.groupmode == 'undefined') {
throw null;
}
return response.groupmode;
});
});
return response.groupmode;
}
/**
@ -219,16 +219,15 @@ export class CoreGroupsProvider {
* @param siteId Site to get the groups from. If not defined, use current site.
* @return Promise resolved when the groups are retrieved.
*/
getAllUserGroups(siteId?: string): Promise<any[]> {
return CoreSites.instance.getSite(siteId).then((site) => {
siteId = siteId || site.getId();
async getAllUserGroups(siteId?: string): Promise<CoreGroup[]> {
const site = await CoreSites.instance.getSite(siteId);
siteId = siteId || site.getId();
if (site.isVersionGreaterEqualThan('3.6')) {
return this.getUserGroupsInCourse(0, siteId);
}
if (site.isVersionGreaterEqualThan('3.6')) {
return this.getUserGroupsInCourse(0, siteId);
}
// @todo Get courses.
});
// @todo Get courses.
}
/**
@ -239,17 +238,13 @@ export class CoreGroupsProvider {
* @param userId ID of the user. If not defined, use the userId related to siteId.
* @return Promise resolved when the groups are retrieved.
*/
getUserGroups(courses: any[], siteId?: string, userId?: number): Promise<any[]> {
async getUserGroups(courses: CoreCourseBase[] | number[], siteId?: string, userId?: number): Promise<CoreGroup[]> {
// Get all courses one by one.
const promises = courses.map((course) => {
const courseId = typeof course == 'object' ? course.id : course;
const promises = this.getCourseIds(courses).map((courseId) => this.getUserGroupsInCourse(courseId, siteId, userId));
return this.getUserGroupsInCourse(courseId, siteId, userId);
});
const courseGroups = await Promise.all(promises);
return Promise.all(promises).then((courseGroups) => {
return [].concat(...courseGroups);
});
return [].concat(...courseGroups);
}
/**
@ -260,26 +255,24 @@ export class CoreGroupsProvider {
* @param userId ID of the user. If not defined, use ID related to siteid.
* @return Promise resolved when the groups are retrieved.
*/
getUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise<any[]> {
return CoreSites.instance.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
const data = {
userid: userId,
courseid: courseId,
};
const preSets = {
cacheKey: this.getUserGroupsInCourseCacheKey(courseId, userId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
async getUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise<CoreGroup[]> {
const site = await CoreSites.instance.getSite(siteId);
userId = userId || site.getUserId();
const data = {
userid: userId,
courseid: courseId,
};
const preSets = {
cacheKey: this.getUserGroupsInCourseCacheKey(courseId, userId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
return site.read('core_group_get_course_user_groups', data, preSets).then((response) => {
if (response && response.groups) {
return response.groups;
} else {
return Promise.reject(null);
}
});
});
const response = await site.read('core_group_get_course_user_groups', data, preSets);
if (!response || !response.groups) {
throw null;
}
return response.groups;
}
/**
@ -310,12 +303,11 @@ export class CoreGroupsProvider {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
invalidateActivityAllowedGroups(cmId: number, userId?: number, siteId?: string): Promise<any> {
return CoreSites.instance.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
async invalidateActivityAllowedGroups(cmId: number, userId?: number, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
userId = userId || site.getUserId();
return site.invalidateWsCacheForKey(this.getActivityAllowedGroupsCacheKey(cmId, userId));
});
await site.invalidateWsCacheForKey(this.getActivityAllowedGroupsCacheKey(cmId, userId));
}
/**
@ -325,10 +317,10 @@ export class CoreGroupsProvider {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
invalidateActivityGroupMode(cmId: number, siteId?: string): Promise<any> {
return CoreSites.instance.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getActivityGroupModeCacheKey(cmId));
});
async invalidateActivityGroupMode(cmId: number, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.invalidateWsCacheForKey(this.getActivityGroupModeCacheKey(cmId));
}
/**
@ -339,12 +331,12 @@ export class CoreGroupsProvider {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
invalidateActivityGroupInfo(cmId: number, userId?: number, siteId?: string): Promise<any> {
async invalidateActivityGroupInfo(cmId: number, userId?: number, siteId?: string): Promise<void> {
const promises = [];
promises.push(this.invalidateActivityAllowedGroups(cmId, userId, siteId));
promises.push(this.invalidateActivityGroupMode(cmId, siteId));
return Promise.all(promises);
await Promise.all(promises);
}
/**
@ -353,14 +345,14 @@ export class CoreGroupsProvider {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
invalidateAllUserGroups(siteId?: string): Promise<any> {
return CoreSites.instance.getSite(siteId).then((site) => {
if (site.isVersionGreaterEqualThan('3.6')) {
return this.invalidateUserGroupsInCourse(0, siteId);
}
async invalidateAllUserGroups(siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
return site.invalidateWsCacheForKeyStartingWith(this.getUserGroupsInCoursePrefixCacheKey());
});
if (site.isVersionGreaterEqualThan('3.6')) {
return this.invalidateUserGroupsInCourse(0, siteId);
}
await site.invalidateWsCacheForKeyStartingWith(this.getUserGroupsInCoursePrefixCacheKey());
}
/**
@ -371,18 +363,13 @@ export class CoreGroupsProvider {
* @param userId User ID. If not defined, use current user.
* @return Promise resolved when the data is invalidated.
*/
invalidateUserGroups(courses: any[], siteId?: string, userId?: number): Promise<any> {
return CoreSites.instance.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
async invalidateUserGroups(courses: CoreCourseBase[] | number[], siteId?: string, userId?: number): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
userId = userId || site.getUserId();
const promises = courses.map((course) => {
const courseId = typeof course == 'object' ? course.id : course;
const promises = this.getCourseIds(courses).map((courseId) => this.invalidateUserGroupsInCourse(courseId, site.id, userId));
return this.invalidateUserGroupsInCourse(courseId, site.id, userId);
});
return Promise.all(promises);
});
await Promise.all(promises);
}
/**
@ -393,12 +380,11 @@ export class CoreGroupsProvider {
* @param userId User ID. If not defined, use current user.
* @return Promise resolved when the data is invalidated.
*/
invalidateUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise<any> {
return CoreSites.instance.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
async invalidateUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
userId = userId || site.getUserId();
return site.invalidateWsCacheForKey(this.getUserGroupsInCourseCacheKey(courseId, userId));
});
await site.invalidateWsCacheForKey(this.getUserGroupsInCourseCacheKey(courseId, userId));
}
/**
@ -418,10 +404,30 @@ export class CoreGroupsProvider {
return groupInfo.defaultGroupId;
}
protected getCourseIds(courses: CoreCourseBase[] | number[]): number[] {
return courses.length > 0 && typeof courses[0] === 'object'
? (courses as CoreCourseBase[]).map((course) => course.id)
: courses as number[];
}
}
export class CoreGroups extends makeSingleton(CoreGroupsProvider) {}
/**
* Specific group info.
*/
export type CoreGroup = {
id: number; // Group ID.
name: string; // Multilang compatible name, course unique'.
description?: string; // Group description text.
descriptionformat?: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
idnumber?: string; // Id number.
courseid?: number; // Coure Id.
};
/**
* Group info for an activity.
*/
@ -429,7 +435,7 @@ export type CoreGroupInfo = {
/**
* List of groups.
*/
groups?: any[];
groups?: CoreGroup[];
/**
* Whether it's separate groups.
@ -446,3 +452,12 @@ export type CoreGroupInfo = {
*/
defaultGroupId?: number;
};
/**
* WS core_group_get_activity_allowed_groups response type.
*/
export type CoreGroupGetActivityAllowedGroupsResponse = {
groups: CoreGroup[]; // List of groups.
canaccessallgroups?: boolean; // Whether the user will be able to access all the activity groups.
warnings?: CoreWSExternalWarning[];
};

View File

@ -0,0 +1,62 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { Injectable } from '@angular/core';
import { CoreCronHandler } from '@services/cron';
import { CoreSites } from '@services/sites';
/**
* Cron handler to update site info every certain time.
*/
@Injectable()
export class CoreSiteInfoCronHandler implements CoreCronHandler {
name = 'CoreSiteInfoCronHandler';
/**
* Execute the process.
* Receives the ID of the site affected, undefined for all sites.
*
* @param siteId ID of the site affected, undefined for all sites.
* @return Promise resolved when done, rejected on failure.
*/
async execute(siteId?: string): Promise<void> {
if (!siteId) {
const siteIds = await CoreSites.instance.getSitesIds();
await Promise.all(siteIds.map((siteId) => CoreSites.instance.updateSiteInfo(siteId)));
} else {
await CoreSites.instance.updateSiteInfo(siteId);
}
}
/**
* Returns handler's interval in milliseconds. Defaults to CoreCronDelegate.DEFAULT_INTERVAL.
*
* @return Interval time (in milliseconds).
*/
getInterval(): number {
return 10800000; // 3 hours.
}
/**
* Check whether it's a synchronization process or not. True if not defined.
*
* @return Whether it's a synchronization process or not.
*/
isSync(): boolean {
return false;
}
}

View File

@ -14,7 +14,7 @@
import { Injectable } from '@angular/core';
import { CoreUtils } from '@services/utils/utils';
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { CoreLogger } from '@singletons/logger';
import { makeSingleton } from '@singletons/core.singletons';
@ -50,12 +50,13 @@ export type CoreInitHandler = {
*/
@Injectable()
export class CoreInitDelegate {
static readonly DEFAULT_PRIORITY = 100; // Default priority for init processes.
static readonly MAX_RECOMMENDED_PRIORITY = 600;
protected initProcesses = {};
protected initProcesses: { [s: string]: CoreInitHandler } = {};
protected logger: CoreLogger;
protected readiness;
protected readiness: CoreInitReadinessPromiseDefer<void>;
constructor() {
this.logger = CoreLogger.getInstance('CoreInitDelegate');
@ -163,6 +164,14 @@ export class CoreInitDelegate {
this.logger.log(`Registering process '${handler.name}'.`);
this.initProcesses[handler.name] = handler;
}
}
export class CoreInit extends makeSingleton(CoreInitDelegate) {}
/**
* Deferred promise for init readiness.
*/
type CoreInitReadinessPromiseDefer<T> = PromiseDefer<T> & {
resolved?: boolean; // If true, readiness have been resolved.
};

View File

@ -15,7 +15,7 @@
import { Injectable } from '@angular/core';
import CoreConfigConstants from '@app/config.json';
import { CoreApp, CoreAppProvider } from '@services/app';
import { CoreAppProvider } from '@services/app';
import { CoreConfig } from '@services/config';
import { makeSingleton, Translate, Platform, Globalization } from '@singletons/core.singletons';
@ -26,6 +26,7 @@ import * as moment from 'moment';
*/
@Injectable()
export class CoreLangProvider {
protected fallbackLanguage = 'en'; // Always use English as fallback language since it contains all strings.
protected defaultLanguage = CoreConfigConstants.default_lang || 'en'; // Lang to use if device lang not valid or is forced.
protected currentLanguage: string; // Save current language in a variable to speed up the get function.
@ -51,7 +52,7 @@ export class CoreLangProvider {
});
});
Translate.instance.onLangChange.subscribe((event: any) => {
Translate.instance.onLangChange.subscribe(() => {
// @todo: Set platform lang and dir.
});
}
@ -63,7 +64,7 @@ export class CoreLangProvider {
* @param strings Object with the strings to add.
* @param prefix A prefix to add to all keys.
*/
addSitePluginsStrings(lang: string, strings: any, prefix?: string): void {
addSitePluginsStrings(lang: string, strings: string[], prefix?: string): void {
lang = lang.replace(/_/g, '-'); // Use the app format instead of Moodle format.
// Initialize structure if it doesn't exist.
@ -109,7 +110,7 @@ export class CoreLangProvider {
* @param language New language to use.
* @return Promise resolved when the change is finished.
*/
changeCurrentLanguage(language: string): Promise<any> {
changeCurrentLanguage(language: string): Promise<unknown> {
const promises = [];
// Change the language, resolving the promise when we receive the first value.
@ -127,7 +128,7 @@ export class CoreLangProvider {
setTimeout(() => {
fallbackSubs.unsubscribe();
});
}, (error) => {
}, () => {
// Resolve with the original language.
resolve(data);
@ -168,7 +169,7 @@ export class CoreLangProvider {
// Load the custom and site plugins strings for the language.
if (this.loadLangStrings(this.customStrings, language) || this.loadLangStrings(this.sitePluginsStrings, language)) {
// Some lang strings have changed, emit an event to update the pipes.
Translate.instance.onLangChange.emit({lang: language, translations: Translate.instance.translations[language]});
Translate.instance.onLangChange.emit({ lang: language, translations: Translate.instance.translations[language] });
}
});
}
@ -195,7 +196,7 @@ export class CoreLangProvider {
*
* @return Custom strings.
*/
getAllCustomStrings(): any {
getAllCustomStrings(): unknown {
return this.customStrings;
}
@ -204,7 +205,7 @@ export class CoreLangProvider {
*
* @return Site plugins strings.
*/
getAllSitePluginsStrings(): any {
getAllSitePluginsStrings(): unknown {
return this.sitePluginsStrings;
}
@ -213,16 +214,13 @@ export class CoreLangProvider {
*
* @return Promise resolved with the current language.
*/
getCurrentLanguage(): Promise<string> {
async getCurrentLanguage(): Promise<string> {
if (typeof this.currentLanguage != 'undefined') {
return Promise.resolve(this.currentLanguage);
return this.currentLanguage;
}
// Get current language from config (user might have changed it).
return CoreConfig.instance.get('current_language').then((language) => {
return language;
}).catch(() => {
return CoreConfig.instance.get('current_language').then((language) => language).catch(() => {
// User hasn't defined a language. If default language is forced, use it.
if (CoreConfigConstants.default_lang && CoreConfigConstants.forcedefaultlanguage) {
return CoreConfigConstants.default_lang;
@ -237,7 +235,6 @@ export class CoreLangProvider {
if (CoreConfigConstants.languages && typeof CoreConfigConstants.languages[language] == 'undefined') {
// Code is NOT supported. Fallback to language without dash. E.g. 'en-US' would fallback to 'en'.
language = language.substr(0, language.indexOf('-'));
}
}
@ -247,10 +244,10 @@ export class CoreLangProvider {
}
return language;
}).catch(() => {
}).catch(() =>
// Error getting locale. Use default language.
return this.defaultLanguage;
});
this.defaultLanguage,
);
} catch (err) {
// Error getting locale. Use default language.
return Promise.resolve(this.defaultLanguage);
@ -286,7 +283,7 @@ export class CoreLangProvider {
* @param lang The language to check.
* @return Promise resolved when done.
*/
getTranslationTable(lang: string): Promise<any> {
getTranslationTable(lang: string): Promise<unknown> {
// Create a promise to convert the observable into a promise.
return new Promise((resolve, reject): void => {
const observer = Translate.instance.getTranslation(lang).subscribe((table) => {
@ -322,14 +319,13 @@ export class CoreLangProvider {
const list: string[] = strings.split(/(?:\r\n|\r|\n)/);
list.forEach((entry: string) => {
const values: string[] = entry.split('|');
let lang: string;
if (values.length < 3) {
// Not enough data, ignore the entry.
return;
}
lang = values[2].replace(/_/g, '-'); // Use the app format instead of Moodle format.
const lang = values[2].replace(/_/g, '-'); // Use the app format instead of Moodle format.
if (lang == this.currentLanguage) {
currentLangChanged = true;
@ -353,7 +349,7 @@ export class CoreLangProvider {
// Some lang strings have changed, emit an event to update the pipes.
Translate.instance.onLangChange.emit({
lang: this.currentLanguage,
translations: Translate.instance.translations[this.currentLanguage]
translations: Translate.instance.translations[this.currentLanguage],
});
}
}
@ -365,7 +361,7 @@ export class CoreLangProvider {
* @param lang Language to load.
* @return Whether the translation table was modified.
*/
loadLangStrings(langObject: any, lang: string): boolean {
loadLangStrings(langObject: CoreLanguageObject, lang: string): boolean {
let langApplied = false;
if (langObject[lang]) {
@ -396,7 +392,7 @@ export class CoreLangProvider {
* @param key String key.
* @param value String value.
*/
loadString(langObject: any, lang: string, key: string, value: string): void {
loadString(langObject: CoreLanguageObject, lang: string, key: string, value: string): void {
lang = lang.replace(/_/g, '-'); // Use the app format instead of Moodle format.
if (Translate.instance.translations[lang]) {
@ -425,7 +421,7 @@ export class CoreLangProvider {
*
* @param strings Strings to unload.
*/
protected unloadStrings(strings: any): void {
protected unloadStrings(strings: CoreLanguageObject): void {
// Iterate over all languages and strings.
for (const lang in strings) {
if (!Translate.instance.translations[lang]) {
@ -446,6 +442,20 @@ export class CoreLangProvider {
}
}
}
}
export class CoreLang extends makeSingleton(CoreLangProvider) {}
/**
* Language object has two leves, first per language and second per string key.
*/
type CoreLanguageObject = {
[s: string]: { // Lang name.
[s: string]: { // String key.
value: string; // Value with replacings done.
original?: string; // Original value of the string.
applied?: boolean; // If the key is applied to the translations table or not.
};
};
};

View File

@ -18,7 +18,7 @@ import { ILocalNotification } from '@ionic-native/local-notifications';
import { CoreApp, CoreAppSchema } from '@services/app';
import { CoreConfig } from '@services/config';
import { CoreEvents, CoreEventsProvider } from '@services/events';
import { CoreEventObserver, CoreEvents, CoreEventsProvider } from '@services/events';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { SQLiteDB } from '@classes/sqlitedb';
@ -33,56 +33,57 @@ import { CoreLogger } from '@singletons/logger';
*/
@Injectable()
export class CoreLocalNotificationsProvider {
// Variables for the database.
protected SITES_TABLE = 'notification_sites'; // Store to asigne unique codes to each site.
protected COMPONENTS_TABLE = 'notification_components'; // Store to asigne unique codes to each component.
protected TRIGGERED_TABLE = 'notifications_triggered'; // Store to prevent re-triggering notifications.
protected static readonly SITES_TABLE = 'notification_sites'; // Store to asigne unique codes to each site.
protected static readonly COMPONENTS_TABLE = 'notification_components'; // Store to asigne unique codes to each component.
protected static readonly TRIGGERED_TABLE = 'notifications_triggered'; // Store to prevent re-triggering notifications.
protected tablesSchema: CoreAppSchema = {
name: 'CoreLocalNotificationsProvider',
version: 1,
tables: [
{
name: this.SITES_TABLE,
name: CoreLocalNotificationsProvider.SITES_TABLE,
columns: [
{
name: 'id',
type: 'TEXT',
primaryKey: true
primaryKey: true,
},
{
name: 'code',
type: 'INTEGER',
notNull: true
notNull: true,
},
],
},
{
name: this.COMPONENTS_TABLE,
name: CoreLocalNotificationsProvider.COMPONENTS_TABLE,
columns: [
{
name: 'id',
type: 'TEXT',
primaryKey: true
primaryKey: true,
},
{
name: 'code',
type: 'INTEGER',
notNull: true
notNull: true,
},
],
},
{
name: this.TRIGGERED_TABLE,
name: CoreLocalNotificationsProvider.TRIGGERED_TABLE,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true
primaryKey: true,
},
{
name: 'at',
type: 'INTEGER',
notNull: true
notNull: true,
},
],
},
@ -91,7 +92,7 @@ export class CoreLocalNotificationsProvider {
protected logger: CoreLogger;
protected appDB: SQLiteDB;
protected dbReady: Promise<any>; // Promise resolved when the app DB is initialized.
protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized.
protected codes: { [s: string]: number } = {};
protected codeRequestsQueue = {};
protected observables = {};
@ -99,8 +100,9 @@ export class CoreLocalNotificationsProvider {
title: '',
texts: [],
ids: [],
timeouts: []
timeouts: [],
};
protected triggerSubscription: Subscription;
protected clickSubscription: Subscription;
protected clearSubscription: Subscription;
@ -110,7 +112,6 @@ export class CoreLocalNotificationsProvider {
protected queueRunner: CoreQueueRunner; // Queue to decrease the number of concurrent calls to the plugin (see MOBILE-3477).
constructor() {
this.logger = CoreLogger.getInstance('CoreLocalNotificationsProvider');
this.queueRunner = new CoreQueueRunner(10);
this.appDB = CoreApp.instance.getDB();
@ -149,7 +150,7 @@ export class CoreLocalNotificationsProvider {
// Create the default channel for local notifications.
this.createDefaultChannel();
Translate.instance.onLangChange.subscribe((event: any) => {
Translate.instance.onLangChange.subscribe(() => {
// Update the channel name.
this.createDefaultChannel();
});
@ -187,7 +188,6 @@ export class CoreLocalNotificationsProvider {
* @return Promise resolved when the notifications are cancelled.
*/
async cancelSiteNotifications(siteId: string): Promise<void> {
if (!this.isAvailable()) {
return;
} else if (!siteId) {
@ -228,15 +228,15 @@ export class CoreLocalNotificationsProvider {
*
* @return Promise resolved when done.
*/
protected createDefaultChannel(): Promise<any> {
protected async createDefaultChannel(): Promise<void> {
if (!CoreApp.instance.isAndroid()) {
return Promise.resolve();
return;
}
return Push.instance.createChannel({
await Push.instance.createChannel({
id: 'default-channel-id',
description: Translate.instance.instant('addon.calendar.calendarreminders'),
importance: 4
importance: 4,
}).catch((error) => {
this.logger.error('Error changing channel name', error);
});
@ -297,7 +297,7 @@ export class CoreLocalNotificationsProvider {
* @return Promise resolved when the component code is retrieved.
*/
protected getComponentCode(component: string): Promise<number> {
return this.requestCode(this.COMPONENTS_TABLE, component);
return this.requestCode(CoreLocalNotificationsProvider.COMPONENTS_TABLE, component);
}
/**
@ -308,16 +308,16 @@ export class CoreLocalNotificationsProvider {
* @return Promise resolved when the site code is retrieved.
*/
protected getSiteCode(siteId: string): Promise<number> {
return this.requestCode(this.SITES_TABLE, siteId);
return this.requestCode(CoreLocalNotificationsProvider.SITES_TABLE, siteId);
}
/**
* Create a unique notification ID, trying to prevent collisions. Generated ID must be a Number (Android).
* The generated ID shouldn't be higher than 2147483647 or it's going to cause problems in Android.
* This function will prevent collisions and keep the number under Android limit if:
* -User has used less than 21 sites.
* -There are less than 11 components.
* -The notificationId passed as parameter is lower than 10000000.
* - User has used less than 21 sites.
* - There are less than 11 components.
* - The notificationId passed as parameter is lower than 10000000.
*
* @param notificationId Notification ID.
* @param component Component triggering the notification.
@ -329,12 +329,10 @@ export class CoreLocalNotificationsProvider {
return Promise.reject(null);
}
return this.getSiteCode(siteId).then((siteCode) => {
return this.getComponentCode(component).then((componentCode) => {
return this.getSiteCode(siteId).then((siteCode) => this.getComponentCode(component).then((componentCode) =>
// We use the % operation to keep the number under Android's limit.
return (siteCode * 100000000 + componentCode * 10000000 + notificationId) % 2147483647;
});
});
(siteCode * 100000000 + componentCode * 10000000 + notificationId) % 2147483647,
));
}
/**
@ -343,7 +341,7 @@ export class CoreLocalNotificationsProvider {
* @param eventName Name of the event.
* @param notification Notification.
*/
protected handleEvent(eventName: string, notification: any): void {
protected handleEvent(eventName: string, notification: ILocalNotification): void {
if (notification && notification.data) {
this.logger.debug('Notification event: ' + eventName + '. Data:', notification.data);
@ -374,7 +372,7 @@ export class CoreLocalNotificationsProvider {
await this.dbReady;
try {
const stored = await this.appDB.getRecord(this.TRIGGERED_TABLE, { id: notification.id });
const stored = await this.appDB.getRecord(CoreLocalNotificationsProvider.TRIGGERED_TABLE, { id: notification.id });
let triggered = (notification.trigger && notification.trigger.at) || 0;
if (typeof triggered != 'number') {
@ -443,15 +441,14 @@ export class CoreLocalNotificationsProvider {
*/
protected processNextRequest(): void {
const nextKey = Object.keys(this.codeRequestsQueue)[0];
let request,
promise;
let promise: Promise<void>;
if (typeof nextKey == 'undefined') {
// No more requests in queue, stop.
return;
}
request = this.codeRequestsQueue[nextKey];
const request = this.codeRequestsQueue[nextKey];
// Check if request is valid.
if (typeof request == 'object' && typeof request.table != 'undefined' && typeof request.id != 'undefined') {
@ -483,7 +480,7 @@ export class CoreLocalNotificationsProvider {
* @param callback Function to call with the data received by the notification.
* @return Object with an "off" property to stop listening for clicks.
*/
registerClick(component: string, callback: Function): any {
registerClick(component: string, callback: CoreLocalNotificationsClickCallback): CoreEventObserver {
return this.registerObserver('click', component, callback);
}
@ -495,7 +492,7 @@ export class CoreLocalNotificationsProvider {
* @param callback Function to call with the data received by the notification.
* @return Object with an "off" property to stop listening for events.
*/
registerObserver(eventName: string, component: string, callback: Function): any {
registerObserver<T>(eventName: string, component: string, callback: CoreLocalNotificationsClickCallback): CoreEventObserver {
this.logger.debug(`Register observer '${component}' for event '${eventName}'.`);
if (typeof this.observables[eventName] == 'undefined') {
@ -504,7 +501,7 @@ export class CoreLocalNotificationsProvider {
if (typeof this.observables[eventName][component] == 'undefined') {
// No observable for this component, create a new one.
this.observables[eventName][component] = new Subject<any>();
this.observables[eventName][component] = new Subject<T>();
}
this.observables[eventName][component].subscribe(callback);
@ -512,7 +509,7 @@ export class CoreLocalNotificationsProvider {
return {
off: (): void => {
this.observables[eventName][component].unsubscribe(callback);
}
},
};
}
@ -522,10 +519,10 @@ export class CoreLocalNotificationsProvider {
* @param id Notification ID.
* @return Promise resolved when it is removed.
*/
async removeTriggered(id: number): Promise<any> {
async removeTriggered(id: number): Promise<void> {
await this.dbReady;
return this.appDB.deleteRecords(this.TRIGGERED_TABLE, { id: id });
await this.appDB.deleteRecords(CoreLocalNotificationsProvider.TRIGGERED_TABLE, { id: id });
}
/**
@ -536,9 +533,9 @@ export class CoreLocalNotificationsProvider {
* @return Promise resolved when the code is retrieved.
*/
protected requestCode(table: string, id: string): Promise<number> {
const deferred = CoreUtils.instance.promiseDefer<number>(),
key = table + '#' + id,
isQueueEmpty = Object.keys(this.codeRequestsQueue).length == 0;
const deferred = CoreUtils.instance.promiseDefer<number>();
const key = table + '#' + id;
const isQueueEmpty = Object.keys(this.codeRequestsQueue).length == 0;
if (typeof this.codeRequestsQueue[key] != 'undefined') {
// There's already a pending request for this store and ID, add the promise to it.
@ -548,7 +545,7 @@ export class CoreLocalNotificationsProvider {
this.codeRequestsQueue[key] = {
table: table,
id: id,
promises: [deferred]
promises: [deferred],
};
}
@ -591,7 +588,6 @@ export class CoreLocalNotificationsProvider {
* @return Promise resolved when the notification is scheduled.
*/
async schedule(notification: ILocalNotification, component: string, siteId: string, alreadyUnique?: boolean): Promise<void> {
if (!alreadyUnique) {
notification.id = await this.getUniqueNotificationId(notification.id, component, siteId);
}
@ -605,12 +601,29 @@ export class CoreLocalNotificationsProvider {
notification.smallIcon = notification.smallIcon || 'res://smallicon';
notification.color = notification.color || CoreConfigConstants.notificoncolor;
const led: any = notification.led || {};
notification.led = {
color: led.color || 'FF9900',
on: led.on || 1000,
off: led.off || 1000
};
if (notification.led !== false) {
let ledColor = 'FF9900';
let ledOn = 1000;
let ledOff = 1000;
if (typeof notification.led === 'string') {
ledColor = notification.led;
} else if (Array.isArray(notification.led)) {
ledColor = notification.led[0] || ledColor;
ledOn = notification.led[1] || ledOn;
ledOff = notification.led[2] || ledOff;
} else if (typeof notification.led === 'object') {
ledColor = notification.led.color || ledColor;
ledOn = notification.led.on || ledOn;
ledOff = notification.led.off || ledOff;
}
notification.led = {
color: ledColor,
on: ledOn,
off: ledOff,
};
}
}
const queueId = 'schedule-' + notification.id;
@ -626,36 +639,34 @@ export class CoreLocalNotificationsProvider {
* @param notification Notification to schedule.
* @return Promise resolved when scheduled.
*/
protected scheduleNotification(notification: ILocalNotification): Promise<void> {
protected async scheduleNotification(notification: ILocalNotification): Promise<void> {
// Check if the notification has been triggered already.
return this.isTriggered(notification, false).then((triggered) => {
// Cancel the current notification in case it gets scheduled twice.
return LocalNotifications.instance.cancel(notification.id).finally(() => {
if (!triggered) {
// Check if sound is enabled for notifications.
let promise;
const triggered = await this.isTriggered(notification, false);
if (this.canDisableSound()) {
promise = CoreConfig.instance.get(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true);
} else {
promise = Promise.resolve(true);
}
// Cancel the current notification in case it gets scheduled twice.
LocalNotifications.instance.cancel(notification.id).finally(async () => {
if (!triggered) {
let soundEnabled: boolean;
return promise.then((soundEnabled) => {
if (!soundEnabled) {
notification.sound = null;
} else {
delete notification.sound; // Use default value.
}
notification.foreground = true;
// Remove from triggered, since the notification could be in there with a different time.
this.removeTriggered(notification.id);
LocalNotifications.instance.schedule(notification);
});
// Check if sound is enabled for notifications.
if (!this.canDisableSound()) {
soundEnabled = true;
} else {
soundEnabled = await CoreConfig.instance.get(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true);
}
});
if (!soundEnabled) {
notification.sound = null;
} else {
delete notification.sound; // Use default value.
}
notification.foreground = true;
// Remove from triggered, since the notification could be in there with a different time.
this.removeTriggered(notification.id);
LocalNotifications.instance.schedule(notification);
}
});
}
@ -666,15 +677,15 @@ export class CoreLocalNotificationsProvider {
* @param notification Triggered notification.
* @return Promise resolved when stored, rejected otherwise.
*/
async trigger(notification: ILocalNotification): Promise<any> {
async trigger(notification: ILocalNotification): Promise<number> {
await this.dbReady;
const entry = {
id: notification.id,
at: notification.trigger && notification.trigger.at ? notification.trigger.at : Date.now()
at: notification.trigger && notification.trigger.at ? notification.trigger.at : Date.now(),
};
return this.appDB.insertRecord(this.TRIGGERED_TABLE, entry);
return this.appDB.insertRecord(CoreLocalNotificationsProvider.TRIGGERED_TABLE, entry);
}
/**
@ -684,14 +695,17 @@ export class CoreLocalNotificationsProvider {
* @param newName The new name.
* @return Promise resolved when done.
*/
async updateComponentName(oldName: string, newName: string): Promise<any> {
async updateComponentName(oldName: string, newName: string): Promise<void> {
await this.dbReady;
const oldId = this.COMPONENTS_TABLE + '#' + oldName,
newId = this.COMPONENTS_TABLE + '#' + newName;
const oldId = CoreLocalNotificationsProvider.COMPONENTS_TABLE + '#' + oldName;
const newId = CoreLocalNotificationsProvider.COMPONENTS_TABLE + '#' + newName;
return this.appDB.updateRecords(this.COMPONENTS_TABLE, {id: newId}, {id: oldId});
await this.appDB.updateRecords(CoreLocalNotificationsProvider.COMPONENTS_TABLE, { id: newId }, { id: oldId });
}
}
export class CoreLocalNotifications extends makeSingleton(CoreLocalNotificationsProvider) {}
export type CoreLocalNotificationsClickCallback<T = unknown> = (value: T) => void;

View File

@ -15,7 +15,7 @@
import { Injectable } from '@angular/core';
import { FileEntry } from '@ionic-native/file';
import { CoreFilepool } from '@services/filepool';
import { CoreFilepool, CoreFilepoolOnProgressCallback } from '@services/filepool';
import { CoreWSExternalFile } from '@services/ws';
import { CoreConstants } from '@core/constants';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
@ -26,6 +26,7 @@ import { makeSingleton } from '@singletons/core.singletons';
*/
@Injectable()
export class CorePluginFileDelegate extends CoreDelegate {
protected handlerNameProperty = 'component';
constructor() {
@ -40,14 +41,12 @@ export class CorePluginFileDelegate extends CoreDelegate {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<any> {
const handler = this.getHandlerForFile({fileurl: fileUrl});
async fileDeleted(fileUrl: string, path: string, siteId?: string): Promise<void> {
const handler = this.getHandlerForFile({ fileurl: fileUrl });
if (handler && handler.fileDeleted) {
return handler.fileDeleted(fileUrl, path, siteId);
await handler.fileDeleted(fileUrl, path, siteId);
}
return Promise.resolve();
}
/**
@ -71,9 +70,8 @@ export class CorePluginFileDelegate extends CoreDelegate {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the file to use. Rejected if cannot download.
*/
protected async getHandlerDownloadableFile(file: CoreWSExternalFile, handler: CorePluginFileHandler, siteId?: string)
: Promise<CoreWSExternalFile> {
protected async getHandlerDownloadableFile(file: CoreWSExternalFile, handler: CorePluginFileHandler, siteId?: string):
Promise<CoreWSExternalFile> {
const isDownloadable = await this.isFileDownloadable(file, siteId);
if (!isDownloadable.downloadable) {
@ -132,7 +130,7 @@ export class CorePluginFileDelegate extends CoreDelegate {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with file size and a boolean to indicate if it is the total size or only partial.
*/
async getFilesDownloadSize(files: CoreWSExternalFile[], siteId?: string): Promise<{ size: number, total: boolean }> {
async getFilesDownloadSize(files: CoreWSExternalFile[], siteId?: string): Promise<{ size: number; total: boolean }> {
const filteredFiles = [];
await Promise.all(files.map(async (file) => {
@ -153,10 +151,10 @@ export class CorePluginFileDelegate extends CoreDelegate {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with file size and a boolean to indicate if it is the total size or only partial.
*/
async getFilesSize(files: CoreWSExternalFile[], siteId?: string): Promise<{ size: number, total: boolean }> {
async getFilesSize(files: CoreWSExternalFile[], siteId?: string): Promise<{ size: number; total: boolean }> {
const result = {
size: 0,
total: true
total: true,
};
await Promise.all(files.map(async (file) => {
@ -231,7 +229,7 @@ export class CorePluginFileDelegate extends CoreDelegate {
* @param siteId Site ID. If not defined, current site.
* @return Promise with the data.
*/
isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise<CorePluginFileDownloadableResult> {
async isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise<CorePluginFileDownloadableResult> {
const handler = this.getHandlerForFile(file);
if (handler && handler.isFileDownloadable) {
@ -239,7 +237,7 @@ export class CorePluginFileDelegate extends CoreDelegate {
}
// Default to true.
return Promise.resolve({downloadable: true});
return { downloadable: true };
}
/**
@ -272,15 +270,15 @@ export class CorePluginFileDelegate extends CoreDelegate {
* @param onProgress Function to call on progress.
* @return Promise resolved when done.
*/
treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: (event: any) => any): Promise<any> {
const handler = this.getHandlerForFile({fileurl: fileUrl});
async treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: CoreFilepoolOnProgressCallback):
Promise<void> {
const handler = this.getHandlerForFile({ fileurl: fileUrl });
if (handler && handler.treatDownloadedFile) {
return handler.treatDownloadedFile(fileUrl, file, siteId, onProgress);
await handler.treatDownloadedFile(fileUrl, file, siteId, onProgress);
}
return Promise.resolve();
}
}
export class CorePluginFile extends makeSingleton(CorePluginFileDelegate) {}
@ -320,7 +318,7 @@ export interface CorePluginFileHandler extends CoreDelegateHandler {
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
fileDeleted?(fileUrl: string, path: string, siteId?: string): Promise<any>;
fileDeleted?(fileUrl: string, path: string, siteId?: string): Promise<void>;
/**
* Check whether a file can be downloaded. If so, return the file to download.
@ -375,7 +373,8 @@ export interface CorePluginFileHandler extends CoreDelegateHandler {
* @param onProgress Function to call on progress.
* @return Promise resolved when done.
*/
treatDownloadedFile?(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: (event: any) => any): Promise<any>;
treatDownloadedFile?(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: CoreFilepoolOnProgressCallback):
Promise<void>;
}
/**

View File

@ -14,7 +14,7 @@
import { Injectable } from '@angular/core';
import * as moment from 'moment';
import moment, { LongDateFormatKey } from 'moment';
import { CoreConstants } from '@core/constants';
import { makeSingleton, Translate } from '@singletons/core.singletons';
@ -24,7 +24,7 @@ import { makeSingleton, Translate } from '@singletons/core.singletons';
@Injectable()
export class CoreTimeUtilsProvider {
protected FORMAT_REPLACEMENTS = { // To convert PHP strf format to Moment format.
protected static readonly FORMAT_REPLACEMENTS = { // To convert PHP strf format to Moment format.
'%a': 'ddd',
'%A': 'dddd',
'%d': 'DD',
@ -65,7 +65,7 @@ export class CoreTimeUtilsProvider {
'%x': 'L',
'%n': '\n',
'%t': '\t',
'%%': '%'
'%%': '%',
};
/**
@ -97,7 +97,8 @@ export class CoreTimeUtilsProvider {
converted += ']';
}
converted += typeof this.FORMAT_REPLACEMENTS[char] != 'undefined' ? this.FORMAT_REPLACEMENTS[char] : char;
converted += typeof CoreTimeUtilsProvider.FORMAT_REPLACEMENTS[char] != 'undefined' ?
CoreTimeUtilsProvider.FORMAT_REPLACEMENTS[char] : char;
} else {
// Not a PHP format. We need to escape them, otherwise the letters could be confused with Moment formats.
if (!escaping) {
@ -129,7 +130,7 @@ export class CoreTimeUtilsProvider {
}
// The component ion-datetime doesn't support escaping characters ([]), so we remove them.
let fixed = format.replace(/[\[\]]/g, '');
let fixed = format.replace(/[[\]]/g, '');
if (fixed.indexOf('A') != -1) {
// Do not use am/pm format because there is a bug in ion-datetime.
@ -250,7 +251,6 @@ export class CoreTimeUtilsProvider {
* @return Duration in a short human readable format.
*/
formatDurationShort(duration: number): string {
const minutes = Math.floor(duration / 60);
const seconds = duration - minutes * 60;
const durations = [];
@ -346,7 +346,7 @@ export class CoreTimeUtilsProvider {
* @param localizedFormat Format to use.
* @return Localized ISO format
*/
getLocalizedDateFormat(localizedFormat: any): string {
getLocalizedDateFormat(localizedFormat: LongDateFormatKey): string {
return moment.localeData().longDateFormat(localizedFormat);
}
@ -366,6 +366,7 @@ export class CoreTimeUtilsProvider {
return moment().startOf('day').unix();
}
}
}
export class CoreTimeUtils extends makeSingleton(CoreTimeUtilsProvider) {}

View File

@ -55,7 +55,7 @@ export class CoreUrlUtilsProvider {
* @param boolToNumber Whether to convert bools to 1 or 0.
* @return URL with params.
*/
addParamsToUrl(url: string, params?: {[key: string]: any}, anchor?: string, boolToNumber?: boolean): string {
addParamsToUrl(url: string, params?: CoreUrlParams, anchor?: string, boolToNumber?: boolean): string {
let separator = url.indexOf('?') != -1 ? '&' : '?';
for (const key in params) {
@ -63,7 +63,7 @@ export class CoreUrlUtilsProvider {
if (boolToNumber && typeof value == 'boolean') {
// Convert booleans to 1 or 0.
value = value ? 1 : 0;
value = value ? '1' : '0';
}
// Ignore objects.
@ -102,7 +102,7 @@ export class CoreUrlUtilsProvider {
canUseTokenPluginFile(url: string, siteUrl: string, accessKey?: string): boolean {
// Do not use tokenpluginfile if site doesn't use slash params, the URL doesn't work.
// Also, only use it for "core" pluginfile endpoints. Some plugins can implement their own endpoint (like customcert).
return accessKey && !url.match(/[\&?]file=/) && (
return accessKey && !url.match(/[&?]file=/) && (
url.indexOf(CoreTextUtils.instance.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 ||
url.indexOf(CoreTextUtils.instance.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0);
}
@ -113,13 +113,13 @@ export class CoreUrlUtilsProvider {
* @param url URL to treat.
* @return Object with the params.
*/
extractUrlParams(url: string): any {
extractUrlParams(url: string): CoreUrlParams {
const regex = /[?&]+([^=&]+)=?([^&]*)?/gi;
const subParamsPlaceholder = '@@@SUBPARAMS@@@';
const params: any = {};
const params: CoreUrlParams = {};
const urlAndHash = url.split('#');
const questionMarkSplit = urlAndHash[0].split('?');
let subParams;
let subParams: string;
if (questionMarkSplit.length > 2) {
// There is more than one question mark in the URL. This can happen if any of the params is a URL with params.
@ -190,10 +190,10 @@ export class CoreUrlUtilsProvider {
url = url.replace('/pluginfile', '/webservice/pluginfile');
}
url = this.addParamsToUrl(url, {token});
url = this.addParamsToUrl(url, { token });
}
return this.addParamsToUrl(url, {offline: 1}); // Always send offline=1 (it's for external repositories).
return this.addParamsToUrl(url, { offline: '1' }); // Always send offline=1 (it's for external repositories).
}
/**
@ -206,7 +206,7 @@ export class CoreUrlUtilsProvider {
url = url.trim();
// Check if the URL starts by http or https.
if (! /^http(s)?\:\/\/.*/i.test(url)) {
if (! /^http(s)?:\/\/.*/i.test(url)) {
// Test first allways https.
url = 'https://' + url;
}
@ -228,7 +228,7 @@ export class CoreUrlUtilsProvider {
* @param page Docs page to go to.
* @return Promise resolved with the Moodle docs URL.
*/
getDocsUrl(release?: string, page: string = 'Mobile_app'): Promise<string> {
async getDocsUrl(release?: string, page: string = 'Mobile_app'): Promise<string> {
let docsUrl = 'https://docs.moodle.org/en/' + page;
if (typeof release != 'undefined') {
@ -240,11 +240,13 @@ export class CoreUrlUtilsProvider {
}
}
return CoreLang.instance.getCurrentLanguage().then((lang) => {
try {
const lang = await CoreLang.instance.getCurrentLanguage();
return docsUrl.replace('/en/', '/' + lang + '/');
}).catch(() => {
} catch (error) {
return docsUrl;
});
}
}
/**
@ -258,13 +260,13 @@ export class CoreUrlUtilsProvider {
return;
}
let videoId;
const params: any = {};
let videoId: string;
const params: CoreUrlParams = {};
url = CoreTextUtils.instance.decodeHTML(url);
// Get the video ID.
let match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/);
let match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/);
if (match && match[2].length === 11) {
videoId = match[2];
@ -276,7 +278,7 @@ export class CoreUrlUtilsProvider {
}
// Now get the playlist (if any).
match = url.match(/[?&]list=([^#\&\?]+)/);
match = url.match(/[?&]list=([^#&?]+)/);
if (match && match[1]) {
params.list = match[1];
@ -286,13 +288,15 @@ export class CoreUrlUtilsProvider {
match = url.match(/[?&]start=(\d+)/);
if (match && match[1]) {
params.start = parseInt(match[1], 10);
params.start = parseInt(match[1], 10).toString();
} else {
// No start param, but it could have a time param.
match = url.match(/[?&]t=(\d+h)?(\d+m)?(\d+s)?/);
if (match) {
params.start = (match[1] ? parseInt(match[1], 10) * 3600 : 0) + (match[2] ? parseInt(match[2], 10) * 60 : 0) +
(match[3] ? parseInt(match[3], 10) : 0);
const start = (match[1] ? parseInt(match[1], 10) * 3600 : 0) +
(match[2] ? parseInt(match[2], 10) * 60 : 0) +
(match[3] ? parseInt(match[3], 10) : 0);
params.start = start.toString();
}
}
@ -328,7 +332,7 @@ export class CoreUrlUtilsProvider {
return;
}
const matches = url.match(/^([^\/:\.\?]*):\/\//);
const matches = url.match(/^([^/:.?]*):\/\//);
if (matches && matches[1]) {
return matches[1];
}
@ -361,11 +365,11 @@ export class CoreUrlUtilsProvider {
getUsernameFromUrl(url: string): string {
if (url.indexOf('@') > -1) {
// Get URL without protocol.
const withoutProtocol = url.replace(/^[^?@\/]*:\/\//, '');
const withoutProtocol = url.replace(/^[^?@/]*:\/\//, '');
const matches = withoutProtocol.match(/[^@]*/);
// Make sure that @ is at the start of the URL, not in a param at the end.
if (matches && matches.length && !matches[0].match(/[\/|?]/)) {
if (matches && matches.length && !matches[0].match(/[/|?]/)) {
return matches[0];
}
}
@ -408,7 +412,7 @@ export class CoreUrlUtilsProvider {
* @return Whether the url uses http or https protocol.
*/
isHttpURL(url: string): boolean {
return /^https?\:\/\/.+/i.test(url);
return /^https?:\/\/.+/i.test(url);
}
/**
@ -427,10 +431,11 @@ export class CoreUrlUtilsProvider {
* Check whether a URL scheme belongs to a local file.
*
* @param scheme Scheme to check.
* @param domain The domain. Needed because in Android the WebView scheme is http.
* @param notUsed Unused parameter.
* @return Whether the scheme belongs to a local file.
*/
isLocalFileUrlScheme(scheme: string, domain: string): boolean {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isLocalFileUrlScheme(scheme: string, notUsed?: string): boolean {
if (scheme) {
scheme = scheme.toLowerCase();
}
@ -483,7 +488,7 @@ export class CoreUrlUtilsProvider {
* @return URL without params.
*/
removeUrlParams(url: string): string {
const matches = url.match(/^[^\?]+/);
const matches = url.match(/^[^?]+/);
return matches && matches[0];
}
@ -515,6 +520,9 @@ export class CoreUrlUtilsProvider {
return url;
}
}
export class CoreUrlUtils extends makeSingleton(CoreUrlUtilsProvider) {}
export type CoreUrlParams = {[key: string]: string};

View File

@ -27,12 +27,18 @@ import { environment } from '@/environments/environment';
* Then you can call the log function you want to use in this logger instance.
*/
export class CoreLogger {
log: LogFunction;
info: LogFunction;
warn: LogFunction;
debug: LogFunction;
error: LogFunction;
// Avoid creating singleton instances.
private constructor() {
// Nothing to do.
}
/**
* Get a logger instance for a certain class, service or component.
*
@ -88,6 +94,7 @@ export class CoreLogger {
logFn.apply(null, args);
};
}
}
/**

View File

@ -72,7 +72,9 @@ interface UrlParts {
export class CoreUrl {
// Avoid creating singleton instances.
private constructor() {}
private constructor() {
// Nothing to do.
}
/**
* Parse parts of a url, using an implicit protocol if it is missing from the url.
@ -123,14 +125,14 @@ export class CoreUrl {
// Match using common suffixes.
const knownSuffixes = [
'\/my\/?',
'\/\\\?redirect=0',
'\/index\\\.php',
'\/course\/view\\\.php',
'\/login\/index\\\.php',
'\/mod\/page\/view\\\.php',
'/my/?',
'/\\?redirect=0',
'/index\\.php',
'/course/view\\.php',
'\\/login/index\\.php',
'/mod/page/view\\.php',
];
const match = url.match(new RegExp(`^https?:\/\/(.*?)(${knownSuffixes.join('|')})`));
const match = url.match(new RegExp(`^https?://(.*?)(${knownSuffixes.join('|')})`));
if (match) {
return match[1];
@ -184,10 +186,10 @@ export class CoreUrl {
*/
static sameDomainAndPath(urlA: string, urlB: string): boolean {
// Add protocol if missing, the parse function requires it.
if (!urlA.match(/^[^\/:\.\?]*:\/\//)) {
if (!urlA.match(/^[^/:.?]*:\/\//)) {
urlA = `https://${urlA}`;
}
if (!urlB.match(/^[^\/:\.\?]*:\/\//)) {
if (!urlB.match(/^[^/:.?]*:\/\//)) {
urlB = `https://${urlB}`;
}
@ -197,4 +199,5 @@ export class CoreUrl {
return partsA.domain == partsB.domain &&
CoreTextUtils.instance.removeEndingSlash(partsA.path) == CoreTextUtils.instance.removeEndingSlash(partsB.path);
}
}

View File

@ -32,6 +32,11 @@ export type CoreWindowOpenOptions = {
*/
export class CoreWindow {
// Avoid creating singleton instances.
private constructor() {
// Nothing to do.
}
/**
* "Safe" implementation of window.open. It will open the URL without overriding the app.
*
@ -73,4 +78,5 @@ export class CoreWindow {
}
}
}
}

Binary file not shown.

View File

@ -9,6 +9,7 @@
* https://ionicframework.com/docs/layout/global-stylesheets
*/
/* Core CSS required for Ionic components to work properly */
@import "~@ionic/angular/css/core.css";
@ -24,3 +25,6 @@
@import "~@ionic/angular/css/text-alignment.css";
@import "~@ionic/angular/css/text-transformation.css";
@import "~@ionic/angular/css/flex-utils.css";
/* Font awesome */
@import "~font-awesome/scss/font-awesome.scss";

View File

@ -24,3 +24,10 @@ declare global {
}
}
/**
* Course base definition.
*/
export type CoreCourseBase = {
id: number; // Course Id.
};