commit
fb047a6e35
|
@ -1,5 +1,5 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<widget android-versionCode="39000" id="com.moodle.moodlemobile" ios-CFBundleVersion="3.9.0.0" version="3.9.0" versionCode="39000" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<widget android-versionCode="39000" id="com.moodle.moodlemobile" ios-CFBundleVersion="3.9.1.0" version="3.9.1" versionCode="39100" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<name>Moodle</name>
|
||||
<description>Moodle official app</description>
|
||||
<author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author>
|
||||
|
@ -241,7 +241,7 @@
|
|||
<true />
|
||||
</edit-config>
|
||||
<edit-config file="*-Info.plist" mode="merge" target="CFBundleShortVersionString">
|
||||
<string>3.9.0</string>
|
||||
<string>3.9.1</string>
|
||||
</edit-config>
|
||||
<config-file parent="FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED" target="*-Info.plist">
|
||||
<string>YES</string>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<Identity Name="3312ADB7.MoodleDesktop"
|
||||
ProcessorArchitecture="x64"
|
||||
Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6"
|
||||
Version="3.9.0.0" />
|
||||
Version="3.9.1.0" />
|
||||
<Properties>
|
||||
<DisplayName>Moodle Desktop</DisplayName>
|
||||
<PublisherDisplayName>Moodle Pty Ltd.</PublisherDisplayName>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "moodlemobile",
|
||||
"version": "3.9.0",
|
||||
"version": "3.9.1",
|
||||
"description": "The official app for Moodle.",
|
||||
"author": {
|
||||
"name": "Moodle Pty Ltd.",
|
||||
|
@ -253,7 +253,7 @@
|
|||
"category": "public.app-category.education",
|
||||
"icon": "resources/desktop/icon.icns",
|
||||
"target": "mas",
|
||||
"bundleVersion": "3.9.0",
|
||||
"bundleVersion": "3.9.1",
|
||||
"extendInfo": {
|
||||
"ElectronTeamID": "2NU57U5PAW"
|
||||
}
|
||||
|
|
|
@ -120,8 +120,6 @@ function add_langs_to_config($langs, $config) {
|
|||
function get_langfolder($lang) {
|
||||
$folder = LANGPACKSFOLDER.'/'.str_replace('-', '_', $lang);
|
||||
if (!is_dir($folder) || !is_file($folder.'/langconfig.php')) {
|
||||
echo "Cannot translate $folder, folder not found";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -173,6 +171,8 @@ function reset_translations_strings() {
|
|||
function build_lang($lang, $keys) {
|
||||
$langfoldername = get_langfolder($lang);
|
||||
if (!$langfoldername) {
|
||||
echo "Cannot translate $lang, folder not found";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -182,15 +182,18 @@ function build_lang($lang, $keys) {
|
|||
$override_langfolder = false;
|
||||
}
|
||||
|
||||
$total = count ($keys);
|
||||
$total = count($keys);
|
||||
$local = 0;
|
||||
|
||||
$string = get_translation_strings($langfoldername, 'langconfig');
|
||||
$parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : "";
|
||||
$langparts = explode('-', $lang, 2);
|
||||
$parentname = $langparts[0] ?? "";
|
||||
$parent = "";
|
||||
|
||||
echo "Processing $lang";
|
||||
if ($parent != "" && $parent != $lang) {
|
||||
echo " ($parent)";
|
||||
// Check parent language exists.
|
||||
if ($parentname != $lang && get_langfolder($parentname)) {
|
||||
echo " ($parentname)";
|
||||
$parent = $parentname;
|
||||
}
|
||||
|
||||
$langFile = false;
|
||||
|
@ -247,6 +250,12 @@ function build_lang($lang, $keys) {
|
|||
$translations[$key] = html_entity_decode($text);
|
||||
}
|
||||
|
||||
if (!empty($parent)) {
|
||||
$translations['core.parentlanguage'] = $parent;
|
||||
} else if (isset($translations['core.parentlanguage'])) {
|
||||
unset($translations['core.parentlanguage']);
|
||||
}
|
||||
|
||||
// Sort and save.
|
||||
ksort($translations);
|
||||
file_put_contents(ASSETSPATH.$lang.'.json', str_replace('\/', '/', json_encode($translations, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)));
|
||||
|
@ -275,6 +284,8 @@ function progressbar($percentage) {
|
|||
function detect_lang($lang, $keys) {
|
||||
$langfoldername = get_langfolder($lang);
|
||||
if (!$langfoldername) {
|
||||
echo "Cannot translate $lang, folder not found";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -1913,7 +1913,6 @@
|
|||
"core.openmodinbrowser": "Obre {{$a}} al navegador",
|
||||
"core.othergroups": "Altres grups",
|
||||
"core.pagea": "Pàgina {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "Utilitzeu el botó de baix per pagar i inscriure-us.",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Telèfon",
|
||||
|
|
|
@ -1909,7 +1909,6 @@
|
|||
"core.openmodinbrowser": "Otevřít {{$a}} v prohlížeči",
|
||||
"core.othergroups": "Další skupiny",
|
||||
"core.pagea": "Stránka {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "Pomocí tlačítka níže můžete provést platbu a během několika minut se zapsat do kurzu!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Telefon",
|
||||
|
|
|
@ -1752,7 +1752,6 @@
|
|||
"core.openinbrowser": "Åben i browser",
|
||||
"core.othergroups": "Andre grupper",
|
||||
"core.pagea": "Side {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "Brug knappen forneden til at betale og blive tilmeldt umiddelbart derefter.",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Telefon",
|
||||
|
|
|
@ -1841,7 +1841,6 @@
|
|||
"core.openinbrowser": "Ανοίξτε στον περιηγητή.",
|
||||
"core.othergroups": "Άλλες ομάδες",
|
||||
"core.pagea": "Σελίδα {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "Χρησιμοποιήστε το παρακάτω πλήκτρο για να πληρώσετε και να εγγραφείτε μέσα σε λίγα λεπτά!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Τηλέφωνο",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"core.listsep": ",",
|
||||
"core.login.loginsteps": "For full access to this site, you first need to create an account.",
|
||||
"core.notenrolledprofile": "This profile is not available because this user is not enrolled in this course.",
|
||||
"core.parentlanguage": "en",
|
||||
"core.paymentinstant": "Use the button below to pay and be enrolled within minutes!",
|
||||
"core.settings.license": "License",
|
||||
"core.strftimedate": "%B %d, %Y",
|
||||
|
|
|
@ -1557,6 +1557,7 @@
|
|||
"core.errorsomedatanotdownloaded": "If you downloaded this activity, please notice that some data isn't downloaded during the download process for performance and data usage reasons.",
|
||||
"core.errorsync": "An error occurred while synchronising. Please try again.",
|
||||
"core.errorsyncblocked": "This {{$a}} cannot be synchronised right now because of an ongoing process. Please try again later. If the problem persists, try restarting the app.",
|
||||
"core.errorurlschemeinvalidsite": "This site URL cannot be opened in this app.",
|
||||
"core.explanationdigitalminor": "This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.",
|
||||
"core.favourites": "Starred",
|
||||
"core.filename": "Filename",
|
||||
|
|
|
@ -1913,7 +1913,7 @@
|
|||
"core.openmodinbrowser": "Abrir {{$a}} en navegador",
|
||||
"core.othergroups": "Otros grupos",
|
||||
"core.pagea": "Página {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.parentlanguage": "es",
|
||||
"core.paymentinstant": "¡Utilice el botón de abajo para pagar y poder inscribirse en minutos!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Teléfono",
|
||||
|
|
|
@ -1913,7 +1913,6 @@
|
|||
"core.openmodinbrowser": "Abrir {{$a}} en el navegador",
|
||||
"core.othergroups": "Otros grupos",
|
||||
"core.pagea": "Página {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "¡Utilice el botón de abajo para pagar y poder matricularse en minutos!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Teléfono",
|
||||
|
|
|
@ -1875,7 +1875,6 @@
|
|||
"core.openinbrowser": "Ouvrir dans le navigateur",
|
||||
"core.othergroups": "Autres groupes",
|
||||
"core.pagea": "Page {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "Le bouton ci-dessous vous permet de payer et de vous inscrire en quelques minutes !",
|
||||
"core.percentagenumber": "{{$a}} %",
|
||||
"core.phone": "Téléphone",
|
||||
|
|
|
@ -1521,7 +1521,6 @@
|
|||
"core.online": "Online",
|
||||
"core.othergroups": "Egyéb csoportok",
|
||||
"core.pagea": "{{$a}} oldal",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "A fizetéshez és a perceken belüli beiratkozáshoz használja az alábbi gombot!",
|
||||
"core.phone": "Telefon",
|
||||
"core.pictureof": "Kép",
|
||||
|
|
|
@ -1769,7 +1769,6 @@
|
|||
"core.openinbrowser": "Apri nel browser",
|
||||
"core.othergroups": "Altri gruppi",
|
||||
"core.pagea": "Pagina {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "Utilizza il pulsante sottostante per pagare ed essere iscritto in pochi minuti!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Telefono",
|
||||
|
|
|
@ -1717,7 +1717,6 @@
|
|||
"core.openinbrowser": "ブラウザで開く",
|
||||
"core.othergroups": "他のグループ",
|
||||
"core.pagea": "ページ {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "下のボタンをお使いください。支払いおよび登録がすぐに完了します!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "電話",
|
||||
|
|
|
@ -1385,7 +1385,6 @@
|
|||
"core.openfullimage": "전체 크기 이미지를 보려면 여기를 클릭하십시오.",
|
||||
"core.openinbrowser": "브라우저에서 열기",
|
||||
"core.pagea": "페이지 {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "신속하게 등록금 지불 및 등록을 마치려면 아래의 버튼을 사용하시오!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "전화",
|
||||
|
|
|
@ -1634,7 +1634,6 @@
|
|||
"core.openinbrowser": "Atidaryti naršyklėje",
|
||||
"core.othergroups": "Kitos grupės",
|
||||
"core.pagea": "{{$a}} puslapis",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "Naudokite toliau pateiktą mygtuką, kad sumokėtumėte ir būtumėte įregistruoti per kelias minutes.",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Telefonas",
|
||||
|
|
|
@ -1576,7 +1576,6 @@
|
|||
"core.online": "På nett",
|
||||
"core.othergroups": "Andre grupper",
|
||||
"core.pagea": "Side {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "Bruk knappen under for å betale og melde deg på kurset.",
|
||||
"core.phone": "Telefon",
|
||||
"core.pictureof": "Bilde av {{$a}}",
|
||||
|
|
|
@ -1809,7 +1809,7 @@
|
|||
"core.openinbrowser": "Abrir no navegador",
|
||||
"core.othergroups": "Outros grupos",
|
||||
"core.pagea": "Página {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.parentlanguage": "pt",
|
||||
"core.paymentinstant": "Clique o botão abaixo para efetuar o pagamento e fazer a sua inscrição em poucos minutos!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Fone",
|
||||
|
|
|
@ -1913,7 +1913,6 @@
|
|||
"core.openmodinbrowser": "Abrir {{$a}} no navegador",
|
||||
"core.othergroups": "Outros grupos",
|
||||
"core.pagea": "Página {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "Use o botão abaixo para pagar e completar a inscrição!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Telefone",
|
||||
|
|
|
@ -1841,7 +1841,6 @@
|
|||
"core.openinbrowser": "Odpri v brskalniku",
|
||||
"core.othergroups": "Ostale skupine",
|
||||
"core.pagea": "Stran {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "Uporabite spodnje gumbe za plačilo in vpis v nekaj minutah!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Telefon",
|
||||
|
|
|
@ -1700,7 +1700,6 @@
|
|||
"core.openinbrowser": "Отвори у веб читачу",
|
||||
"core.othergroups": "Друге групе",
|
||||
"core.pagea": "Страница {{$a}}",
|
||||
"core.parentlanguage": "en",
|
||||
"core.paymentinstant": "Употребите дугме испод како бисте извршили уплату и уписали курс у року од неколико минута!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Телефон",
|
||||
|
|
|
@ -1700,7 +1700,6 @@
|
|||
"core.openinbrowser": "Otvori u veb čitaču",
|
||||
"core.othergroups": "Druge grupe",
|
||||
"core.pagea": "Stranica {{$a}}",
|
||||
"core.parentlanguage": "en",
|
||||
"core.paymentinstant": "Upotrebite dugme ispod kako biste izvršili uplatu i upisali kurs u roku od nekoliko minuta!",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "Telefon",
|
||||
|
|
|
@ -1844,7 +1844,6 @@
|
|||
"core.openinbrowser": "在浏览器中打开",
|
||||
"core.othergroups": "其他小组",
|
||||
"core.pagea": "页 {{$a}}",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "点击下面的按钮便可以快速付费并加入课程!",
|
||||
"core.phone": "电话",
|
||||
"core.pictureof": "{{$a}}的头像",
|
||||
|
|
|
@ -1636,7 +1636,6 @@
|
|||
"core.openinbrowser": "以瀏覽器開啟",
|
||||
"core.othergroups": "其他群組",
|
||||
"core.pagea": "第 {{$a}} 頁",
|
||||
"core.parentlanguage": "",
|
||||
"core.paymentinstant": "使用以下按鈕立即付款及註冊。",
|
||||
"core.percentagenumber": "{{$a}}%",
|
||||
"core.phone": "電話",
|
||||
|
|
|
@ -21,9 +21,9 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
|
|||
/**
|
||||
* Component to show a loading spinner and message while data is being loaded.
|
||||
*
|
||||
* It will show a spinner with a message and hide all the content until 'dataLoaded' variable is set to true.
|
||||
* If 'message' and 'dynMessage' attributes aren't set, default message "Loading" is shown.
|
||||
* 'message' attribute accepts hardcoded strings, variables, filters, etc. E.g. message="'core.loading' | translate".
|
||||
* It will show a spinner with a message and hide all the content until 'hideUntil' variable is set to a truthy value (!!hideUntil).
|
||||
* If 'message' isn't set, default message "Loading" is shown.
|
||||
* 'message' attribute accepts hardcoded strings, variables, filters, etc. E.g. [message]="'core.loading' | translate".
|
||||
*
|
||||
* Usage:
|
||||
* <core-loading [message]="loadingMessage" [hideUntil]="dataLoaded">
|
||||
|
@ -44,7 +44,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
|
|||
animations: [coreShowHideAnimation]
|
||||
})
|
||||
export class CoreLoadingComponent implements OnInit, OnChanges {
|
||||
@Input() hideUntil: boolean; // Determine when should the contents be shown.
|
||||
@Input() hideUntil: any; // Determine when should the contents be shown.
|
||||
@Input() message?: string; // Message to show while loading.
|
||||
@ViewChild('content') content: ElementRef;
|
||||
|
||||
|
@ -69,7 +69,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
// Add class if loaded on init.
|
||||
if (this.hideUntil) {
|
||||
if (!!this.hideUntil) {
|
||||
this.element.classList.add('core-loading-loaded');
|
||||
this.content.nativeElement.classList.add('core-loading-content');
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges {
|
|||
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (changes.hideUntil) {
|
||||
if (changes.hideUntil.currentValue === true) {
|
||||
if (!!this.hideUntil) {
|
||||
setTimeout(() => {
|
||||
// Content is loaded so, center the spinner on the content itself.
|
||||
this.element.classList.add('core-loading-loaded');
|
||||
|
@ -96,7 +96,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges {
|
|||
// Trigger the event after a timeout since the elements inside ngIf haven't been added to DOM yet.
|
||||
setTimeout(() => {
|
||||
this.eventsProvider.trigger(CoreEventsProvider.CORE_LOADING_CHANGED, {
|
||||
loaded: changes.hideUntil.currentValue,
|
||||
loaded: !!this.hideUntil,
|
||||
uniqueId: this.uniqueId
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"app_id": "com.moodle.moodlemobile",
|
||||
"appname": "Moodle Mobile",
|
||||
"desktopappname": "Moodle Desktop",
|
||||
"versioncode": 3900,
|
||||
"versionname": "3.9.0",
|
||||
"versioncode": 3910,
|
||||
"versionname": "3.9.1",
|
||||
"cache_update_frequency_usually": 420000,
|
||||
"cache_update_frequency_often": 1200000,
|
||||
"cache_update_frequency_sometimes": 3600000,
|
||||
|
@ -81,6 +81,7 @@
|
|||
"siteurl": "",
|
||||
"sitename": "",
|
||||
"multisitesdisplay": "",
|
||||
"onlyallowlistedsites": false,
|
||||
"skipssoconfirmation": false,
|
||||
"forcedefaultlanguage": false,
|
||||
"privacypolicy": "https:\/\/moodle.net\/moodle-app-privacy\/",
|
||||
|
|
|
@ -105,4 +105,10 @@ ion-app.app-root page-core-login-site {
|
|||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.core-login-site-qrcode-separator {
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,16 @@
|
|||
<div padding>
|
||||
<button ion-button block [disabled]="siteChecked && !isBrowserSSO && !credForm.valid" class="core-login-login-button">{{ 'core.login.loginbutton' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="showScanQR">
|
||||
<div class="core-login-site-qrcode-separator">{{ 'core.login.or' | translate }}</div>
|
||||
<ion-item class="core-login-site-qrcode" no-lines>
|
||||
<a ion-button block color="light" margin-top icon-start text-wrap (click)="showInstructionsAndScanQR()">
|
||||
<core-icon name="fa-qrcode" aria-hidden="true"></core-icon>
|
||||
{{ 'core.scanqr' | translate }}
|
||||
</a>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</form>
|
||||
|
||||
<!-- Forgotten password button. -->
|
||||
|
|
|
@ -16,12 +16,14 @@ import { Component, ViewChild, ElementRef } from '@angular/core';
|
|||
import { IonicPage, NavController, NavParams } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreUtils } from '@providers/utils/utils';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreLoginHelperProvider } from '../../providers/helper';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { CoreConfigConstants } from '../../../../configconstants';
|
||||
import { CoreCustomURLSchemes } from '@providers/urlschemes';
|
||||
|
||||
/**
|
||||
* Page to enter the user credentials.
|
||||
|
@ -47,6 +49,7 @@ export class CoreLoginCredentialsPage {
|
|||
isBrowserSSO = false;
|
||||
isFixedUrlSet = false;
|
||||
showForgottenPassword = true;
|
||||
showScanQR: boolean;
|
||||
|
||||
protected siteConfig;
|
||||
protected eventThrown = false;
|
||||
|
@ -74,6 +77,17 @@ export class CoreLoginCredentialsPage {
|
|||
username: [navParams.get('username') || '', Validators.required],
|
||||
password: ['', Validators.required]
|
||||
});
|
||||
|
||||
const canScanQR = CoreUtils.instance.canScanQR();
|
||||
if (canScanQR) {
|
||||
if (typeof CoreConfigConstants['displayqroncredentialscreen'] == 'undefined') {
|
||||
this.showScanQR = this.loginHelper.isFixedUrlSet();
|
||||
} else {
|
||||
this.showScanQR = !!CoreConfigConstants['displayqroncredentialscreen'];
|
||||
}
|
||||
} else {
|
||||
this.showScanQR = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -267,4 +281,46 @@ export class CoreLoginCredentialsPage {
|
|||
signup(): void {
|
||||
this.navCtrl.push('CoreLoginEmailSignupPage', { siteUrl: this.siteUrl });
|
||||
}
|
||||
|
||||
/**
|
||||
* Show instructions and scan QR code.
|
||||
*/
|
||||
showInstructionsAndScanQR(): void {
|
||||
// Show some instructions first.
|
||||
this.domUtils.showAlertWithOptions({
|
||||
title: this.translate.instant('core.login.faqwhereisqrcode'),
|
||||
message: this.translate.instant('core.login.faqwhereisqrcodeanswer',
|
||||
{$image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML}),
|
||||
buttons: [
|
||||
{
|
||||
text: this.translate.instant('core.cancel'),
|
||||
role: 'cancel'
|
||||
},
|
||||
{
|
||||
text: this.translate.instant('core.next'),
|
||||
handler: (): void => {
|
||||
this.scanQR();
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a QR code and put its text in the URL input.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async scanQR(): Promise<void> {
|
||||
// Scan for a QR code.
|
||||
const text = await CoreUtils.instance.scanQR();
|
||||
|
||||
if (text && CoreCustomURLSchemes.instance.isCustomURL(text)) {
|
||||
try {
|
||||
await CoreCustomURLSchemes.instance.handleCustomURL(text);
|
||||
} catch (error) {
|
||||
CoreCustomURLSchemes.instance.treatHandleCustomURLError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,16 +14,22 @@
|
|||
<div text-center padding margin-bottom [class.hidden]="hasSites || enteredSiteUrl" class="core-login-site-logo">
|
||||
<img src="assets/img/login_logo.png" class="avatar-full login-logo" role="presentation">
|
||||
</div>
|
||||
<form ion-list [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites || fixedDisplay == 'select'" #siteFormEl>
|
||||
<form ion-list [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites || siteSelector == 'select'" #siteFormEl>
|
||||
<!-- Form to input the site URL if there are no fixed sites. -->
|
||||
<ng-container *ngIf="!fixedSites">
|
||||
<ng-container *ngIf="siteSelector == 'url'">
|
||||
<ion-item>
|
||||
<ion-label stacked><h2>{{ 'core.login.siteaddress' | translate }}</h2></ion-label>
|
||||
<ion-input name="url" type="url" placeholder="https://campus.example.edu" formControlName="siteUrl" [core-auto-focus]="showKeyboard"></ion-input>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="siteSelector != 'url'">
|
||||
<ion-item>
|
||||
<ion-label stacked><h2>{{ 'core.login.siteaddress' | translate }}</h2></ion-label>
|
||||
<ion-input name="url" placeholder="https://campus.example.edu" formControlName="siteUrl" [core-auto-focus]="showKeyboard" (ionChange)="searchSite($event, siteForm.value.siteUrl)"></ion-input>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ion-list *ngIf="!fixedSites" [class.hidden]="!hasSites && !enteredSiteUrl" class="core-login-site-list">
|
||||
<ion-list [class.hidden]="!hasSites && !enteredSiteUrl" class="core-login-site-list">
|
||||
<ion-item no-lines class="core-login-site-list-title"><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item>
|
||||
<button ion-item *ngIf="enteredSiteUrl" (click)="connect($event, enteredSiteUrl.url)" [attr.aria-label]="'core.login.connect' | translate" detail-push class="core-login-entered-site">
|
||||
<ion-thumbnail item-start>
|
||||
|
@ -47,44 +53,51 @@
|
|||
</div>
|
||||
</ion-list>
|
||||
|
||||
<div *ngIf="!fixedSites && !hasSites && loadingSites" class="core-login-site-nolist-loading"><ion-spinner></ion-spinner></div>
|
||||
<div *ngIf="!hasSites && loadingSites" class="core-login-site-nolist-loading"><ion-spinner></ion-spinner></div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Pick the site from a list of fixed sites. -->
|
||||
<ion-item *ngIf="fixedSites && fixedDisplay == 'select'" margin-vertical text-wrap>
|
||||
<ion-item *ngIf="fixedSites && siteSelector == 'select'" margin-vertical text-wrap>
|
||||
<ion-label stacked for="siteSelect">{{ 'core.login.selectsite' | translate }}</ion-label>
|
||||
<ion-select formControlName="siteUrl" name="url" placeholder="{{ 'core.login.siteaddress' | translate }}" interface="action-sheet">
|
||||
<ion-option *ngFor="let site of fixedSites" [value]="site.url">{{site.name}}</ion-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="!fixedSites && showScanQR && !hasSites && !enteredSiteUrl">
|
||||
<ion-item *ngIf="(fixedSites && siteSelector == 'select') || (!fixedSites && siteSelector == 'url')" no-lines>
|
||||
<button ion-button block [disabled]="!siteForm.valid" text-wrap>{{ 'core.login.connect' | translate }}</button>
|
||||
</ion-item>
|
||||
</form>
|
||||
|
||||
<ng-container *ngIf="fixedSites">
|
||||
<!-- Pick the site from a list of fixed sites. -->
|
||||
<ion-list *ngIf="siteSelector == 'list' || siteSelector == 'listnourl'">
|
||||
<ion-item no-lines><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item>
|
||||
<ion-searchbar *ngIf="fixedSites.length > 4" [(ngModel)]="filter" (ionInput)="filterChanged($event)" (ionCancel)="filterChanged()" [placeholder]="'core.login.findyoursite' | translate"></ion-searchbar>
|
||||
<ion-item *ngFor="let site of filteredSites" (click)="connect($event, site.url)" [title]="site.name" detail-push text-wrap>
|
||||
<h2>{{site.name}}</h2>
|
||||
<p *ngIf="siteSelector == 'list'">{{site.url}}</p>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<!-- Display them using buttons. -->
|
||||
<div *ngIf="siteSelector == 'buttons'">
|
||||
<p class="padding no-padding-bottom">{{ 'core.login.selectsite' | translate }}</p>
|
||||
<a *ngFor="let site of fixedSites" ion-button block (click)="connect($event, site.url)" [title]="site.name" margin-bottom>{{site.name}}</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showScanQR && !hasSites && !enteredSiteUrl">
|
||||
<div class="core-login-site-qrcode-separator">{{ 'core.login.or' | translate }}</div>
|
||||
<ion-item class="core-login-site-qrcode">
|
||||
<a ion-button block color="light" margin-top icon-start (click)="showInstructionsAndScanQR()">
|
||||
<ion-item class="core-login-site-qrcode" no-lines>
|
||||
<a ion-button block color="light" margin-top icon-start (click)="showInstructionsAndScanQR()" text-wrap>
|
||||
<core-icon name="fa-qrcode" aria-hidden="true"></core-icon>
|
||||
{{ 'core.scanqr' | translate }}
|
||||
</a>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Pick the site from a list of fixed sites. -->
|
||||
<ion-list *ngIf="fixedSites && (fixedDisplay == 'list' || fixedDisplay == 'listnourl')">
|
||||
<ion-item no-lines><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item>
|
||||
<ion-searchbar *ngIf="fixedSites.length > 4" [(ngModel)]="filter" (ionInput)="filterChanged($event)" (ionCancel)="filterChanged()" [placeholder]="'core.login.findyoursite' | translate"></ion-searchbar>
|
||||
<ion-item *ngFor="let site of filteredSites" (click)="connect($event, site.url)" [title]="site.name" detail-push text-wrap>
|
||||
<h2>{{site.name}}</h2>
|
||||
<p *ngIf="fixedDisplay == 'list'">{{site.url}}</p>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<!-- Display them using buttons. -->
|
||||
<div *ngIf="fixedSites && fixedDisplay == 'buttons'">
|
||||
<p class="padding no-padding-bottom">{{ 'core.login.selectsite' | translate }}</p>
|
||||
<a *ngFor="let site of fixedSites" ion-button block (click)="connect($event, site.url)" [title]="site.name" margin-bottom>{{site.name}}</a>
|
||||
</div>
|
||||
|
||||
<!-- Help. -->
|
||||
<ion-list no-lines margin-top>
|
||||
<a ion-item text-center text-wrap class="core-login-need-help" (click)="showHelp()" detail-none>
|
||||
|
|
|
@ -129,10 +129,4 @@ ion-app.app-root page-core-login-site {
|
|||
.core-login-default-icon {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.core-login-site-qrcode-separator {
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ export class CoreLoginSitePage {
|
|||
siteForm: FormGroup;
|
||||
fixedSites: CoreLoginSiteInfo[];
|
||||
filteredSites: CoreLoginSiteInfo[];
|
||||
fixedDisplay = 'buttons';
|
||||
siteSelector = 'sitefinder';
|
||||
showKeyboard = false;
|
||||
filter = '';
|
||||
sites: CoreLoginSiteInfoExtended[] = [];
|
||||
|
@ -80,17 +80,16 @@ export class CoreLoginSitePage {
|
|||
protected textUtils: CoreTextUtilsProvider) {
|
||||
|
||||
this.showKeyboard = !!navParams.get('showKeyboard');
|
||||
this.showScanQR = this.utils.canScanQR();
|
||||
|
||||
let url = '';
|
||||
this.siteSelector = CoreConfigConstants.multisitesdisplay;
|
||||
|
||||
// Load fixed sites if they're set.
|
||||
if (this.loginHelper.hasSeveralFixedSites()) {
|
||||
this.fixedSites = <any[]> this.loginHelper.getFixedSites();
|
||||
this.fixedDisplay = CoreConfigConstants.multisitesdisplay;
|
||||
// Autoselect if not defined.
|
||||
if (['list', 'listnourl', 'select', 'buttons'].indexOf(this.fixedDisplay) < 0) {
|
||||
this.fixedDisplay = this.fixedSites.length > 8 ? 'list' : (this.fixedSites.length > 3 ? 'select' : 'buttons');
|
||||
if (['list', 'listnourl', 'select', 'buttons'].indexOf(this.siteSelector) < 0) {
|
||||
this.siteSelector = this.fixedSites.length > 8 ? 'list' : (this.fixedSites.length > 3 ? 'select' : 'buttons');
|
||||
}
|
||||
this.filteredSites = this.fixedSites;
|
||||
url = this.fixedSites[0].url;
|
||||
|
@ -103,6 +102,9 @@ export class CoreLoginSitePage {
|
|||
});
|
||||
}
|
||||
|
||||
this.showScanQR = this.utils.canScanQR() && (typeof CoreConfigConstants['displayqronsitescreen'] == 'undefined' ||
|
||||
!!CoreConfigConstants['displayqronsitescreen']);
|
||||
|
||||
this.siteForm = fb.group({
|
||||
siteUrl: [url, this.moodleUrlValidator()]
|
||||
});
|
||||
|
|
|
@ -67,7 +67,8 @@ export class CoreMainMenuMorePage implements OnDestroy {
|
|||
this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.loadSiteInfo.bind(this),
|
||||
sitesProvider.getCurrentSiteId());
|
||||
this.loadSiteInfo();
|
||||
this.showScanQR = this.utils.canScanQR();
|
||||
this.showScanQR = this.utils.canScanQR() &&
|
||||
!this.sitesProvider.getCurrentSite().isFeatureDisabled('CoreMainMenuDelegate_QrReader');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
"errorsomedatanotdownloaded": "If you downloaded this activity, please notice that some data isn't downloaded during the download process for performance and data usage reasons.",
|
||||
"errorsync": "An error occurred while synchronising. Please try again.",
|
||||
"errorsyncblocked": "This {{$a}} cannot be synchronised right now because of an ongoing process. Please try again later. If the problem persists, try restarting the app.",
|
||||
"errorurlschemeinvalidsite": "This site URL cannot be opened in this app.",
|
||||
"explanationdigitalminor": "This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.",
|
||||
"favourites": "Starred",
|
||||
"filename": "Filename",
|
||||
|
|
|
@ -55,7 +55,9 @@ export class CoreLangProvider {
|
|||
|
||||
translate.onLangChange.subscribe((event: any) => {
|
||||
platform.setLang(event.lang, true);
|
||||
platform.setDir(this.translate.instant('core.thisdirection'), true);
|
||||
|
||||
const dir = this.translate.instant('core.thisdirection');
|
||||
platform.setDir(dir.indexOf('rtl') != -1 ? 'rtl' : 'ltr', true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins
|
|||
import { CoreConfigConstants } from '../configconstants';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
import { makeSingleton } from '@singletons/core.singletons';
|
||||
import { CoreUrl } from '@singletons/url';
|
||||
|
||||
/**
|
||||
* All params that can be in a custom URL scheme.
|
||||
|
@ -166,6 +167,12 @@ export class CoreCustomURLSchemesProvider {
|
|||
}
|
||||
|
||||
try {
|
||||
const isValid = await this.isInFixedSiteUrls(data.siteUrl);
|
||||
|
||||
if (!isValid) {
|
||||
throw this.translate.instant('core.errorurlschemeinvalidsite');
|
||||
}
|
||||
|
||||
if (data.redirect && data.redirect.match(/^https?:\/\//) && data.redirect.indexOf(data.siteUrl) == -1) {
|
||||
// Redirect URL must belong to the same site. Reject.
|
||||
throw this.translate.instant('core.contentlinks.errorredirectothersite');
|
||||
|
@ -540,6 +547,38 @@ export class CoreCustomURLSchemesProvider {
|
|||
this.domUtils.showErrorModalDefault(error.error, this.translate.instant('core.login.invalidsite'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a site URL is one of the fixed sites for the app (in case there are fixed sites).
|
||||
*
|
||||
* @param siteUrl Site URL to check.
|
||||
* @return Promise resolved with boolean: whether is one of the fixed sites.
|
||||
*/
|
||||
protected async isInFixedSiteUrls(siteUrl: string): Promise<boolean> {
|
||||
if (this.loginHelper.isFixedUrlSet()) {
|
||||
|
||||
return CoreUrl.sameDomainAndPath(siteUrl, <string> this.loginHelper.getFixedSites());
|
||||
} else if (this.loginHelper.hasSeveralFixedSites()) {
|
||||
const sites = <any[]> this.loginHelper.getFixedSites();
|
||||
|
||||
const site = sites.find((site) => {
|
||||
return CoreUrl.sameDomainAndPath(siteUrl, site.url);
|
||||
});
|
||||
|
||||
return !!site;
|
||||
} else if (CoreConfigConstants.multisitesdisplay == 'sitefinder' && CoreConfigConstants.onlyallowlistedsites) {
|
||||
// Call the sites finder to validate the site.
|
||||
const result = await this.sitesProvider.findSites(siteUrl.replace(/^https?\:\/\/|\.\w{2,3}\/?$/g, ''));
|
||||
|
||||
const site = result && result.find((site) => {
|
||||
return CoreUrl.sameDomainAndPath(siteUrl, site.url);
|
||||
});
|
||||
|
||||
return !!site;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -162,7 +162,7 @@ export class CoreWSProvider {
|
|||
this.logger = logger.getInstance('CoreWSProvider');
|
||||
|
||||
platform.ready().then(() => {
|
||||
if (this.appProvider.isMobile()) {
|
||||
if (this.appProvider.isIOS()) {
|
||||
(<any> cordova).plugin.http.setHeader('User-Agent', navigator.userAgent);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CoreTextUtils } from '@providers/utils/text';
|
||||
|
||||
/**
|
||||
* Parts contained within a url.
|
||||
*/
|
||||
|
@ -172,4 +174,27 @@ export class CoreUrl {
|
|||
static removeProtocol(url: string): string {
|
||||
return url.replace(/^[a-zA-Z]+:\/\//i, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two URLs have the same domain and path.
|
||||
*
|
||||
* @param urlA First URL.
|
||||
* @param urlB Second URL.
|
||||
* @return Whether they have same domain and path.
|
||||
*/
|
||||
static sameDomainAndPath(urlA: string, urlB: string): boolean {
|
||||
// Add protocol if missing, the parse function requires it.
|
||||
if (!urlA.match(/^[^\/:\.\?]*:\/\//)) {
|
||||
urlA = `https://${urlA}`;
|
||||
}
|
||||
if (!urlB.match(/^[^\/:\.\?]*:\/\//)) {
|
||||
urlB = `https://${urlB}`;
|
||||
}
|
||||
|
||||
const partsA = CoreUrl.parse(urlA);
|
||||
const partsB = CoreUrl.parse(urlB);
|
||||
|
||||
return partsA.domain == partsB.domain &&
|
||||
CoreTextUtils.instance.removeEndingSlash(partsA.path) == CoreTextUtils.instance.removeEndingSlash(partsB.path);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue