commit
8c1fc373c9
|
@ -84,9 +84,9 @@
|
|||
}
|
||||
},
|
||||
"font_sizes": [
|
||||
62.5,
|
||||
75.89,
|
||||
93.75
|
||||
100,
|
||||
110,
|
||||
120
|
||||
],
|
||||
"customurlscheme": "moodlemobile",
|
||||
"siteurl": "",
|
||||
|
|
|
@ -0,0 +1,327 @@
|
|||
#!/bin/bash
|
||||
source "functions.sh"
|
||||
|
||||
#Saves or updates a key on langindex_old.json
|
||||
function save_key {
|
||||
local key=$1
|
||||
local found=$2
|
||||
|
||||
print_ok "$key=$found"
|
||||
echo "{\"$key\": \"$found\"}" > langindex_old.json
|
||||
jq -s '.[0] + .[1]' langindex.json langindex_old.json > langindex_new.json
|
||||
mv langindex_new.json langindex.json
|
||||
}
|
||||
|
||||
#Removes a key on langindex_old.json
|
||||
function remove_key {
|
||||
local key=$1
|
||||
|
||||
cat langindex.json | jq 'del(."'$key'")' > langindex_new.json
|
||||
mv langindex_new.json langindex.json
|
||||
print_ok "Deleted unused key $key"
|
||||
}
|
||||
|
||||
#Check if and i exists in php file
|
||||
function exists_in_file {
|
||||
local file=$1
|
||||
local id=$2
|
||||
|
||||
file=`echo $file | sed s/^mod_workshop_assessment/workshopform/1`
|
||||
file=`echo $file | sed s/^mod_assign_/assign/1`
|
||||
file=`echo $file | sed s/^mod_//1`
|
||||
|
||||
completeFile="$LANGPACKSFOLDER/en/$file.php"
|
||||
if [ -f "$completeFile" ]; then
|
||||
coincidence=`grep "string\[\'$id\'\]" $completeFile`
|
||||
if [ ! -z "$coincidence" ]; then
|
||||
found=$file
|
||||
return
|
||||
fi
|
||||
fi
|
||||
found=0
|
||||
}
|
||||
|
||||
#Checks if a key exists on the original local_moodlemobileapp.php
|
||||
function exists_in_mobile {
|
||||
local file='local_moodlemobileapp'
|
||||
exists_in_file $file $key
|
||||
}
|
||||
|
||||
function do_match {
|
||||
match=$1
|
||||
filematch=""
|
||||
|
||||
coincidence=`grep "$match" $LANGPACKSFOLDER/en/*.php | wc -l`
|
||||
if [ $coincidence -eq 1 ]; then
|
||||
filematch=`grep "$match" $LANGPACKSFOLDER/en/*.php | cut -d'/' -f5 | cut -d'.' -f1`
|
||||
exists_in_file $filematch $plainid
|
||||
elif [ $coincidence -gt 0 ] && [ "$#" -gt 1 ]; then
|
||||
print_message $2
|
||||
tput setaf 6
|
||||
grep "$match" $LANGPACKSFOLDER/en/*.php
|
||||
fi
|
||||
}
|
||||
|
||||
#Find if the id or the value can be found on files to help providing a solution.
|
||||
function find_matches {
|
||||
do_match "string\[\'$plainid\'\] = \'$value\'" "Found EXACT match for $key in the following paths"
|
||||
if [ $coincidence -gt 0 ]; then
|
||||
case=1
|
||||
return
|
||||
fi
|
||||
|
||||
do_match " = \'$value\'" "Found some string VALUES for $key in the following paths"
|
||||
if [ $coincidence -gt 0 ]; then
|
||||
case=2
|
||||
return
|
||||
fi
|
||||
|
||||
do_match "string\[\'$plainid\'\]" "Found some string KEYS for $key in the following paths, value $value"
|
||||
if [ $coincidence -gt 0 ]; then
|
||||
case=3
|
||||
return
|
||||
fi
|
||||
|
||||
print_message "No match found for $key add it to local_moodlemobileapp"
|
||||
save_key $key "local_moodlemobileapp"
|
||||
}
|
||||
|
||||
function find_single_matches {
|
||||
do_match "string\[\'$plainid\'\] = \'$value\'"
|
||||
if [ ! -z $filematch ] && [ $found != 0 ]; then
|
||||
case=1
|
||||
return
|
||||
fi
|
||||
|
||||
do_match " = \'$value\'"
|
||||
if [ ! -z $filematch ] && [ $filematch != 'local_moodlemobileapp' ]; then
|
||||
case=2
|
||||
print_message "Found some string VALUES for $key in the following paths $filematch"
|
||||
tput setaf 6
|
||||
grep "$match" $LANGPACKSFOLDER/en/*.php
|
||||
return
|
||||
fi
|
||||
|
||||
do_match "string\[\'$plainid\'\]"
|
||||
if [ ! -z $filematch ] && [ $found != 0 ]; then
|
||||
case=3
|
||||
return
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
#Tries to gues the file where the id will be found.
|
||||
function guess_file {
|
||||
local key=$1
|
||||
local value=$2
|
||||
|
||||
local type=`echo $key | cut -d'.' -f1`
|
||||
local component=`echo $key | cut -d'.' -f2`
|
||||
local plainid=`echo $key | cut -d'.' -f3-`
|
||||
|
||||
if [ -z "$plainid" ]; then
|
||||
plainid=$component
|
||||
component='moodle'
|
||||
fi
|
||||
|
||||
exists_in_file $component $plainid
|
||||
|
||||
if [ $found == 0 ]; then
|
||||
tempid=`echo $plainid | sed s/^mod_//1`
|
||||
if [ $component == 'moodle' ] && [ "$tempid" != "$plainid" ]; then
|
||||
exists_in_file $plainid pluginname
|
||||
|
||||
if [ $found != 0 ]; then
|
||||
found=$found/pluginname
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Not found in file, try in local_moodlemobileapp
|
||||
if [ $found == 0 ]; then
|
||||
exists_in_mobile
|
||||
fi
|
||||
|
||||
# Still not found, if only found in one file, use it.
|
||||
if [ $found == 0 ]; then
|
||||
find_single_matches
|
||||
fi
|
||||
|
||||
# Last fallback.
|
||||
if [ $found == 0 ]; then
|
||||
exists_in_file 'moodle' $plainid
|
||||
fi
|
||||
|
||||
if [ $found == 0 ]; then
|
||||
find_matches
|
||||
else
|
||||
save_key $key $found
|
||||
fi
|
||||
}
|
||||
|
||||
function current_translation_exists {
|
||||
local key=$1
|
||||
local current=$2
|
||||
local file=$3
|
||||
|
||||
plainid=`echo $key | cut -d'.' -f3-`
|
||||
|
||||
if [ -z "$plainid" ]; then
|
||||
plainid=`echo $key | cut -d'.' -f2`
|
||||
fi
|
||||
|
||||
local currentFile=`echo $current | cut -d'/' -f1`
|
||||
local currentStr=`echo $current | cut -d'/' -f2-`
|
||||
if [ $currentFile == $current ]; then
|
||||
currentStr=$plainid
|
||||
fi
|
||||
|
||||
exists_in_file $currentFile $currentStr
|
||||
if [ $found == 0 ]; then
|
||||
# Translation not found.
|
||||
exec="jq -r .\"$key\" $file"
|
||||
value=`$exec`
|
||||
|
||||
print_error "Translation of '$currentStr' not found in '$currentFile'"
|
||||
|
||||
guess_file $key "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
#Finds if there's a better file where to get the id from.
|
||||
function find_better_file {
|
||||
local key=$1
|
||||
local value=$2
|
||||
local current=$3
|
||||
|
||||
local type=`echo $key | cut -d'.' -f1`
|
||||
local component=`echo $key | cut -d'.' -f2`
|
||||
local plainid=`echo $key | cut -d'.' -f3-`
|
||||
|
||||
if [ -z "$plainid" ]; then
|
||||
plainid=$component
|
||||
component='moodle'
|
||||
fi
|
||||
|
||||
local currentFile=`echo $current | cut -d'/' -f1`
|
||||
local currentStr=`echo $current | cut -d'/' -f2-`
|
||||
if [ $currentFile == $current ]; then
|
||||
currentStr=$plainid
|
||||
fi
|
||||
|
||||
exists_in_file $component $plainid
|
||||
if [ $found != 0 ] && [ $currentStr == $plainid ]; then
|
||||
if [ $found != $currentFile ]; then
|
||||
print_ok "Key '$key' found in component, no need to replace old '$current'"
|
||||
fi
|
||||
|
||||
return
|
||||
fi
|
||||
|
||||
# Still not found, if only found in one file, use it.
|
||||
if [ $found == 0 ]; then
|
||||
find_single_matches
|
||||
fi
|
||||
|
||||
if [ $found != 0 ] && [ $found != $currentFile ] && [ $case -lt 3 ]; then
|
||||
print_message "Indexed string '$key' found in '$found' better than '$current'"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ $currentFile == 'local_moodlemobileapp' ]; then
|
||||
exists_in_mobile
|
||||
else
|
||||
exists_in_file $currentFile $currentStr
|
||||
fi
|
||||
|
||||
if [ $found == 0 ]; then
|
||||
print_error "Indexed string '$key' not found on current place '$current'"
|
||||
if [ $currentFile != 'local_moodlemobileapp' ]; then
|
||||
print_error "Execute this on AMOS
|
||||
CPY [$currentStr,$currentFile],[$key,local_moodlemobileapp]"
|
||||
save_key $key "local_moodlemobileapp"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Parses the file.
|
||||
function parse_file {
|
||||
findbetter=$2
|
||||
keys=`jq -r 'keys[]' $1`
|
||||
for key in $keys; do
|
||||
# Check if already parsed.
|
||||
exec="jq -r .\"$key\" langindex.json"
|
||||
found=`$exec`
|
||||
|
||||
if [ -z "$found" ] || [ "$found" == 'null' ]; then
|
||||
exec="jq -r .\"$key\" $1"
|
||||
value=`$exec`
|
||||
guess_file $key "$value"
|
||||
else
|
||||
if [ "$found" == 'donottranslate' ]; then
|
||||
# Do nothing since is not translatable.
|
||||
continue
|
||||
elif [ ! -z "$findbetter" ]; then
|
||||
exec="jq -r .\"$key\" $1"
|
||||
value=`$exec`
|
||||
find_better_file "$key" "$value" "$found"
|
||||
elif [ "$found" != 'local_moodlemobileapp' ]; then
|
||||
current_translation_exists "$key" "$found" "$1"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Do some cleanup
|
||||
langkeys=`jq -r 'keys[]' langindex.json`
|
||||
findkeys="${keys[@]}"
|
||||
for key in $langkeys; do
|
||||
# Check if already used.
|
||||
array_contains "$key" "$findkeys"
|
||||
|
||||
if [ -z "$found" ] || [ "$found" == 'null' ]; then
|
||||
remove_key $key
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Checks if an array contains an string.
|
||||
function array_contains {
|
||||
local hayjack=$2
|
||||
local needle=$1
|
||||
found=''
|
||||
for i in $hayjack; do
|
||||
if [ "$i" == "$needle" ] ; then
|
||||
found=$i
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
print_title 'Generating language from code...'
|
||||
gulp lang
|
||||
|
||||
print_title 'Getting languages'
|
||||
git clone https://git.in.moodle.com/moodle/moodle-langpacks.git $LANGPACKSFOLDER
|
||||
pushd $LANGPACKSFOLDER
|
||||
BRANCHES=($(git branch -r --format="%(refname:lstrip=3)" --sort="refname" | grep MOODLE_))
|
||||
BRANCH=${BRANCHES[${#BRANCHES[@]}-1]}
|
||||
git checkout $BRANCH
|
||||
git pull
|
||||
popd
|
||||
|
||||
print_title 'Processing file'
|
||||
#Create langindex.json if not exists.
|
||||
if [ ! -f 'langindex.json' ]; then
|
||||
echo "{}" > langindex.json
|
||||
fi
|
||||
|
||||
findbetter=$1
|
||||
parse_file '../src/assets/lang/en.json' $findbetter
|
||||
|
||||
echo
|
||||
|
||||
jq -S --indent 2 -s '.[0]' langindex.json > langindex_new.json
|
||||
mv langindex_new.json langindex.json
|
||||
rm langindex_old.json
|
||||
|
||||
print_ok 'All done!'
|
|
@ -0,0 +1,67 @@
|
|||
#!/bin/bash
|
||||
|
||||
LANGPACKSFOLDER='../../moodle-langpacks'
|
||||
|
||||
function check_success_exit {
|
||||
if [ $? -ne 0 ]; then
|
||||
print_error "$1"
|
||||
exit 1
|
||||
elif [ "$#" -gt 1 ]; then
|
||||
print_ok "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
function check_success {
|
||||
if [ $? -ne 0 ]; then
|
||||
print_error "$1"
|
||||
elif [ "$#" -gt 1 ]; then
|
||||
print_ok "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
function print_success {
|
||||
if [ $? -ne 0 ]; then
|
||||
print_message "$1"
|
||||
$3=0
|
||||
else
|
||||
print_ok "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
function print_error {
|
||||
tput setaf 1; echo " ERROR: $1"; tput sgr0
|
||||
}
|
||||
|
||||
function print_ok {
|
||||
tput setaf 2; echo " OK: $1"; tput sgr0
|
||||
echo
|
||||
}
|
||||
|
||||
function print_message {
|
||||
tput setaf 3; echo "-------- $1"; tput sgr0
|
||||
echo
|
||||
}
|
||||
|
||||
function print_title {
|
||||
stepnumber=$(($stepnumber + 1))
|
||||
echo
|
||||
tput setaf 5; echo "$stepnumber $1"; tput sgr0
|
||||
tput setaf 5; echo '=================='; tput sgr0
|
||||
}
|
||||
|
||||
function telegram_notify {
|
||||
if [ ! -z $TELEGRAM_APIKEY ] && [ ! -z $TELEGRAM_CHATID ] ; then
|
||||
MESSAGE="Travis error: $1%0ABranch: $TRAVIS_BRANCH%0ARepo: $TRAVIS_REPO_SLUG"
|
||||
URL="https://api.telegram.org/bot$TELEGRAM_APIKEY/sendMessage"
|
||||
|
||||
curl -s -X POST $URL -d chat_id=$TELEGRAM_CHATID -d text="$MESSAGE"
|
||||
fi
|
||||
}
|
||||
|
||||
function notify_on_error_exit {
|
||||
if [ $? -ne 0 ]; then
|
||||
print_error "$1"
|
||||
telegram_notify "$1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
|
@ -0,0 +1,474 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Helper functions converting moodle strings to json.
|
||||
*/
|
||||
|
||||
function detect_languages($languages, $keys) {
|
||||
echo "\n\n\n";
|
||||
|
||||
$all_languages = glob(LANGPACKSFOLDER.'/*' , GLOB_ONLYDIR);
|
||||
function get_lang_from_dir($dir) {
|
||||
return str_replace('_', '-', explode('/', $dir)[3]);
|
||||
}
|
||||
function get_lang_not_wp($langname) {
|
||||
return (substr($langname, -3) !== '-wp');
|
||||
}
|
||||
$all_languages = array_map('get_lang_from_dir', $all_languages);
|
||||
$all_languages = array_filter($all_languages, 'get_lang_not_wp');
|
||||
|
||||
$detect_lang = array_diff($all_languages, $languages);
|
||||
$new_langs = array();
|
||||
foreach ($detect_lang as $lang) {
|
||||
reset_translations_strings();
|
||||
$new = detect_lang($lang, $keys);
|
||||
if ($new) {
|
||||
$new_langs[$lang] = $lang;
|
||||
}
|
||||
}
|
||||
|
||||
return $new_langs;
|
||||
}
|
||||
|
||||
function build_languages($languages, $keys, $added_langs = []) {
|
||||
// Process the languages.
|
||||
foreach ($languages as $lang) {
|
||||
reset_translations_strings();
|
||||
$ok = build_lang($lang, $keys);
|
||||
if ($ok) {
|
||||
$added_langs[$lang] = $lang;
|
||||
}
|
||||
}
|
||||
|
||||
return $added_langs;
|
||||
}
|
||||
|
||||
function get_langindex_keys() {
|
||||
$local = 0;
|
||||
// Process the index file, just once.
|
||||
$keys = file_get_contents('langindex.json');
|
||||
$keys = (array) json_decode($keys);
|
||||
|
||||
foreach ($keys as $key => $value) {
|
||||
$map = new StdClass();
|
||||
if ($value == 'local_moodlemobileapp') {
|
||||
$map->file = $value;
|
||||
$map->string = $key;
|
||||
$local++;
|
||||
} else {
|
||||
$exp = explode('/', $value, 2);
|
||||
$map->file = $exp[0];
|
||||
if (count($exp) == 2) {
|
||||
$map->string = $exp[1];
|
||||
} else {
|
||||
$exp = explode('.', $key, 3);
|
||||
|
||||
if (count($exp) == 3) {
|
||||
$map->string = $exp[2];
|
||||
} else {
|
||||
$map->string = $exp[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$keys[$key] = $map;
|
||||
}
|
||||
|
||||
$total = count($keys);
|
||||
echo "Total strings to translate $total ($local local)\n";
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
function add_langs_to_config($langs, $config) {
|
||||
$changed = false;
|
||||
$config_langs = get_object_vars($config['languages']);
|
||||
foreach ($langs as $lang) {
|
||||
if (!isset($config_langs[$lang])) {
|
||||
$langfoldername = str_replace('-', '_', $lang);
|
||||
|
||||
$string = [];
|
||||
include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php');
|
||||
$config['languages']->$lang = $string['thislanguage'];
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
// Sort languages by key.
|
||||
$config['languages'] = json_decode( json_encode( $config['languages'] ), true );
|
||||
ksort($config['languages']);
|
||||
$config['languages'] = json_decode( json_encode( $config['languages'] ), false );
|
||||
file_put_contents(CONFIG, json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
|
||||
function get_langfolder($lang) {
|
||||
$folder = LANGPACKSFOLDER.'/'.str_replace('-', '_', $lang);
|
||||
if (!is_dir($folder) || !is_file($folder.'/langconfig.php')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $folder;
|
||||
}
|
||||
|
||||
function get_translation_strings($langfoldername, $file, $override_folder = false) {
|
||||
global $strings;
|
||||
|
||||
if (isset($strings[$file])) {
|
||||
return $strings[$file];
|
||||
}
|
||||
|
||||
$string = import_translation_strings($langfoldername, $file);
|
||||
$string_override = $override_folder ? import_translation_strings($override_folder, $file) : false;
|
||||
|
||||
if ($string) {
|
||||
$strings[$file] = $string;
|
||||
if ($string_override) {
|
||||
$strings[$file] = array_merge($strings[$file], $string_override);
|
||||
}
|
||||
} else if ($string_override) {
|
||||
$strings[$file] = $string_override;
|
||||
} else {
|
||||
$strings[$file] = false;
|
||||
}
|
||||
|
||||
return $strings[$file];
|
||||
}
|
||||
|
||||
function import_translation_strings($langfoldername, $file) {
|
||||
$path = $langfoldername.'/'.$file.'.php';
|
||||
// Apply translations.
|
||||
if (!file_exists($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$string = [];
|
||||
include($path);
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
function reset_translations_strings() {
|
||||
global $strings;
|
||||
$strings = [];
|
||||
}
|
||||
|
||||
function build_lang($lang, $keys) {
|
||||
$langfoldername = get_langfolder($lang);
|
||||
if (!$langfoldername) {
|
||||
echo "Cannot translate $lang, folder not found";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (OVERRIDE_LANG_SUFIX) {
|
||||
$override_langfolder = get_langfolder($lang.OVERRIDE_LANG_SUFIX);
|
||||
} else {
|
||||
$override_langfolder = false;
|
||||
}
|
||||
|
||||
$total = count($keys);
|
||||
$local = 0;
|
||||
|
||||
$langparts = explode('-', $lang, 2);
|
||||
$parentname = $langparts[0] ? $langparts[0] : "";
|
||||
$parent = "";
|
||||
|
||||
echo "Processing $lang";
|
||||
// Check parent language exists.
|
||||
if ($parentname != $lang && get_langfolder($parentname)) {
|
||||
echo " ($parentname)";
|
||||
$parent = $parentname;
|
||||
}
|
||||
|
||||
$langFile = false;
|
||||
// Not yet translated. Do not override.
|
||||
if (file_exists(ASSETSPATH.$lang.'.json')) {
|
||||
// Load lang files just once.
|
||||
$langFile = file_get_contents(ASSETSPATH.$lang.'.json');
|
||||
$langFile = (array) json_decode($langFile);
|
||||
}
|
||||
|
||||
$translations = [];
|
||||
// Add the translation to the array.
|
||||
foreach ($keys as $key => $value) {
|
||||
$string = get_translation_strings($langfoldername, $value->file, $override_langfolder);
|
||||
// Apply translations.
|
||||
if (!$string) {
|
||||
if ($value->file == 'donottranslate') {
|
||||
// Restore it form the json.
|
||||
if ($langFile && is_array($langFile) && isset($langFile[$key])) {
|
||||
$translations[$key] = $langFile[$key];
|
||||
} else {
|
||||
// If not present, do not count it in the total.
|
||||
$total--;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TOTRANSLATE) {
|
||||
echo "\n\t\tTo translate $value->string on $value->file";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($string[$value->string]) || ($lang == 'en' && $value->file == 'local_moodlemobileapp')) {
|
||||
// Not yet translated. Do not override.
|
||||
if ($langFile && is_array($langFile) && isset($langFile[$key])) {
|
||||
$translations[$key] = $langFile[$key];
|
||||
|
||||
if ($value->file == 'local_moodlemobileapp') {
|
||||
$local++;
|
||||
}
|
||||
}
|
||||
if (TOTRANSLATE && !isset($string[$value->string])) {
|
||||
echo "\n\t\tTo translate $value->string on $value->file";
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
$text = $string[$value->string];
|
||||
}
|
||||
|
||||
if ($value->file != 'local_moodlemobileapp') {
|
||||
$text = str_replace('$a->', '$a.', $text);
|
||||
$text = str_replace('{$a', '{{$a', $text);
|
||||
$text = str_replace('}', '}}', $text);
|
||||
// Prevent double.
|
||||
$text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text);
|
||||
} else {
|
||||
// @TODO: Remove that line when core.cannotconnect and core.login.invalidmoodleversion are completelly changed to use $a
|
||||
if (($key == 'core.cannotconnect' || $key == 'core.login.invalidmoodleversion') && strpos($text, '2.4') != false) {
|
||||
$text = str_replace('2.4', '{{$a}}', $text);
|
||||
}
|
||||
$local++;
|
||||
}
|
||||
|
||||
$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)));
|
||||
|
||||
$success = count($translations);
|
||||
$percentage = floor($success/$total * 100);
|
||||
$bar = progressbar($percentage);
|
||||
if (strlen($lang) <= 2 && !$parent) {
|
||||
echo "\t";
|
||||
}
|
||||
echo "\t\t$success of $total -> $percentage% $bar ($local local)\n";
|
||||
|
||||
if ($lang == 'en') {
|
||||
generate_local_moodlemobileapp($keys, $translations);
|
||||
override_component_lang_files($keys, $translations);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function progressbar($percentage) {
|
||||
$done = floor($percentage/10);
|
||||
return "\t".str_repeat('=', $done) . str_repeat('-', 10-$done);
|
||||
}
|
||||
|
||||
function detect_lang($lang, $keys) {
|
||||
$langfoldername = get_langfolder($lang);
|
||||
if (!$langfoldername) {
|
||||
echo "Cannot translate $lang, folder not found";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$total = count ($keys);
|
||||
$success = 0;
|
||||
$local = 0;
|
||||
|
||||
$string = get_translation_strings($langfoldername, 'langconfig');
|
||||
$parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : "";
|
||||
if (!isset($string['thislanguage'])) {
|
||||
echo "Cannot translate $lang, translated name not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
$title = $lang;
|
||||
if ($parent != "" && $parent != $lang) {
|
||||
$title .= " ($parent)";
|
||||
}
|
||||
$langname = $string['thislanguage'];
|
||||
$title .= " ".$langname." -D";
|
||||
|
||||
// Add the translation to the array.
|
||||
foreach ($keys as $key => $value) {
|
||||
$string = get_translation_strings($langfoldername, $value->file);
|
||||
// Apply translations.
|
||||
if (!$string) {
|
||||
// Do not count non translatable in the totals.
|
||||
if ($value->file == 'donottranslate') {
|
||||
$total--;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($string[$value->string])) {
|
||||
continue;
|
||||
} else {
|
||||
$text = $string[$value->string];
|
||||
}
|
||||
|
||||
if ($value->file == 'local_moodlemobileapp') {
|
||||
$local++;
|
||||
}
|
||||
|
||||
$success++;
|
||||
}
|
||||
|
||||
$percentage = floor($success/$total * 100);
|
||||
$bar = progressbar($percentage);
|
||||
|
||||
echo "Checking ".$title.str_repeat("\t", 7 - floor(mb_strlen($title, 'UTF-8')/8));
|
||||
echo "\t$success of $total -> $percentage% $bar ($local local)";
|
||||
if (($percentage > 75 && $local > 50) || ($percentage > 50 && $local > 75)) {
|
||||
echo " \t DETECTED\n";
|
||||
return true;
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function save_key($key, $value, $path) {
|
||||
$filePath = $path . '/en.json';
|
||||
|
||||
$file = file_get_contents($filePath);
|
||||
$file = (array) json_decode($file);
|
||||
$value = html_entity_decode($value);
|
||||
if (!isset($file[$key]) || $file[$key] != $value) {
|
||||
$file[$key] = $value;
|
||||
ksort($file);
|
||||
file_put_contents($filePath, str_replace('\/', '/', json_encode($file, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)));
|
||||
}
|
||||
}
|
||||
|
||||
function override_component_lang_files($keys, $translations) {
|
||||
echo "Override component lang files.\n";
|
||||
foreach ($translations as $key => $value) {
|
||||
$path = '../src/';
|
||||
$exp = explode('.', $key, 3);
|
||||
|
||||
$type = $exp[0];
|
||||
if (count($exp) == 3) {
|
||||
$component = $exp[1];
|
||||
$plainid = $exp[2];
|
||||
} else {
|
||||
$component = 'moodle';
|
||||
$plainid = $exp[1];
|
||||
}
|
||||
switch($type) {
|
||||
case 'core':
|
||||
case 'addon':
|
||||
switch($component) {
|
||||
case 'moodle':
|
||||
$path .= 'lang';
|
||||
break;
|
||||
default:
|
||||
$path .= $type.'/'.str_replace('_', '/', $component).'/lang';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'assets':
|
||||
$path .= $type.'/'.$component;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
if (is_file($path.'/en.json')) {
|
||||
save_key($plainid, $value, $path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates local moodle mobile app file to update languages in AMOS.
|
||||
*
|
||||
* @param [array] $keys Translation keys.
|
||||
* @param [array] $translations English translations.
|
||||
*/
|
||||
function generate_local_moodlemobileapp($keys, $translations) {
|
||||
echo "Generate local_moodlemobileapp.\n";
|
||||
$string = '<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Version details.
|
||||
*
|
||||
* @package local
|
||||
* @subpackage moodlemobileapp
|
||||
* @copyright 2014 Juan Leyva <juanleyvadelgado@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string[\'appstoredescription\'] = \'NOTE: This official Moodle Mobile app will ONLY work with Moodle sites that have been set up to allow it. Please talk to your Moodle administrator if you have any problems connecting.
|
||||
|
||||
If your Moodle site has been configured correctly, you can use this app to:
|
||||
|
||||
- browse the content of your courses, even when offline
|
||||
- receive instant notifications of messages and other events
|
||||
- quickly find and contact other people in your courses
|
||||
- upload images, audio, videos and other files from your mobile device
|
||||
- view your course grades
|
||||
- and more!
|
||||
|
||||
Please see http://docs.moodle.org/en/Mobile_app for all the latest information.
|
||||
|
||||
We’d really appreciate any good reviews about the functionality so far, and your suggestions on what else you want this app to do!
|
||||
|
||||
The app requires the following permissions:
|
||||
Record audio - For recording audio to upload to Moodle
|
||||
Read and modify the contents of your SD card - Contents are downloaded to the SD Card so you can see them offline
|
||||
Network access - To be able to connect with your Moodle site and check if you are connected or not to switch to offline mode
|
||||
Run at startup - So you receive local notifications even when the app is running in the background
|
||||
Prevent phone from sleeping - So you can receive push notifications anytime\';'."\n";
|
||||
foreach ($keys as $key => $value) {
|
||||
if (isset($translations[$key]) && $value->file == 'local_moodlemobileapp') {
|
||||
$string .= '$string[\''.$key.'\'] = \''.str_replace("'", "\'", $translations[$key]).'\';'."\n";
|
||||
}
|
||||
}
|
||||
$string .= '$string[\'pluginname\'] = \'Moodle Mobile language strings\';'."\n";
|
||||
|
||||
file_put_contents('../../moodle-local_moodlemobileapp/lang/en/local_moodlemobileapp.php', $string."\n");
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Script for converting moodle strings to json.
|
||||
*/
|
||||
|
||||
// Check we are in CLI.
|
||||
if (isset($_SERVER['REMOTE_ADDR'])) {
|
||||
exit(1);
|
||||
}
|
||||
define('MOODLE_INTERNAL', 1);
|
||||
define('LANGPACKSFOLDER', '../../moodle-langpacks');
|
||||
define('ASSETSPATH', '../src/assets/lang/');
|
||||
define('CONFIG', '../config/config.json');
|
||||
define('OVERRIDE_LANG_SUFIX', false);
|
||||
|
||||
global $strings;
|
||||
require_once('lang_functions.php');
|
||||
|
||||
$config = file_get_contents(CONFIG);
|
||||
$config = (array) json_decode($config);
|
||||
$config_langs = array_keys(get_object_vars($config['languages']));
|
||||
|
||||
// Set languages to do. If script is called using a language it will be used as unique.
|
||||
if (isset($argv[1]) && !empty($argv[1])) {
|
||||
$forcedetect = false;
|
||||
define('TOTRANSLATE', true);
|
||||
$languages = explode(',', $argv[1]);
|
||||
} else {
|
||||
$forcedetect = true;
|
||||
define('TOTRANSLATE', false);
|
||||
$languages = $config_langs;
|
||||
}
|
||||
|
||||
$keys = get_langindex_keys();
|
||||
|
||||
$added_langs = build_languages($languages, $keys);
|
||||
|
||||
if ($forcedetect) {
|
||||
$new_langs = detect_languages($languages, $keys);
|
||||
|
||||
if (!empty($new_langs)) {
|
||||
echo "\n\n\nThe following languages are going to be added\n\n\n";
|
||||
$added_langs = build_languages($new_langs, $keys, $added_langs);
|
||||
}
|
||||
}
|
||||
|
||||
add_langs_to_config($added_langs, $config);
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/bash
|
||||
source "functions.sh"
|
||||
forceLang=$1
|
||||
|
||||
print_title 'Getting languages'
|
||||
git clone --depth 1 --no-single-branch https://git.in.moodle.com/moodle/moodle-langpacks.git $LANGPACKSFOLDER
|
||||
pushd $LANGPACKSFOLDER
|
||||
BRANCHES=($(git branch -r --format="%(refname:lstrip=3)" --sort="refname" | grep MOODLE_))
|
||||
BRANCH=${BRANCHES[${#BRANCHES[@]}-1]}
|
||||
git checkout $BRANCH
|
||||
git pull
|
||||
popd
|
||||
|
||||
print_title 'Getting local mobile langs'
|
||||
git clone --depth 1 https://github.com/moodlehq/moodle-local_moodlemobileapp.git ../../moodle-local_moodlemobileapp
|
||||
|
||||
if [ -z $forceLang ]; then
|
||||
php -f moodle_to_json.php
|
||||
else
|
||||
php -f moodle_to_json.php "$forceLang"
|
||||
fi
|
||||
|
||||
print_ok 'All done!'
|
|
@ -18,6 +18,8 @@ import { NavController } from '@ionic/angular';
|
|||
import { CoreLangProvider } from '@services/lang';
|
||||
import { CoreLoginHelperProvider } from '@core/login/services/helper';
|
||||
import { CoreEvents, CoreEventSessionExpiredData } from '@singletons/events';
|
||||
import { Network, NgZone, Platform } from '@singletons/core.singletons';
|
||||
import { CoreApp } from '@services/app';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -45,14 +47,73 @@ export class AppComponent implements OnInit {
|
|||
this.langProvider.clearCustomStrings();
|
||||
|
||||
// Remove version classes from body.
|
||||
// @todo
|
||||
// this.removeVersionClass();
|
||||
this.removeVersionClass();
|
||||
});
|
||||
|
||||
// Listen for session expired events.
|
||||
CoreEvents.on(CoreEvents.SESSION_EXPIRED, (data: CoreEventSessionExpiredData) => {
|
||||
this.loginHelper.sessionExpired(data);
|
||||
});
|
||||
|
||||
this.onPlatformReady();
|
||||
}
|
||||
|
||||
protected async onPlatformReady(): Promise<void> {
|
||||
await Platform.instance.ready();
|
||||
|
||||
// Refresh online status when changes.
|
||||
Network.instance.onChange().subscribe(() => {
|
||||
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||
NgZone.instance.run(() => {
|
||||
const isOnline = CoreApp.instance.isOnline();
|
||||
const hadOfflineMessage = document.body.classList.contains('core-offline');
|
||||
|
||||
document.body.classList.toggle('core-offline', !isOnline);
|
||||
|
||||
if (isOnline && hadOfflineMessage) {
|
||||
document.body.classList.add('core-online');
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove('core-online');
|
||||
}, 3000);
|
||||
} else if (!isOnline) {
|
||||
document.body.classList.remove('core-online');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to add version to body classes.
|
||||
*
|
||||
* @param release Current release number of the site.
|
||||
*/
|
||||
protected addVersionClass(release: string): void {
|
||||
const parts = release.split('.', 3);
|
||||
|
||||
parts[1] = parts[1] || '0';
|
||||
parts[2] = parts[2] || '0';
|
||||
|
||||
document.body.classList.add('version-' + parts[0],
|
||||
'version-' + parts[0] + '-' + parts[1],
|
||||
'version-' + parts[0] + '-' + parts[1] + '-' + parts[2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to remove all version classes form body.
|
||||
*/
|
||||
protected removeVersionClass(): void {
|
||||
const remove: string[] = [];
|
||||
|
||||
Array.from(document.body.classList).forEach((tempClass) => {
|
||||
if (tempClass.substring(0, 8) == 'version-') {
|
||||
remove.push(tempClass);
|
||||
}
|
||||
});
|
||||
|
||||
remove.forEach((tempClass) => {
|
||||
document.body.classList.remove(tempClass);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ import { CoreRecaptchaComponent } from './recaptcha/recaptcha';
|
|||
import { CoreRecaptchaModalComponent } from './recaptcha/recaptchamodal';
|
||||
import { CoreShowPasswordComponent } from './show-password/show-password';
|
||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||
import { CoreTabsComponent } from './tabs/tabs';
|
||||
|
||||
import { CoreDirectivesModule } from '@app/directives/directives.module';
|
||||
import { CorePipesModule } from '@app/pipes/pipes.module';
|
||||
|
||||
|
@ -44,6 +46,7 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
|
|||
CoreRecaptchaModalComponent,
|
||||
CoreShowPasswordComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
CoreTabsComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -64,6 +67,7 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
|
|||
CoreRecaptchaModalComponent,
|
||||
CoreShowPasswordComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
CoreTabsComponent,
|
||||
],
|
||||
})
|
||||
export class CoreComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<ion-tabs>
|
||||
<ion-tab-bar slot="top" class="core-tabs-bar" [hidden]="!tabs || numTabsShown < 1">
|
||||
<ion-spinner *ngIf="!hideUntil"></ion-spinner>
|
||||
<ion-row *ngIf="hideUntil">
|
||||
<ion-col class="col-with-arrow ion-no-padding" (click)="slidePrev()" size="1">
|
||||
<ion-icon *ngIf="showPrevButton" name="fa-chevron-left"></ion-icon>
|
||||
</ion-col>
|
||||
<ion-col class="ion-no-padding" size="10">
|
||||
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slideOpts" [dir]="direction" role="tablist"
|
||||
[attr.aria-label]="description" aria-hidden="false">
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<ion-slide [hidden]="!hideUntil" [attr.aria-selected]="selected == tab.id" class="tab-slide"
|
||||
[attr.aria-label]="tab.title | translate" role="tab" [attr.aria-controls]="tab.id" [id]="tab.id + '-tab'"
|
||||
[tabindex]="selected == tab.id ? null : -1">
|
||||
|
||||
<ion-tab-button (ionTabButtonClick)="selectTab(tab.id, $event)" [tab]="tab.page" [layout]="layout"
|
||||
class="{{tab.class}}">
|
||||
<ion-icon *ngIf="tab.icon" [name]="tab.icon"></ion-icon>
|
||||
<ion-label>{{ tab.title | translate}}</ion-label>
|
||||
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
|
||||
</ion-tab-button>
|
||||
</ion-slide>
|
||||
</ng-container>
|
||||
</ion-slides>
|
||||
</ion-col>
|
||||
<ion-col class="col-with-arrow ion-no-padding" (click)="slideNext()" size="1">
|
||||
<ion-icon *ngIf="showNextButton" name="fa-chevron-right"></ion-icon>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-tab-bar>
|
||||
</ion-tabs>
|
|
@ -0,0 +1,72 @@
|
|||
:host {
|
||||
--tabs-background: var(--background);
|
||||
--tabs-color: var(--color);
|
||||
height: 100%;
|
||||
display: block;
|
||||
|
||||
ion-tabs {
|
||||
background: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ion-tab-bar.core-tabs-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: var(--tabs-background);
|
||||
color: var(--tabs-color);
|
||||
-webkit-filter: drop-shadow(0px 3px 3px rgba(var(--drop-shadow)));
|
||||
filter: drop-shadow(0px 3px 3px rgba(var(--drop-shadow)));
|
||||
border: 0;
|
||||
|
||||
ion-row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-slide {
|
||||
border-bottom: 2px solid transparent;
|
||||
min-width: 100px;
|
||||
min-height: 56px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
ion-tab-button {
|
||||
ion-label {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
line-height: 1.2em;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-selected=true] {
|
||||
color: var(--color-active);
|
||||
border-bottom-color: var(--border-color-active);
|
||||
ion-tab-button {
|
||||
color: var(--color-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ion-col {
|
||||
text-align: center;
|
||||
line-height: 1.6rem;
|
||||
|
||||
&.col-with-arrow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.tabs-hidden {
|
||||
display: none !important;
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,629 @@
|
|||
// (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,
|
||||
Output,
|
||||
EventEmitter,
|
||||
OnInit,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
AfterViewInit,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
} from '@angular/core';
|
||||
import { Platform, IonSlides, IonTabs, NavController } from '@ionic/angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreConfig } from '@services/config';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
import { CoreUtils } from '@/app/services/utils/utils';
|
||||
import { NavigationOptions } from '@ionic/angular/providers/nav-controller';
|
||||
import { Params } from '@angular/router';
|
||||
|
||||
/**
|
||||
* This component displays some top scrollable tabs that will autohide on vertical scroll.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <core-tabs selectedIndex="1" [tabs]="tabs"></core-tabs>
|
||||
*
|
||||
* Tab contents will only be shown if that tab is selected.
|
||||
*
|
||||
* @todo: Test behaviour when tabs are added late.
|
||||
* @todo: Test RTL and tab history.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-tabs',
|
||||
templateUrl: 'core-tabs.html',
|
||||
styleUrls: ['tabs.scss'],
|
||||
})
|
||||
export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
|
||||
|
||||
|
||||
// Minimum tab's width to display fully the word "Competencies" which is the longest tab in the app.
|
||||
protected static readonly MIN_TAB_WIDTH = 107;
|
||||
// Max height that allows tab hiding.
|
||||
protected static readonly MAX_HEIGHT_TO_HIDE_TABS = 768;
|
||||
|
||||
@Input() protected selectedIndex = 0; // Index of the tab to select.
|
||||
@Input() hideUntil = false; // Determine when should the contents be shown.
|
||||
/**
|
||||
* Determine tabs layout.
|
||||
*/
|
||||
@Input() layout: 'icon-top' | 'icon-start' | 'icon-end' | 'icon-bottom' | 'icon-hide' | 'label-hide' = 'icon-hide';
|
||||
@Input() tabs: CoreTab[] = [];
|
||||
@Output() protected ionChange: EventEmitter<CoreTab> = new EventEmitter<CoreTab>(); // Emitted when the tab changes.
|
||||
|
||||
@ViewChild(IonSlides) protected slides?: IonSlides;
|
||||
@ViewChild(IonTabs) protected ionTabs?: IonTabs;
|
||||
|
||||
selected?: string; // Selected tab id.
|
||||
showPrevButton = false;
|
||||
showNextButton = false;
|
||||
maxSlides = 3;
|
||||
numTabsShown = 0;
|
||||
direction = 'ltr';
|
||||
description = '';
|
||||
lastScroll = 0;
|
||||
slideOpts = {
|
||||
initialSlide: 0,
|
||||
slidesPerView: 3,
|
||||
centerInsufficientSlides: true,
|
||||
};
|
||||
|
||||
protected initialized = false;
|
||||
protected afterViewInitTriggered = false;
|
||||
|
||||
protected tabBarHeight = 0;
|
||||
protected tabBarElement?: HTMLIonTabBarElement; // The top tab bar element.
|
||||
protected tabsElement?: HTMLIonTabsElement; // The ionTabs native Element.
|
||||
protected tabsShown = true;
|
||||
protected resizeFunction?: EventListenerOrEventListenerObject;
|
||||
protected isDestroyed = false;
|
||||
protected isCurrentView = true;
|
||||
protected shouldSlideToInitial = false; // Whether we need to slide to the initial slide because it's out of view.
|
||||
protected hasSliddenToInitial = false; // Whether we've already slidden to the initial slide or there was no need.
|
||||
protected selectHistory: string[] = [];
|
||||
|
||||
protected firstSelectedTab?: string; // ID of the first selected tab to control history.
|
||||
protected unregisterBackButtonAction: any;
|
||||
protected languageChangedSubscription: Subscription;
|
||||
protected isInTransition = false; // Weather Slides is in transition.
|
||||
protected slidesSwiper: any;
|
||||
protected slidesSwiperLoaded = false;
|
||||
|
||||
constructor(
|
||||
protected element: ElementRef,
|
||||
platform: Platform,
|
||||
translate: TranslateService,
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
this.direction = platform.isRTL ? 'rtl' : 'ltr';
|
||||
|
||||
// Change the side when the language changes.
|
||||
this.languageChangedSubscription = translate.onLangChange.subscribe(() => {
|
||||
setTimeout(() => {
|
||||
this.direction = platform.isRTL ? 'rtl' : 'ltr';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.tabs.forEach((tab) => {
|
||||
this.initTab(tab);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Init tab info.
|
||||
*
|
||||
* @param tab Tab class.
|
||||
*/
|
||||
protected initTab(tab: CoreTab): void {
|
||||
tab.id = tab.id || 'core-tab-' + CoreUtils.instance.getUniqueId('CoreTabsComponent');
|
||||
if (typeof tab.enabled == 'undefined') {
|
||||
tab.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View has been initialized.
|
||||
*/
|
||||
async ngAfterViewInit(): Promise<void> {
|
||||
if (this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabBarElement = this.element.nativeElement.querySelector('ion-tab-bar');
|
||||
this.tabsElement = this.element.nativeElement.querySelector('ion-tabs');
|
||||
|
||||
this.slidesSwiper = await this.slides?.getSwiper();
|
||||
this.slidesSwiper.once('progress', () => {
|
||||
this.slidesSwiperLoaded = true;
|
||||
this.calculateSlides();
|
||||
});
|
||||
|
||||
this.afterViewInitTriggered = true;
|
||||
|
||||
if (!this.initialized && this.hideUntil) {
|
||||
// Tabs should be shown, initialize them.
|
||||
await this.initializeTabs();
|
||||
}
|
||||
|
||||
this.resizeFunction = this.windowResized.bind(this);
|
||||
|
||||
window.addEventListener('resize', this.resizeFunction!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(): void {
|
||||
this.tabs.forEach((tab) => {
|
||||
this.initTab(tab);
|
||||
});
|
||||
|
||||
// We need to wait for ngAfterViewInit because we need core-tab components to be executed.
|
||||
if (!this.initialized && this.hideUntil && this.afterViewInitTriggered) {
|
||||
// Tabs should be shown, initialize them.
|
||||
// Use a setTimeout so child core-tab update their inputs before initializing the tabs.
|
||||
setTimeout(() => {
|
||||
this.initializeTabs();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User entered the page that contains the component.
|
||||
*/
|
||||
ionViewDidEnter(): void {
|
||||
this.isCurrentView = true;
|
||||
|
||||
this.calculateSlides();
|
||||
|
||||
this.registerBackButtonAction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register back button action.
|
||||
*/
|
||||
protected registerBackButtonAction(): void {
|
||||
this.unregisterBackButtonAction = CoreApp.instance.registerBackButtonAction(() => {
|
||||
// The previous page in history is not the last one, we need the previous one.
|
||||
if (this.selectHistory.length > 1) {
|
||||
const tabIndex = this.selectHistory[this.selectHistory.length - 2];
|
||||
|
||||
// Remove curent and previous tabs from history.
|
||||
this.selectHistory = this.selectHistory.filter((tabId) => this.selected != tabId && tabIndex != tabId);
|
||||
|
||||
this.selectTab(tabIndex);
|
||||
|
||||
return true;
|
||||
} else if (this.selected != this.firstSelectedTab) {
|
||||
// All history is gone but we are not in the first selected tab.
|
||||
this.selectHistory = [];
|
||||
|
||||
this.selectTab(this.firstSelectedTab!);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, 750);
|
||||
}
|
||||
|
||||
/**
|
||||
* User left the page that contains the component.
|
||||
*/
|
||||
ionViewDidLeave(): void {
|
||||
// Unregister the custom back button action for this page
|
||||
this.unregisterBackButtonAction && this.unregisterBackButtonAction();
|
||||
|
||||
this.isCurrentView = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate slides.
|
||||
*/
|
||||
protected async calculateSlides(): Promise<void> {
|
||||
if (!this.isCurrentView || !this.initialized) {
|
||||
// Don't calculate if component isn't in current view, the calculations are wrong.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.tabsShown) {
|
||||
if (window.innerHeight >= CoreTabsComponent.MAX_HEIGHT_TO_HIDE_TABS) {
|
||||
// Ensure tabbar is shown.
|
||||
this.tabsShown = true;
|
||||
this.tabBarElement!.classList.remove('tabs-hidden');
|
||||
this.lastScroll = 0;
|
||||
}
|
||||
}
|
||||
|
||||
await this.calculateMaxSlides();
|
||||
|
||||
this.updateSlides();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the tab bar height.
|
||||
*/
|
||||
protected calculateTabBarHeight(): void {
|
||||
if (!this.tabBarElement || !this.tabsElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabBarHeight = this.tabBarElement.offsetHeight;
|
||||
|
||||
if (this.tabsShown) {
|
||||
// Smooth translation.
|
||||
this.tabsElement.style.top = - this.lastScroll + 'px';
|
||||
this.tabsElement.style.height = 'calc(100% + ' + scroll + 'px';
|
||||
} else {
|
||||
this.tabBarElement.classList.add('tabs-hidden');
|
||||
this.tabsElement.style.top = '0';
|
||||
this.tabsElement.style.height = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tab on a index.
|
||||
*
|
||||
* @param tabId Tab ID.
|
||||
* @return Selected tab.
|
||||
*/
|
||||
protected getTabIndex(tabId: string): number {
|
||||
return this.tabs.findIndex((tab) => tabId == tab.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current selected tab.
|
||||
*
|
||||
* @return Selected tab.
|
||||
*/
|
||||
getSelected(): CoreTab | undefined {
|
||||
const index = this.selected && this.getTabIndex(this.selected);
|
||||
|
||||
return index && index >= 0 ? this.tabs[index] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the tabs, determining the first tab to be shown.
|
||||
*/
|
||||
protected async initializeTabs(): Promise<void> {
|
||||
let selectedTab: CoreTab | undefined = this.tabs[this.selectedIndex || 0] || undefined;
|
||||
|
||||
if (!selectedTab || !selectedTab.enabled) {
|
||||
// The tab is not enabled or not shown. Get the first tab that is enabled.
|
||||
selectedTab = this.tabs.find((tab) => tab.enabled) || undefined;
|
||||
}
|
||||
|
||||
if (!selectedTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.firstSelectedTab = selectedTab.id;
|
||||
this.selectTab(this.firstSelectedTab);
|
||||
|
||||
// Setup tab scrolling.
|
||||
this.calculateTabBarHeight();
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
// Check which arrows should be shown.
|
||||
this.calculateSlides();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method executed when the slides are changed.
|
||||
*/
|
||||
async slideChanged(): Promise<void> {
|
||||
if (!this.slidesSwiperLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isInTransition = false;
|
||||
const slidesCount = await this.slides?.length() || 0;
|
||||
if (slidesCount > 0) {
|
||||
this.showPrevButton = !await this.slides?.isBeginning();
|
||||
this.showNextButton = !await this.slides?.isEnd();
|
||||
} else {
|
||||
this.showPrevButton = false;
|
||||
this.showNextButton = false;
|
||||
}
|
||||
|
||||
const currentIndex = await this.slides!.getActiveIndex();
|
||||
if (this.shouldSlideToInitial && currentIndex != this.selectedIndex) {
|
||||
// Current tab has changed, don't slide to initial anymore.
|
||||
this.shouldSlideToInitial = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the number of slides to show.
|
||||
*/
|
||||
protected async updateSlides(): Promise<void> {
|
||||
this.numTabsShown = this.tabs.reduce((prev: number, current: CoreTab) => current.enabled ? prev + 1 : prev, 0);
|
||||
|
||||
this.slideOpts.slidesPerView = Math.min(this.maxSlides, this.numTabsShown);
|
||||
this.slidesSwiper.params.slidesPerView = this.slideOpts.slidesPerView;
|
||||
|
||||
this.calculateTabBarHeight();
|
||||
await this.slides!.update();
|
||||
|
||||
if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slideOpts.slidesPerView) {
|
||||
this.hasSliddenToInitial = true;
|
||||
this.shouldSlideToInitial = true;
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.shouldSlideToInitial) {
|
||||
this.slides!.slideTo(this.selectedIndex, 0);
|
||||
this.shouldSlideToInitial = false;
|
||||
}
|
||||
}, 400);
|
||||
|
||||
return;
|
||||
} else if (this.selectedIndex) {
|
||||
this.hasSliddenToInitial = true;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.slideChanged(); // Call slide changed again, sometimes the slide active index takes a while to be updated.
|
||||
}, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the number of slides that can fit on the screen.
|
||||
*/
|
||||
protected async calculateMaxSlides(): Promise<void> {
|
||||
if (!this.slidesSwiperLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.maxSlides = 3;
|
||||
const width = this.slidesSwiper.width;
|
||||
if (width) {
|
||||
const fontSize = await
|
||||
CoreConfig.instance.get(CoreConstants.SETTINGS_FONT_SIZE, CoreConstants.CONFIG.font_sizes[0]);
|
||||
|
||||
this.maxSlides = Math.floor(width / (fontSize / CoreConstants.CONFIG.font_sizes[0] *
|
||||
CoreTabsComponent.MIN_TAB_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that shows the next tab.
|
||||
*/
|
||||
async slideNext(): Promise<void> {
|
||||
// Stop if slides are in transition.
|
||||
if (!this.showNextButton || this.isInTransition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.slides!.isBeginning()) {
|
||||
// Slide to the second page.
|
||||
this.slides!.slideTo(this.maxSlides);
|
||||
} else {
|
||||
const currentIndex = await this.slides!.getActiveIndex();
|
||||
if (typeof currentIndex !== 'undefined') {
|
||||
const nextSlideIndex = currentIndex + this.maxSlides;
|
||||
this.isInTransition = true;
|
||||
if (nextSlideIndex < this.numTabsShown) {
|
||||
// Slide to the next page.
|
||||
await this.slides!.slideTo(nextSlideIndex);
|
||||
} else {
|
||||
// Slide to the latest slide.
|
||||
await this.slides!.slideTo(this.numTabsShown - 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that shows the previous tab.
|
||||
*/
|
||||
async slidePrev(): Promise<void> {
|
||||
// Stop if slides are in transition.
|
||||
if (!this.showPrevButton || this.isInTransition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.slides!.isEnd()) {
|
||||
this.slides!.slideTo(this.numTabsShown - this.maxSlides * 2);
|
||||
// Slide to the previous of the latest page.
|
||||
} else {
|
||||
const currentIndex = await this.slides!.getActiveIndex();
|
||||
if (typeof currentIndex !== 'undefined') {
|
||||
const prevSlideIndex = currentIndex - this.maxSlides;
|
||||
this.isInTransition = true;
|
||||
if (prevSlideIndex >= 0) {
|
||||
// Slide to the previous page.
|
||||
await this.slides!.slideTo(prevSlideIndex);
|
||||
} else {
|
||||
// Slide to the first page.
|
||||
await this.slides!.slideTo(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the tabs. This is used when the user is scrolling inside a tab.
|
||||
*
|
||||
* @param scrollEvent Scroll event to check scroll position.
|
||||
* @param content Content element to check measures.
|
||||
*/
|
||||
protected showHideTabs(scrollEvent: CustomEvent, content: HTMLElement): void {
|
||||
if (!this.tabBarElement || !this.tabsElement || !content) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always show on very tall screens.
|
||||
if (window.innerHeight >= CoreTabsComponent.MAX_HEIGHT_TO_HIDE_TABS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.tabBarHeight && this.tabBarElement.offsetHeight != this.tabBarHeight) {
|
||||
// Wrong tab height, recalculate it.
|
||||
this.calculateTabBarHeight();
|
||||
}
|
||||
|
||||
if (!this.tabBarHeight) {
|
||||
// We don't have the tab bar height, this means the tab bar isn't shown.
|
||||
return;
|
||||
}
|
||||
|
||||
const scroll = parseInt(scrollEvent.detail.scrollTop, 10);
|
||||
if (scroll <= 0) {
|
||||
// Ensure tabbar is shown.
|
||||
this.tabsElement.style.top = '0';
|
||||
this.tabsElement.style.height = '';
|
||||
this.tabBarElement!.classList.remove('tabs-hidden');
|
||||
this.tabsShown = true;
|
||||
this.lastScroll = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (scroll == this.lastScroll) {
|
||||
// Ensure scroll has been modified to avoid flicks.
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.tabsShown && scroll > this.tabBarHeight) {
|
||||
this.tabsShown = false;
|
||||
|
||||
// Hide tabs.
|
||||
this.tabBarElement.classList.add('tabs-hidden');
|
||||
this.tabsElement.style.top = '0';
|
||||
this.tabsElement.style.height = '';
|
||||
} else if (!this.tabsShown && scroll <= this.tabBarHeight) {
|
||||
this.tabsShown = true;
|
||||
this.tabBarElement!.classList.remove('tabs-hidden');
|
||||
}
|
||||
|
||||
if (this.tabsShown && content.scrollHeight > content.clientHeight + (this.tabBarHeight - scroll)) {
|
||||
// Smooth translation.
|
||||
this.tabsElement.style.top = - scroll + 'px';
|
||||
this.tabsElement.style.height = 'calc(100% + ' + scroll + 'px';
|
||||
}
|
||||
// Use lastScroll after moving the tabs to avoid flickering.
|
||||
this.lastScroll = parseInt(scrollEvent.detail.scrollTop, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab selected.
|
||||
*
|
||||
* @param tabId Selected tab index.
|
||||
* @param e Event.
|
||||
*/
|
||||
async selectTab(tabId: string, e?: Event): Promise<void> {
|
||||
let index = this.tabs.findIndex((tab) => tabId == tab.id);
|
||||
if (index < 0 || index >= this.tabs.length) {
|
||||
if (this.selected) {
|
||||
// Invalid index do not change tab.
|
||||
e && e.preventDefault();
|
||||
e && e.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Index isn't valid, select the first one.
|
||||
index = 0;
|
||||
}
|
||||
|
||||
const selectedTab = this.tabs[index];
|
||||
if (tabId == this.selected || !selectedTab || !selectedTab.enabled) {
|
||||
// Already selected or not enabled.
|
||||
|
||||
e && e.preventDefault();
|
||||
e && e.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selected) {
|
||||
await this.slides!.slideTo(index);
|
||||
}
|
||||
|
||||
const pageParams: NavigationOptions = {};
|
||||
if (selectedTab.pageParams) {
|
||||
pageParams.queryParams = selectedTab.pageParams;
|
||||
}
|
||||
const ok = await this.navCtrl.navigateForward(selectedTab.page, pageParams);
|
||||
|
||||
if (ok) {
|
||||
this.selectHistory.push(tabId);
|
||||
this.selected = tabId;
|
||||
this.selectedIndex = index;
|
||||
|
||||
this.ionChange.emit(selectedTab);
|
||||
|
||||
const content = this.ionTabs!.outlet.nativeEl.querySelector('ion-content');
|
||||
|
||||
if (content) {
|
||||
const scroll = await content.getScrollElement();
|
||||
content.scrollEvents = true;
|
||||
content.addEventListener('ionScroll', (e: CustomEvent): void => {
|
||||
this.showHideTabs(e, scroll);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt tabs to a window resize.
|
||||
*/
|
||||
protected windowResized(): void {
|
||||
setTimeout(() => {
|
||||
this.calculateSlides();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
|
||||
if (this.resizeFunction) {
|
||||
window.removeEventListener('resize', this.resizeFunction);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Core Tab class.
|
||||
*/
|
||||
class CoreTab {
|
||||
|
||||
id = ''; // Unique tab id.
|
||||
class = ''; // Class, if needed.
|
||||
title = ''; // The translatable tab title.
|
||||
icon?: string; // The tab icon.
|
||||
badge?: string; // A badge to add in the tab.
|
||||
badgeStyle?: string; // The badge color.
|
||||
enabled = true; // Whether the tab is enabled.
|
||||
page = ''; // Page to navigate to.
|
||||
pageParams?: Params; // Page params.
|
||||
|
||||
}
|
|
@ -13,9 +13,27 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { CoreHomeDelegate } from '../mainmenu/services/home.delegate';
|
||||
import { CoreCoursesDashboardHandler } from './handlers/dashboard';
|
||||
import { CoreCoursesDashboardPage } from './pages/dashboard/dashboard.page';
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: CoreCoursesDashboardPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
declarations: [],
|
||||
})
|
||||
export class CoreCoursesModule { }
|
||||
export class CoreCoursesModule {
|
||||
|
||||
constructor(homeDelegate: CoreHomeDelegate) {
|
||||
homeDelegate.registerHandler(new CoreCoursesDashboardHandler());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
// (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 { CoreHomeHandler, CoreHomeHandlerData } from '@core/mainmenu/services/home.delegate';
|
||||
|
||||
/**
|
||||
* Handler to add Home into main menu.
|
||||
*/
|
||||
export class CoreCoursesDashboardHandler implements CoreHomeHandler {
|
||||
|
||||
name = 'CoreCoursesDashboard';
|
||||
priority = 1100;
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): Promise<boolean> {
|
||||
return this.isEnabledForSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a certain site.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async isEnabledForSite(siteId?: string): Promise<boolean> {
|
||||
// @todo
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data needed to render the handler.
|
||||
*
|
||||
* @return Data needed to render the handler.
|
||||
*/
|
||||
getDisplayData(): CoreHomeHandlerData {
|
||||
return {
|
||||
title: 'core.courses.mymoodle',
|
||||
page: 'home/dashboard',
|
||||
class: 'core-courses-dashboard-handler',
|
||||
icon: 'fa-tachometer-alt',
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<ion-content>
|
||||
<!-- @todo -->
|
||||
<core-empty-box icon="fa-home" [message]="'core.courses.nocourses' | translate">
|
||||
<div>Dashboard</div>
|
||||
</core-empty-box>
|
||||
</ion-content>
|
|
@ -0,0 +1,47 @@
|
|||
// (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 { CommonModule } from '@angular/common';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
import { CoreCoursesDashboardPage } from './dashboard.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreCoursesDashboardPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreCoursesDashboardPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreCoursesDashboardPageModule {}
|
|
@ -0,0 +1,36 @@
|
|||
// (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, OnInit } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Page that displays the Home.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-courses-dashboard',
|
||||
templateUrl: 'dashboard.html',
|
||||
styleUrls: ['dashboard.scss'],
|
||||
})
|
||||
export class CoreCoursesDashboardPage implements OnInit {
|
||||
|
||||
siteName = 'Hello world';
|
||||
|
||||
/**
|
||||
* Initialize the component.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// @todo
|
||||
}
|
||||
|
||||
}
|
|
@ -45,7 +45,7 @@
|
|||
</div>
|
||||
|
||||
<div *ngIf="step == 2" class="core-login-onboarding-step">
|
||||
<ul class="core-login-onboarding-text">
|
||||
<ul class="core-login-onboarding-text ion-text-start">
|
||||
<li><ion-icon name="far-check-circle"></ion-icon> {{ 'core.login.onboardingcreatemanagecourses' | translate }}</li>
|
||||
<li><ion-icon name="far-check-circle"></ion-icon> {{ 'core.login.onboardingenrolmanagestudents' | translate }}</li>
|
||||
<li><ion-icon name="far-check-circle"></ion-icon> {{ 'core.login.onboardingprovidefeedback' | translate }}</li>
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
.core-login-onboarding-step {
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
/* @todo @include media-breakpoint-up(md) {
|
||||
@media (min-width: 768px) {
|
||||
max-width: 80%;
|
||||
}*/
|
||||
}
|
||||
margin: 0 auto;
|
||||
|
||||
p {
|
||||
|
@ -14,8 +14,7 @@
|
|||
ul {
|
||||
margin-bottom: 30px;
|
||||
list-style-type: none;
|
||||
// @todo @include text-align('start');
|
||||
// @todo @include padding-horizontal(10px, null);
|
||||
padding-inline-start: 10px;
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,16 @@
|
|||
--color: var(--core-login-text-color);
|
||||
}
|
||||
|
||||
img {
|
||||
form .item.item-input,
|
||||
form .core-username.item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
form .core-username.item p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.core-login-site-logo img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
@ -14,6 +23,7 @@
|
|||
|
||||
.core-sitename {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.core-login-site-logo {
|
||||
|
@ -25,4 +35,8 @@
|
|||
.core-login-forgotten-password {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.core-login-reconnect-warning {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
<ion-title>{{ 'core.login.login' | translate }}</ion-title>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button router-direction="forward" routerLink="/settings/app"
|
||||
<ion-button router-direction="forward" routerLink="/settings"
|
||||
[attr.aria-label]="'core.settings.appsettings' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-cog"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-content class="ion-padding">
|
||||
<core-loading [hideUntil]="pageLoaded">
|
||||
<div class="ion-text-wrap ion-text-center ion-margin-bottom">
|
||||
<div class="core-login-site-logo">
|
||||
|
@ -32,7 +32,8 @@
|
|||
<form ion-list [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #credentialsForm>
|
||||
<ion-item *ngIf="siteChecked && !isBrowserSSO">
|
||||
<ion-input type="text" name="username" placeholder="{{ 'core.login.username' | translate }}"
|
||||
formControlName="username" autocapitalize="none" autocorrect="off"></ion-input>
|
||||
formControlName="username" autocapitalize="none" autocorrect="off" autocomplete="username" enterkeyhint="next"
|
||||
required="true"></ion-input>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="siteChecked && !isBrowserSSO" class="ion-margin-bottom">
|
||||
<core-show-password [name]="'password'">
|
||||
|
|
|
@ -7,15 +7,12 @@
|
|||
<ion-title>{{ 'core.login.reconnect' | translate }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-content class="ion-padding">
|
||||
<div class="ion-text-wrap ion-text-center ion-margin-bottom" [ngClass]="{'item-avatar-center': showSiteAvatar}">
|
||||
<ng-container *ngIf="showSiteAvatar">
|
||||
<ion-avatar>
|
||||
<!-- Show user avatar. -->
|
||||
<img [src]="userAvatar" class="avatar" core-external-content [siteId]="siteId" role="presentation"
|
||||
alt="{{ 'core.pictureof' | translate:{$a: userFullName} }}" onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
</ng-container>
|
||||
<!-- Show user avatar. -->
|
||||
<img *ngIf="showSiteAvatar" [src]="userAvatar" class="large-avatar" core-external-content [siteId]="siteId"
|
||||
role="presentation"
|
||||
alt="{{ 'core.pictureof' | translate:{$a: userFullName} }}" onError="this.src='assets/img/user-avatar.png'">
|
||||
|
||||
<div class="core-login-site-logo" *ngIf="!showSiteAvatar">
|
||||
<!-- Show site logo or a default image. -->
|
||||
|
@ -29,14 +26,14 @@
|
|||
<p class="core-siteurl">{{siteUrl}}</p>
|
||||
|
||||
<ion-item *ngIf="!isLoggedOut" class="ion-text-center core-login-reconnect-warning" lines="none">
|
||||
<ion-label color="danger">
|
||||
<ion-label color="danger" class="ion-text-wrap">
|
||||
<ion-icon name="fas-exclamation-circle" slot="start"></ion-icon>
|
||||
{{ 'core.login.reconnectdescription' | translate }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
<form ion-list *ngIf="!isOAuth" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm>
|
||||
<ion-item class="ion-text-wrap core-username">
|
||||
<form *ngIf="!isOAuth" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm>
|
||||
<ion-item class="ion-text-wrap core-username item-interactive">
|
||||
<ion-label>
|
||||
<p>{{username}}</p>
|
||||
</ion-label>
|
||||
|
@ -44,7 +41,8 @@
|
|||
<ion-item class="ion-margin-bottom">
|
||||
<core-show-password [name]="'password'">
|
||||
<ion-input class="core-ioninput-password" name="password" type="password"
|
||||
placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false">
|
||||
placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"
|
||||
autocomplete="current-password" enterkeyhint="go" required="true">
|
||||
</ion-input>
|
||||
</core-show-password>
|
||||
</ion-item>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<ion-title>{{ 'core.login.connecttomoodle' | translate }}</ion-title>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button router-direction="forward" routerLink="/settings/app"
|
||||
<ion-button router-direction="forward" routerLink="/settings"
|
||||
[attr.aria-label]="'core.settings.appsettings' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-cog"></ion-icon>
|
||||
</ion-button>
|
||||
|
|
|
@ -80,7 +80,10 @@
|
|||
|
||||
.core-login-site-list-loading {
|
||||
position: absolute;
|
||||
//@todo @include position(0, 0, 0, 0);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
@ -89,11 +92,12 @@
|
|||
background-color: rgba(255, 255, 255, 0.5);
|
||||
z-index: 1;
|
||||
ion-spinner {
|
||||
flex: 1;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.core-login-site-nolist-loading {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -102,30 +106,29 @@
|
|||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
/* @todo
|
||||
@include media-breakpoint-up(md) {
|
||||
.scroll-content > * {
|
||||
|
||||
@media (min-width: 768px) {
|
||||
ion-content > * {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.core-login-site-logo {
|
||||
margin-top: 20%;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
margin: 0;
|
||||
ion-content .core-login-site-logo {
|
||||
margin-top: 20%;
|
||||
&.hidden {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
.core-login-entered-site {
|
||||
background-color: gray; // @todo $gray-lighter;
|
||||
background-color: var(--gray-lighter);
|
||||
ion-thumbnail {
|
||||
box-shadow: 0 0 4px #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.core-login-default-icon {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<ion-button *ngIf="sites && sites.length > 0" (click)="toggleDelete()" [attr.aria-label]="'core.delete' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-pencil-alt"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button router-direction="forward" routerLink="/settings/app"
|
||||
<ion-button router-direction="forward" routerLink="/settings"
|
||||
[attr.aria-label]="'core.settings.appsettings' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-cog"></ion-icon>
|
||||
</ion-button>
|
||||
|
|
|
@ -16,7 +16,12 @@
|
|||
</ion-header>
|
||||
<ion-content>
|
||||
<!-- @todo -->
|
||||
<core-empty-box icon="fa-home" [message]="'core.courses.nocourses' | translate">
|
||||
<div>Home page</div>
|
||||
</core-empty-box>
|
||||
<core-loading [hideUntil]="loaded" *ngIf="tabs.length > 0">
|
||||
<core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" [tabs]="tabs"></core-tabs>
|
||||
<ng-container *ngIf="tabs.length == 0">
|
||||
<core-empty-box icon="fa-home" [message]="'core.courses.nocourses' | translate">
|
||||
<div>Home page</div>
|
||||
</core-empty-box>
|
||||
</ng-container>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
|
|
@ -27,6 +27,13 @@ const routes: Routes = [
|
|||
{
|
||||
path: '',
|
||||
component: CoreHomePage,
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard', // @todo: Add this route dynamically.
|
||||
loadChildren: () =>
|
||||
import('@core/courses/pages/dashboard/dashboard.page.module').then(m => m.CoreCoursesDashboardPageModule),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -12,7 +12,10 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CoreHomeDelegate, CoreHomeHandlerToDisplay } from '../../services/home.delegate';
|
||||
|
||||
/**
|
||||
* Page that displays the Home.
|
||||
|
@ -24,13 +27,65 @@ import { Component, OnInit } from '@angular/core';
|
|||
})
|
||||
export class CoreHomePage implements OnInit {
|
||||
|
||||
siteName = 'Hello world';
|
||||
siteName!: string;
|
||||
tabs: CoreHomeHandlerToDisplay[] = [];
|
||||
loaded = false;
|
||||
selectedTab?: number;
|
||||
|
||||
protected subscription?: Subscription;
|
||||
|
||||
constructor(
|
||||
protected homeDelegate: CoreHomeDelegate,
|
||||
) {
|
||||
this.loadSiteName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// @todo
|
||||
this.subscription = this.homeDelegate.getHandlers().subscribe((handlers) => {
|
||||
handlers && this.initHandlers(handlers);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Init handlers on change (size or handlers).
|
||||
*/
|
||||
initHandlers(handlers: CoreHomeHandlerToDisplay[]): void {
|
||||
// Re-build the list of tabs. If a handler is already in the list, use existing object to prevent re-creating the tab.
|
||||
const newTabs: CoreHomeHandlerToDisplay[] = handlers.map((handler) => {
|
||||
// Check if the handler is already in the tabs list. If so, use it.
|
||||
const tab = this.tabs.find((tab) => tab.title == handler.title);
|
||||
|
||||
return tab || handler;
|
||||
})
|
||||
// Sort them by priority so new handlers are in the right position.
|
||||
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||
|
||||
if (typeof this.selectedTab == 'undefined') {
|
||||
let maxPriority = 0;
|
||||
let maxIndex = 0;
|
||||
newTabs.forEach((tab, index) => {
|
||||
if ((tab.selectPriority || 0) > maxPriority) {
|
||||
maxPriority = tab.selectPriority || 0;
|
||||
maxIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
this.selectedTab = maxIndex;
|
||||
}
|
||||
|
||||
this.tabs = newTabs;
|
||||
|
||||
this.loaded = this.homeDelegate.areHandlersLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the site name.
|
||||
*/
|
||||
protected loadSiteName(): void {
|
||||
this.siteName = CoreSites.instance.getCurrentSite()!.getSiteName();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<ion-tabs #mainTabs [hidden]="!showTabs">
|
||||
|
||||
<ion-tab-bar slot="bottom">
|
||||
<ion-tabs #mainTabs [hidden]="!showTabs" [class]="'placement-' + tabsPlacement" [class.tabshidden]="hidden">
|
||||
<ion-tab-bar slot="bottom" [hidden]="hidden">
|
||||
<ion-spinner *ngIf="!loaded"></ion-spinner>
|
||||
|
||||
<ion-tab-button tab="redirect" [disabled]="true" [hidden]="true"></ion-tab-button> <!-- [root]="redirectPage" [rootParams]="redirectParams" -->
|
||||
|
|
|
@ -42,6 +42,7 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
|||
redirectParams?: Params;
|
||||
showTabs = false;
|
||||
tabsPlacement = 'bottom';
|
||||
hidden = false;
|
||||
|
||||
protected subscription?: Subscription;
|
||||
protected redirectObs?: CoreEventObserver;
|
||||
|
@ -178,6 +179,22 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change tabs visibility to show/hide them from the view.
|
||||
*
|
||||
* @param visible If show or hide the tabs.
|
||||
*/
|
||||
changeVisibility(visible: boolean): void {
|
||||
if (this.hidden == visible) {
|
||||
// Change needed.
|
||||
this.hidden = !visible;
|
||||
|
||||
/* setTimeout(() => {
|
||||
this.viewCtrl.getContent().resize();
|
||||
});*/
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a redirect.
|
||||
*
|
||||
|
@ -246,7 +263,7 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
|
|||
|
||||
// User confirmed, go to root.
|
||||
this.mainTabs?.select(page);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// User canceled.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,93 +1,89 @@
|
|||
ion-icon.tab-button-icon {
|
||||
text-overflow: unset;
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
transition: margin 500ms ease-in-out, transform 300ms ease-in-out;
|
||||
}
|
||||
:host{
|
||||
--menutabbar-size: 60px;
|
||||
|
||||
.ion-md-fa-graduation-cap,
|
||||
.ion-ios-fa-graduation-cap,
|
||||
.ion-ios-fa-graduation-cap-outline,
|
||||
.ion-fa-graduation-cap {
|
||||
// @todo @extend .fa-graduation-cap;
|
||||
// @todo @extend .fa;
|
||||
font-size: 21px;
|
||||
height: 21px;
|
||||
ion-tabs {
|
||||
-webkit-filter: drop-shadow(0px 3px 3px rgba(var(--drop-shadow)));
|
||||
filter: drop-shadow(0px 3px 3px rgba(var(--drop-shadow)));
|
||||
}
|
||||
|
||||
}
|
||||
ion-tab-button ion-icon {
|
||||
text-overflow: unset;
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ion-ios-fa-graduation-cap-outline {
|
||||
color: transparent;
|
||||
-webkit-text-stroke-width: 0.8px;
|
||||
// @todo -webkit-text-stroke-color: $tabs-tab-color-inactive;
|
||||
font-size: 23px;
|
||||
height: 23px;
|
||||
}
|
||||
ion-tabs.placement-bottom ion-tab-button {
|
||||
ion-icon {
|
||||
transition: margin 500ms ease-in-out, transform 300ms ease-in-out;
|
||||
}
|
||||
ion-badge {
|
||||
top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ion-md-fa-newspaper-o,
|
||||
.ion-ios-fa-newspaper-o,
|
||||
.ion-ios-fa-newspaper-o-outline,
|
||||
.ion-fa-newspaper-o {
|
||||
// @todo @extend .fa-newspaper-o;
|
||||
// @todo @extend .fa;
|
||||
font-size: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.ion-ios-fa-newspaper-o-outline {
|
||||
font-size: 23px;
|
||||
height: 23px;
|
||||
}
|
||||
|
||||
.core-network-message {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
transition: all 500ms ease-in-out;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.core-online-message,
|
||||
.core-offline-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
.core-online ion-app.app-root page-core-mainmenu,
|
||||
.core-offline ion-app.app-root page-core-mainmenu {
|
||||
|
||||
core-ion-tabs[tabsplacement="bottom"] ion-icon.tab-button-icon {
|
||||
margin-bottom: $core-network-message-height / 2;
|
||||
|
||||
&.icon-ios {
|
||||
margin-bottom: 14px;
|
||||
ion-tabs.placement-side {
|
||||
flex-direction: row;
|
||||
ion-tab-bar {
|
||||
order: -1;
|
||||
width: var(--menutabbar-size);
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
ion-tab-button {
|
||||
width: 100%;
|
||||
ion-badge {
|
||||
top: calc(50% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.core-network-message {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
transition: all 500ms ease-in-out;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.core-online-message,
|
||||
.core-offline-message {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.core-online),
|
||||
:host-context(.core-offline) {
|
||||
ion-tabs.placement-bottom ion-tab-button ion-icon {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ion-tabs.placement-bottom ion-tab-button.ios ion-icon {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.core-network-message {
|
||||
visibility: visible;
|
||||
height: $core-network-message-height;
|
||||
height: 16px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.core-offline ion-app.app-root page-core-mainmenu .core-offline-message,
|
||||
.core-online ion-app.app-root page-core-mainmenu .core-online-message {
|
||||
:host-context(.core-offline) .core-offline-message,
|
||||
:host-context(.core-online) .core-online-message {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.core-online ion-app.app-root page-core-mainmenu .core-network-message {
|
||||
background: $green;
|
||||
:host-context(.core-online) .core-network-message {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.core-offline ion-app.app-root page-core-mainmenu .core-network-message {
|
||||
background: $red;
|
||||
}*/
|
||||
:host-context(.core-offline) .core-network-message {
|
||||
background: var(--red);
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
|||
@Component({
|
||||
selector: 'page-core-mainmenu-more',
|
||||
templateUrl: 'more.html',
|
||||
styleUrls: ['more.scss'],
|
||||
})
|
||||
export class CoreMainMenuMorePage implements OnInit, OnDestroy {
|
||||
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
$core-more-icon: $gray-darker !default;
|
||||
$core-more-background-ios: $list-ios-background-color !default;
|
||||
$core-more-background-md: $list-md-background-color !default;
|
||||
$core-more-activated-background-ios: color-shade($core-more-background-ios) !default;
|
||||
$core-more-activated-background-md: color-shade($core-more-background-md) !default;
|
||||
$core-more-divider-ios: $item-ios-divider-background !default;
|
||||
$core-more-divider-md: $item-md-divider-background !default;
|
||||
$core-more-border-ios: $list-ios-border-color !default;
|
||||
$core-more-border-md: $list-md-border-color !default;
|
||||
$core-more-color-ios: $list-ios-text-color!default;
|
||||
$core-more-color-md: $list-md-text-color !default;
|
||||
|
||||
.item-block {
|
||||
&.item-ios {
|
||||
background-color: $core-more-background-ios;
|
||||
color: $core-more-color-ios;
|
||||
p {
|
||||
color: $core-more-color-ios;
|
||||
}
|
||||
|
||||
.item-inner {
|
||||
border-bottom: $hairlines-width solid $core-more-border-ios;
|
||||
}
|
||||
}
|
||||
&.item-md {
|
||||
background-color: $core-more-background-md;
|
||||
color: $core-more-color-md;
|
||||
p {
|
||||
color: $core-more-color-md;
|
||||
}
|
||||
|
||||
.item-inner {
|
||||
border-bottom: 1px solid $core-more-border-md;
|
||||
}
|
||||
}
|
||||
|
||||
&.activated {
|
||||
&.item-ios {
|
||||
background-color: $core-more-activated-background-ios;
|
||||
}
|
||||
&.item-md {
|
||||
background-color: $core-more-activated-background-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
color: $core-more-icon;
|
||||
}
|
||||
|
||||
.item-divider {
|
||||
&.item-ios {
|
||||
background-color: $core-more-divider-ios;
|
||||
}
|
||||
|
||||
&.item-md {
|
||||
background-color: $core-more-divider-md;
|
||||
border-bottom: $core-more-border-md;
|
||||
}
|
||||
}
|
||||
|
||||
@include darkmode() {
|
||||
ion-icon {
|
||||
color: $core-dark-text-color;
|
||||
}
|
||||
|
||||
.item-divider {
|
||||
&.item-ios,
|
||||
&.item-md {
|
||||
color: $core-dark-text-color;
|
||||
background-color: $core-dark-item-divider-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.item-block {
|
||||
&.item-ios,
|
||||
&.item-md {
|
||||
color: $core-dark-text-color;
|
||||
background-color: $core-dark-item-bg-color;
|
||||
p {
|
||||
color: $core-dark-text-color;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.activated {
|
||||
&.item-ios {
|
||||
background-color: $core-more-activated-background-ios;
|
||||
}
|
||||
&.item-md {
|
||||
background-color: $core-more-activated-background-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -0,0 +1,172 @@
|
|||
// (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 { Params } from '@angular/router';
|
||||
import { Subject, BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
|
||||
/**
|
||||
* Interface that all main menu handlers must implement.
|
||||
*/
|
||||
export interface CoreHomeHandler extends CoreDelegateHandler {
|
||||
/**
|
||||
* The highest priority is displayed first.
|
||||
*/
|
||||
priority?: number;
|
||||
|
||||
/**
|
||||
* Returns the data needed to render the handler.
|
||||
*
|
||||
* @return Data.
|
||||
*/
|
||||
getDisplayData(): CoreHomeHandlerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data needed to render a main menu handler. It's returned by the handler.
|
||||
*/
|
||||
export interface CoreHomeHandlerData {
|
||||
/**
|
||||
* Name of the page to load for the handler.
|
||||
*/
|
||||
page: string;
|
||||
|
||||
/**
|
||||
* Title to display for the handler.
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Class to add to the displayed handler.
|
||||
*/
|
||||
class?: string;
|
||||
|
||||
/**
|
||||
* If true, the badge number is being loaded. Only used if showBadge is true.
|
||||
*/
|
||||
loading?: boolean;
|
||||
|
||||
/**
|
||||
* Params to pass to the page.
|
||||
*/
|
||||
pageParams?: Params;
|
||||
|
||||
/**
|
||||
* If the handler has badge to show or not.
|
||||
*/
|
||||
showBadge?: boolean;
|
||||
|
||||
/**
|
||||
* Text to display on the badge. Only used if showBadge is true.
|
||||
*/
|
||||
badge?: string;
|
||||
|
||||
/**
|
||||
* Name of the icon to display for the handler.
|
||||
*/
|
||||
icon?: string; // Name of the icon to display in the tab.
|
||||
}
|
||||
|
||||
/**
|
||||
* Data returned by the delegate for each handler.
|
||||
*/
|
||||
export interface CoreHomeHandlerToDisplay extends CoreHomeHandlerData {
|
||||
/**
|
||||
* Name of the handler.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Priority of the handler.
|
||||
*/
|
||||
priority?: number;
|
||||
|
||||
/**
|
||||
* Priority to select handler.
|
||||
*/
|
||||
selectPriority?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service to interact with plugins to be shown in the main menu. Provides functions to register a plugin
|
||||
* and notify an update in the data.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoreHomeDelegate extends CoreDelegate {
|
||||
|
||||
protected loaded = false;
|
||||
protected siteHandlers: Subject<CoreHomeHandlerToDisplay[]> = new BehaviorSubject<CoreHomeHandlerToDisplay[]>([]);
|
||||
protected featurePrefix = 'CoreHomeDelegate_';
|
||||
|
||||
constructor() {
|
||||
super('CoreHomeDelegate', true);
|
||||
|
||||
CoreEvents.on(CoreEvents.LOGOUT, this.clearSiteHandlers.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if handlers are loaded.
|
||||
*
|
||||
* @return True if handlers are loaded, false otherwise.
|
||||
*/
|
||||
areHandlersLoaded(): boolean {
|
||||
return this.loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear current site handlers. Reserved for core use.
|
||||
*/
|
||||
protected clearSiteHandlers(): void {
|
||||
this.loaded = false;
|
||||
this.siteHandlers.next([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the handlers for the current site.
|
||||
*
|
||||
* @return An observable that will receive the handlers.
|
||||
*/
|
||||
getHandlers(): Subject<CoreHomeHandlerToDisplay[]> {
|
||||
return this.siteHandlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update handlers Data.
|
||||
*/
|
||||
updateData(): void {
|
||||
const displayData: CoreHomeHandlerToDisplay[] = [];
|
||||
|
||||
for (const name in this.enabledHandlers) {
|
||||
const handler = <CoreHomeHandler> this.enabledHandlers[name];
|
||||
const data = <CoreHomeHandlerToDisplay> handler.getDisplayData();
|
||||
|
||||
data.name = name;
|
||||
data.priority = handler.priority;
|
||||
|
||||
displayData.push(data);
|
||||
}
|
||||
|
||||
// Sort them by priority.
|
||||
displayData.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||
|
||||
this.loaded = true;
|
||||
this.siteHandlers.next(displayData);
|
||||
}
|
||||
|
||||
}
|
|
@ -13,14 +13,14 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { CoreApp } from '@services/app';
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
templateUrl: 'app.html',
|
||||
})
|
||||
export class CoreSettingsAppPage {
|
||||
export class CoreSettingsAppPage implements OnInit {
|
||||
|
||||
// @ViewChild(CoreSplitViewComponent) splitviewCtrl?: CoreSplitViewComponent;
|
||||
|
||||
|
@ -42,7 +42,7 @@ export class CoreSettingsAppPage {
|
|||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
ionViewDidLoad(): void {
|
||||
ngOnInit(): void {
|
||||
if (this.selectedPage) {
|
||||
this.openSettings(this.selectedPage);
|
||||
} /* else if (this.splitviewCtrl!.isOn()) {
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
|
||||
<ion-title>{{ 'core.settings.general' | translate }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.settings.language' | translate }}</h2>
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="selectedLanguage" (ionChange)="languageChanged()" interface="action-sheet"
|
||||
[interfaceOptions]="{header: 'core.settings.language' | translate}">
|
||||
<ion-select-option *ngFor="let entry of languages" [value]="entry.code">{{ entry.name }}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap core-settings-general-font-size item-interactive">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.settings.fontsize' | translate }}</h2>
|
||||
</ion-label>
|
||||
<ion-segment [(ngModel)]="selectedFontSize" (ionChange)="fontSizeChanged()" color="primary" item-content>
|
||||
<ion-segment-button *ngFor="let fontSize of fontSizes" [value]="fontSize.size"
|
||||
[ngStyle]="{'font-size.px': fontSize.style}">
|
||||
{{ 'core.settings.fontsizecharacter' | translate }}
|
||||
<!-- Empty element styled with the largest font size, so all buttons share a common baseline. -->
|
||||
<span [ngStyle]="{'font-size.px': fontSizes[fontSizes.length - 1].style}"></span>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap core-settings-general-color-scheme" *ngIf="colorSchemes.length > 0">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.settings.colorscheme' | translate }}</h2>
|
||||
<p *ngIf="colorSchemeDisabled" class="text-danger">{{ 'core.settings.forcedsetting' | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="selectedScheme" (ionChange)="colorSchemeChanged()" interface="action-sheet"
|
||||
[disabled]="colorSchemeDisabled" [interfaceOptions]="{header: 'core.settings.colorscheme' | translate}">
|
||||
<ion-select-option *ngFor="let scheme of colorSchemes" [value]="scheme">
|
||||
{{ 'core.settings.colorscheme-' + scheme | translate }}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.settings.enablerichtexteditor' | translate }}</h2>
|
||||
<p>{{ 'core.settings.enablerichtexteditordescription' | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-toggle [(ngModel)]="richTextEditor" (ionChange)="richTextEditorChanged()"></ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.settings.debugdisplay' | translate }}</h2>
|
||||
<p>{{ 'core.settings.debugdisplaydescription' | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-toggle [(ngModel)]="debugDisplay" (ionChange)="debugDisplayChanged()"></ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="analyticsSupported">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.settings.enablefirebaseanalytics' | translate }}</h2>
|
||||
<p>{{ 'core.settings.enablefirebaseanalyticsdescription' | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-toggle [(ngModel)]="analyticsEnabled" (ionChange)="analyticsEnabledChanged()"></ion-toggle>
|
||||
</ion-item>
|
||||
</ion-content>
|
|
@ -0,0 +1,48 @@
|
|||
// (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 { CommonModule } from '@angular/common';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
import { CoreSettingsGeneralPage } from './general.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreSettingsGeneralPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreSettingsGeneralPage,
|
||||
],
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
FormsModule,
|
||||
],
|
||||
})
|
||||
export class CoreSettingsGeneralPageModule {}
|
|
@ -0,0 +1,168 @@
|
|||
// (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 } from '@angular/core';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
import { CoreConfig } from '@services/config';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreLang } from '@services/lang';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
// import { CorePushNotifications } from '@core/pushnotifications/services/pushnotifications';
|
||||
import { CoreSettingsHelper, CoreColorScheme } from '../../services/settings.helper';
|
||||
|
||||
/**
|
||||
* Page that displays the general settings.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-settings-general',
|
||||
templateUrl: 'general.html',
|
||||
styleUrls: ['general.scss'],
|
||||
})
|
||||
export class CoreSettingsGeneralPage {
|
||||
|
||||
languages: { code: string; name: string }[] = [];
|
||||
selectedLanguage = '';
|
||||
fontSizes: { size: number; style: number; selected: boolean }[] = [];
|
||||
selectedFontSize = 0;
|
||||
richTextEditor = true;
|
||||
debugDisplay = false;
|
||||
analyticsSupported = false;
|
||||
analyticsEnabled = false;
|
||||
colorSchemes: CoreColorScheme[] = [];
|
||||
selectedScheme: CoreColorScheme = CoreColorScheme.LIGHT;
|
||||
colorSchemeDisabled = false;
|
||||
|
||||
constructor() {
|
||||
this.asyncInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Async part of the constructor.
|
||||
*/
|
||||
protected async asyncInit(): Promise<void> {
|
||||
|
||||
// Get the supported languages.
|
||||
const languages = CoreConstants.CONFIG.languages;
|
||||
for (const code in languages) {
|
||||
this.languages.push({
|
||||
code: code,
|
||||
name: languages[code],
|
||||
});
|
||||
}
|
||||
// Sort them by name.
|
||||
this.languages.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.selectedLanguage = await CoreLang.instance.getCurrentLanguage();
|
||||
|
||||
// Configure color schemes.
|
||||
if (!CoreConstants.CONFIG.forceColorScheme) {
|
||||
this.colorSchemeDisabled = CoreSettingsHelper.instance.isColorSchemeDisabledInSite();
|
||||
|
||||
if (this.colorSchemeDisabled) {
|
||||
this.colorSchemes.push(CoreColorScheme.LIGHT);
|
||||
this.selectedScheme = this.colorSchemes[0];
|
||||
} else {
|
||||
this.colorSchemes.push(CoreColorScheme.LIGHT);
|
||||
this.colorSchemes.push(CoreColorScheme.DARK);
|
||||
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches ||
|
||||
window.matchMedia('(prefers-color-scheme: light)').matches) {
|
||||
this.colorSchemes.push(CoreColorScheme.AUTO);
|
||||
}
|
||||
|
||||
this.selectedScheme = await CoreConfig.instance.get(CoreConstants.SETTINGS_COLOR_SCHEME, CoreColorScheme.LIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedFontSize = await CoreConfig.instance.get(
|
||||
CoreConstants.SETTINGS_FONT_SIZE,
|
||||
CoreConstants.CONFIG.font_sizes[0],
|
||||
);
|
||||
|
||||
this.fontSizes = CoreConstants.CONFIG.font_sizes.map((size) =>
|
||||
({
|
||||
size,
|
||||
// Absolute pixel size based on 1.4rem body text when this size is selected.
|
||||
style: Math.round(size * 16 / 100),
|
||||
selected: size === this.selectedFontSize,
|
||||
}));
|
||||
|
||||
|
||||
this.richTextEditor = await CoreConfig.instance.get(CoreConstants.SETTINGS_RICH_TEXT_EDITOR, true);
|
||||
|
||||
this.debugDisplay = await CoreConfig.instance.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, false);
|
||||
|
||||
this.analyticsSupported = CoreConstants.CONFIG.enableanalytics;
|
||||
if (this.analyticsSupported) {
|
||||
this.analyticsEnabled = await CoreConfig.instance.get(CoreConstants.SETTINGS_ANALYTICS_ENABLED, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new language is selected.
|
||||
*/
|
||||
languageChanged(): void {
|
||||
CoreLang.instance.changeCurrentLanguage(this.selectedLanguage).finally(() => {
|
||||
CoreEvents.trigger(CoreEvents.LANGUAGE_CHANGED, this.selectedLanguage);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new font size is selected.
|
||||
*/
|
||||
fontSizeChanged(): void {
|
||||
this.fontSizes = this.fontSizes.map((fontSize) => {
|
||||
fontSize.selected = fontSize.size === this.selectedFontSize;
|
||||
|
||||
return fontSize;
|
||||
});
|
||||
|
||||
CoreSettingsHelper.instance.setFontSize(this.selectedFontSize);
|
||||
CoreConfig.instance.set(CoreConstants.SETTINGS_FONT_SIZE, this.selectedFontSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new color scheme is selected.
|
||||
*/
|
||||
colorSchemeChanged(): void {
|
||||
CoreSettingsHelper.instance.setColorScheme(this.selectedScheme);
|
||||
CoreConfig.instance.set(CoreConstants.SETTINGS_COLOR_SCHEME, this.selectedScheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the rich text editor is enabled or disabled.
|
||||
*/
|
||||
richTextEditorChanged(): void {
|
||||
CoreConfig.instance.set(CoreConstants.SETTINGS_RICH_TEXT_EDITOR, this.richTextEditor ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the debug display setting is enabled or disabled.
|
||||
*/
|
||||
debugDisplayChanged(): void {
|
||||
CoreConfig.instance.set(CoreConstants.SETTINGS_DEBUG_DISPLAY, this.debugDisplay ? 1 : 0);
|
||||
CoreDomUtils.instance.setDebugDisplay(this.debugDisplay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the analytics setting is enabled or disabled.
|
||||
*
|
||||
* @todo
|
||||
*/
|
||||
async analyticsEnabledChanged(): Promise<void> {
|
||||
// await this.pushNotificationsProvider.enableAnalytics(this.analyticsEnabled);
|
||||
|
||||
CoreConfig.instance.set(CoreConstants.SETTINGS_ANALYTICS_ENABLED, this.analyticsEnabled ? 1 : 0);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
:host {
|
||||
.core-settings-general-font-size ion-segment {
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
|
||||
<ion-title>{{ 'core.settings.opensourcelicenses' | translate }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-item button *ngIf="error" class="ion-text-wrap" [href]="licensesUrl" core-link auto-login="no">
|
||||
<ion-label>
|
||||
{{ 'core.settings.opensourcelicenses' | translate }}
|
||||
</ion-label>
|
||||
<ion-button [href]="licensesUrl" target="_blank" fill="clear" slot="end" core-link auto-login="no">
|
||||
{{ 'core.view' | translate }}</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="!error">
|
||||
<ion-item *ngFor="let license of licenses" class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>
|
||||
<a *ngIf="license.repository" [href]="license.repository" core-link
|
||||
auto-login="no">{{ license.name }}</a>
|
||||
<ng-container *ngIf="!license.repository">{{ license.name }}</ng-container> - {{ license.version }}
|
||||
</h2>
|
||||
<h3 *ngIf="license.publisher">
|
||||
{{ 'core.settings.publisher' | translate }}{{ 'core.labelsep' | translate }} {{ license.publisher }}
|
||||
</h3>
|
||||
<p>{{ 'core.settings.license' | translate }}{{ 'core.labelsep' | translate }} {{ license.licenses }}</p>
|
||||
<p><a *ngIf="license.url" [href]="license.url" core-link auto-login="no">{{ license.url }}</a></p>
|
||||
<p><a *ngIf="license.email" [href]="'mailto:' + license.email" core-link
|
||||
auto-login="no">{{ license.email }}</a></p>
|
||||
</ion-label>
|
||||
<ion-button *ngIf="license.licenseUrl" [href]="license.licenseUrl" target="_blank"
|
||||
fill="clear" slot="end" core-link auto-login="no">{{ 'core.view' | translate }}</ion-button>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,48 @@
|
|||
// (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 { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
|
||||
import { CoreSettingsLicensesPage } from './licenses.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreSettingsLicensesPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreSettingsLicensesPage,
|
||||
],
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CorePipesModule,
|
||||
],
|
||||
})
|
||||
export class CoreSettingsLicensesPageModule {}
|
|
@ -0,0 +1,88 @@
|
|||
// (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, OnInit } from '@angular/core';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
import { Http } from '@/app/singletons/core.singletons';
|
||||
|
||||
/**
|
||||
* Defines license info
|
||||
*/
|
||||
interface CoreSettingsLicense {
|
||||
name: string;
|
||||
version: string;
|
||||
licenses: string;
|
||||
repository?: string;
|
||||
publisher?: string;
|
||||
url?: string;
|
||||
email?: string;
|
||||
licenseUrl?: string;
|
||||
licenseFile?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page that displays the open source licenses information.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-settings-licenses',
|
||||
templateUrl: 'licenses.html',
|
||||
})
|
||||
export class CoreSettingsLicensesPage implements OnInit {
|
||||
|
||||
licensesUrl: string;
|
||||
loaded = false;
|
||||
licenses: CoreSettingsLicense[] = [];
|
||||
error = false;
|
||||
|
||||
constructor() {
|
||||
let version = 'v' + CoreConstants.CONFIG.versionname;
|
||||
if (version.indexOf('-') > 0) {
|
||||
version = 'integration';
|
||||
}
|
||||
|
||||
this.licensesUrl = 'https://raw.githubusercontent.com/moodlehq/moodleapp/' + version + '/licenses.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
const licenses = await Http.instance.get(this.licensesUrl).toPromise();
|
||||
this.licenses = Object.keys(licenses).map((name) => {
|
||||
const license = licenses[name];
|
||||
|
||||
const nameSplit = name.lastIndexOf('@');
|
||||
license.name = name.substring(0, nameSplit);
|
||||
license.version = name.substring(nameSplit + 1);
|
||||
|
||||
if (license.repository) {
|
||||
license.repository = license.repository.replace('git://', 'https://');
|
||||
if (license.repository.indexOf('github.com') > 0) {
|
||||
license.licenseUrl = license.repository + '/blob/' + license.version + '/' + license.licenseFile;
|
||||
}
|
||||
}
|
||||
|
||||
return license;
|
||||
});
|
||||
|
||||
this.error = false;
|
||||
} catch {
|
||||
this.error = true;
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,431 @@
|
|||
// (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 { CoreApp } from '@services/app';
|
||||
import { CoreCron } from '@services/cron';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
import { CoreConfig } from '@services/config';
|
||||
// import { CoreFilterProvider } from '@core/filter/providers/filter';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
// import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { makeSingleton, Translate } from '@singletons/core.singletons';
|
||||
|
||||
/**
|
||||
* Object with space usage and cache entries that can be erased.
|
||||
*/
|
||||
export interface CoreSiteSpaceUsage {
|
||||
cacheEntries?: number; // Number of cached entries that can be cleared.
|
||||
spaceUsage?: number; // Space used in this site (total files + estimate of cache).
|
||||
}
|
||||
|
||||
/**
|
||||
* Constants to define color schemes.
|
||||
*/
|
||||
export const enum CoreColorScheme {
|
||||
AUTO = 'auto',
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark',
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings helper service.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoreSettingsHelperProvider {
|
||||
|
||||
protected syncPromises: { [s: string]: Promise<void> } = {};
|
||||
protected prefersDark?: MediaQueryList;
|
||||
|
||||
constructor() {
|
||||
// protected filterProvider: CoreFilterProvider,
|
||||
// protected courseProvider: CoreCourseProvider,
|
||||
|
||||
if (!CoreConstants.CONFIG.forceColorScheme) {
|
||||
// Update color scheme when a user enters or leaves a site, or when the site info is updated.
|
||||
const applySiteScheme = (): void => {
|
||||
if (this.isColorSchemeDisabledInSite()) {
|
||||
// Dark mode is disabled, force light mode.
|
||||
this.setColorScheme(CoreColorScheme.LIGHT);
|
||||
} else {
|
||||
this.prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
// Reset color scheme settings.
|
||||
this.initColorScheme();
|
||||
}
|
||||
};
|
||||
|
||||
CoreEvents.on(CoreEvents.LOGIN, applySiteScheme.bind(this));
|
||||
|
||||
CoreEvents.on(CoreEvents.SITE_UPDATED, applySiteScheme.bind(this));
|
||||
|
||||
CoreEvents.on(CoreEvents.LOGOUT, () => {
|
||||
// Reset color scheme settings.
|
||||
this.initColorScheme();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes files of a site and the tables that can be cleared.
|
||||
*
|
||||
* @param siteName Site Name.
|
||||
* @param siteId: Site ID.
|
||||
* @return Resolved with detailed new info when done.
|
||||
* @todo filterProvider and courseProviderpart.
|
||||
*/
|
||||
async deleteSiteStorage(siteName: string, siteId: string): Promise<CoreSiteSpaceUsage> {
|
||||
const siteInfo: CoreSiteSpaceUsage = {
|
||||
cacheEntries: 0,
|
||||
spaceUsage: 0,
|
||||
};
|
||||
|
||||
// siteName = await this.filterProvider.formatText(siteName, { clean: true, singleLine: true, filter: false }, [], siteId);
|
||||
|
||||
const title = Translate.instance.instant('core.settings.deletesitefilestitle');
|
||||
const message = Translate.instance.instant('core.settings.deletesitefiles', { sitename: siteName });
|
||||
|
||||
await CoreDomUtils.instance.showConfirm(message, title);
|
||||
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
// Clear cache tables.
|
||||
const cleanSchemas = CoreSites.instance.getSiteTableSchemasToClear(site);
|
||||
const promises: Promise<number | void>[] = cleanSchemas.map((name) => site.getDb().deleteRecords(name));
|
||||
const filepoolService = CoreFilepool.instance;
|
||||
|
||||
|
||||
promises.push(site.deleteFolder().then(() => {
|
||||
filepoolService.clearAllPackagesStatus(siteId);
|
||||
filepoolService.clearFilepool(siteId);
|
||||
// this.courseProvider.clearAllCoursesStatus(siteId);
|
||||
|
||||
siteInfo.spaceUsage = 0;
|
||||
|
||||
return;
|
||||
}).catch(async (error) => {
|
||||
if (error && error.code === FileError.NOT_FOUND_ERR) {
|
||||
// Not found, set size 0.
|
||||
filepoolService.clearAllPackagesStatus(siteId);
|
||||
siteInfo.spaceUsage = 0;
|
||||
} else {
|
||||
// Error, recalculate the site usage.
|
||||
CoreDomUtils.instance.showErrorModal('core.settings.errordeletesitefiles', true);
|
||||
|
||||
siteInfo.spaceUsage = await site.getSpaceUsage();
|
||||
}
|
||||
}).then(async () => {
|
||||
CoreEvents.trigger(CoreEvents.SITE_STORAGE_DELETED, {}, siteId);
|
||||
|
||||
siteInfo.cacheEntries = await this.calcSiteClearRows(site);
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return siteInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates each site's usage, and the total usage.
|
||||
*
|
||||
* @param siteId ID of the site. Current site if undefined.
|
||||
* @return Resolved with detailed info when done.
|
||||
*/
|
||||
async getSiteSpaceUsage(siteId?: string): Promise<CoreSiteSpaceUsage> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
// Get space usage.
|
||||
const siteInfo: CoreSiteSpaceUsage = {
|
||||
cacheEntries: 0,
|
||||
spaceUsage: 0,
|
||||
};
|
||||
|
||||
siteInfo.cacheEntries = await this.calcSiteClearRows(site);
|
||||
siteInfo.spaceUsage = await site.getTotalUsage();
|
||||
|
||||
return siteInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the number of rows to be deleted on a site.
|
||||
*
|
||||
* @param site Site object.
|
||||
* @return If there are rows to delete or not.
|
||||
*/
|
||||
protected async calcSiteClearRows(site: CoreSite): Promise<number> {
|
||||
const clearTables = CoreSites.instance.getSiteTableSchemasToClear(site);
|
||||
|
||||
let totalEntries = 0;
|
||||
|
||||
await Promise.all(clearTables.map(async (name) =>
|
||||
totalEntries = await site.getDb().countRecords(name) + totalEntries));
|
||||
|
||||
return totalEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a certain processor from a list of processors.
|
||||
*
|
||||
* @param processors List of processors.
|
||||
* @param name Name of the processor to get.
|
||||
* @param fallback True to return first processor if not found, false to not return any. Defaults to true.
|
||||
* @return Processor.
|
||||
* @todo
|
||||
*/
|
||||
getProcessor(processors: any[], name: string, fallback: boolean = true): any {
|
||||
if (!processors || !processors.length) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < processors.length; i++) {
|
||||
if (processors[i].name == name) {
|
||||
return processors[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Processor not found, return first if requested.
|
||||
if (fallback) {
|
||||
return processors[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the components and notifications that have a certain processor.
|
||||
*
|
||||
* @param processor Name of the processor to filter.
|
||||
* @param components Array of components.
|
||||
* @return Filtered components.
|
||||
* @todo
|
||||
*/
|
||||
getProcessorComponents(processor: string, components: any[]): any[] {
|
||||
return processor? components : [];
|
||||
/*
|
||||
const result = [];
|
||||
|
||||
components.forEach((component) => {
|
||||
// Create a copy of the component with an empty list of notifications.
|
||||
const componentCopy = CoreUtils.instance.clone(component);
|
||||
componentCopy.notifications = [];
|
||||
|
||||
component.notifications.forEach((notification) => {
|
||||
let hasProcessor = false;
|
||||
for (let i = 0; i < notification.processors.length; i++) {
|
||||
const proc = notification.processors[i];
|
||||
if (proc.name == processor) {
|
||||
hasProcessor = true;
|
||||
notification.currentProcessor = proc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasProcessor) {
|
||||
// Add the notification.
|
||||
componentCopy.notifications.push(notification);
|
||||
}
|
||||
});
|
||||
|
||||
if (componentCopy.notifications.length) {
|
||||
// At least 1 notification added, add the component to the result.
|
||||
result.push(componentCopy);
|
||||
}
|
||||
});
|
||||
|
||||
return result;*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the synchronization promise of a site.
|
||||
*
|
||||
* @param siteId ID of the site.
|
||||
* @return Sync promise or null if site is not being syncrhonized.
|
||||
*/
|
||||
async getSiteSyncPromise(siteId: string): Promise<void> {
|
||||
if (this.syncPromises[siteId]) {
|
||||
return this.syncPromises[siteId];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize a site.
|
||||
*
|
||||
* @param syncOnlyOnWifi True to sync only on wifi, false otherwise.
|
||||
* @param siteId ID of the site to synchronize.
|
||||
* @return Promise resolved when synchronized, rejected if failure.
|
||||
*/
|
||||
async synchronizeSite(syncOnlyOnWifi: boolean, siteId: string): Promise<void> {
|
||||
if (this.syncPromises[siteId]) {
|
||||
// There's already a sync ongoing for this site, return the promise.
|
||||
return this.syncPromises[siteId];
|
||||
}
|
||||
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const hasSyncHandlers = CoreCron.instance.hasManualSyncHandlers();
|
||||
|
||||
if (site.isLoggedOut()) {
|
||||
// Cannot sync logged out sites.
|
||||
throw Translate.instance.instant('core.settings.cannotsyncloggedout');
|
||||
} else if (hasSyncHandlers && !CoreApp.instance.isOnline()) {
|
||||
// We need connection to execute sync.
|
||||
throw Translate.instance.instant('core.settings.cannotsyncoffline');
|
||||
} else if (hasSyncHandlers && syncOnlyOnWifi && CoreApp.instance.isNetworkAccessLimited()) {
|
||||
throw Translate.instance.instant('core.settings.cannotsyncwithoutwifi');
|
||||
}
|
||||
|
||||
const syncPromise = Promise.all([
|
||||
// Invalidate all the site files so they are re-downloaded.
|
||||
CoreUtils.instance.ignoreErrors(CoreFilepool.instance.invalidateAllFiles(siteId)),
|
||||
// Invalidate and synchronize site data.
|
||||
site.invalidateWsCache(),
|
||||
this.checkSiteLocalMobile(site),
|
||||
CoreSites.instance.updateSiteInfo(site.getId()),
|
||||
CoreCron.instance.forceSyncExecution(site.getId()),
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
]).then(() => {
|
||||
return;
|
||||
});
|
||||
|
||||
this.syncPromises[siteId] = syncPromise;
|
||||
|
||||
try {
|
||||
await syncPromise;
|
||||
} finally {
|
||||
delete this.syncPromises[siteId];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if local_mobile was added to the site.
|
||||
*
|
||||
* @param site Site to check.
|
||||
* @return Promise resolved if no action needed.
|
||||
*/
|
||||
protected async checkSiteLocalMobile(site: CoreSite): Promise<void> {
|
||||
try {
|
||||
// Check if local_mobile was installed in Moodle.
|
||||
await site.checkIfLocalMobileInstalledAndNotUsed();
|
||||
} catch {
|
||||
// Not added, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// Local mobile was added. Throw invalid session to force reconnect and create a new token.
|
||||
CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {}, site.getId());
|
||||
|
||||
throw Translate.instance.instant('core.lostconnection');
|
||||
}
|
||||
|
||||
/**
|
||||
* Init Settings related to DOM.
|
||||
*/
|
||||
async initDomSettings(): Promise<void> {
|
||||
// Set the font size based on user preference.
|
||||
const fontSize = await CoreConfig.instance.get(CoreConstants.SETTINGS_FONT_SIZE, CoreConstants.CONFIG.font_sizes[0]);
|
||||
this.setFontSize(fontSize);
|
||||
|
||||
this.initColorScheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the color scheme.
|
||||
*/
|
||||
async initColorScheme(): Promise<void> {
|
||||
if (CoreConstants.CONFIG.forceColorScheme) {
|
||||
this.setColorScheme(CoreConstants.CONFIG.forceColorScheme);
|
||||
} else {
|
||||
const scheme = await CoreConfig.instance.get(CoreConstants.SETTINGS_COLOR_SCHEME, CoreColorScheme.LIGHT);
|
||||
this.setColorScheme(scheme);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if color scheme is disabled in a site.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with whether color scheme is disabled.
|
||||
*/
|
||||
async isColorSchemeDisabled(siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
return this.isColorSchemeDisabledInSite(site);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if color scheme is disabled in a site.
|
||||
*
|
||||
* @param site Site instance. If not defined, current site.
|
||||
* @return Whether color scheme is disabled.
|
||||
*/
|
||||
isColorSchemeDisabledInSite(site?: CoreSite): boolean {
|
||||
site = site || CoreSites.instance.getCurrentSite();
|
||||
|
||||
return site ? site.isFeatureDisabled('NoDelegate_DarkMode') : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set document default font size.
|
||||
*
|
||||
* @param fontSize Font size in percentage.
|
||||
*/
|
||||
setFontSize(fontSize: number): void {
|
||||
// @todo Since zoom is deprecated and fontSize is not working, we should do some research here.
|
||||
document.documentElement.style.zoom = fontSize + '%';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set body color scheme.
|
||||
*
|
||||
* @param colorScheme Name of the color scheme.
|
||||
*/
|
||||
setColorScheme(colorScheme: CoreColorScheme): void {
|
||||
if (colorScheme == CoreColorScheme.AUTO && this.prefersDark) {
|
||||
// Listen for changes to the prefers-color-scheme media query.
|
||||
this.prefersDark.addEventListener('change', this.toggleDarkModeListener);
|
||||
|
||||
this.toggleDarkMode(this.prefersDark.matches);
|
||||
} else {
|
||||
// Stop listening to changes.
|
||||
this.prefersDark?.removeEventListener('change', this.toggleDarkModeListener);
|
||||
|
||||
this.toggleDarkMode(colorScheme == CoreColorScheme.DARK);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener function to toggle dark mode.
|
||||
*
|
||||
* @param e Event object.
|
||||
*/
|
||||
protected toggleDarkModeListener = (e: MediaQueryListEvent): void => {
|
||||
document.body.classList.toggle('dark', e.matches);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles dark mode based on enabled boolean.
|
||||
*
|
||||
* @param enable True to enable dark mode, false to disable.
|
||||
*/
|
||||
protected toggleDarkMode(enable: boolean = false): void {
|
||||
document.body.classList.toggle('dark', enable);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CoreSettingsHelper extends makeSingleton(CoreSettingsHelperProvider) {}
|
|
@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
|
|||
import { Routes } from '@angular/router';
|
||||
|
||||
import { CoreMainMenuRoutingModule } from '@core/mainmenu/mainmenu-routing.module';
|
||||
import { CoreSettingsHelperProvider } from './services/settings.helper';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -25,7 +26,21 @@ const routes: Routes = [
|
|||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [CoreMainMenuRoutingModule.forChild(routes)],
|
||||
exports: [CoreMainMenuRoutingModule],
|
||||
imports: [
|
||||
CoreMainMenuRoutingModule.forChild(routes),
|
||||
],
|
||||
exports: [
|
||||
CoreMainMenuRoutingModule,
|
||||
],
|
||||
providers: [
|
||||
CoreSettingsHelperProvider,
|
||||
],
|
||||
})
|
||||
export class CoreSettingsInitModule {}
|
||||
export class CoreSettingsInitModule {
|
||||
|
||||
constructor(settingsHelper: CoreSettingsHelperProvider) {
|
||||
// @todo
|
||||
// settingsHelper.initDomSettings();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,6 +20,14 @@ const routes: Routes = [
|
|||
path: 'about',
|
||||
loadChildren: () => import('./pages/about/about.page.module').then( m => m.CoreSettingsAboutPageModule),
|
||||
},
|
||||
{
|
||||
path: 'general',
|
||||
loadChildren: () => import('./pages/general/general.page.module').then( m => m.CoreSettingsGeneralPageModule),
|
||||
},
|
||||
{
|
||||
path: 'licenses',
|
||||
loadChildren: () => import('./pages/licenses/licenses.page.module').then( m => m.CoreSettingsLicensesPageModule),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./pages/app/app.page.module').then( m => m.CoreSettingsAppPageModule),
|
||||
|
|
|
@ -44,7 +44,7 @@ export class CoreFaIconDirective implements OnChanges {
|
|||
/**
|
||||
* Detect icon name and use svg.
|
||||
*/
|
||||
setIcon(): void {
|
||||
async setIcon(): Promise<void> {
|
||||
let library = 'ionic';
|
||||
let iconName = this.name;
|
||||
const parts = iconName.split('-', 2);
|
||||
|
@ -73,13 +73,11 @@ export class CoreFaIconDirective implements OnChanges {
|
|||
this.element.setAttribute('src', src);
|
||||
|
||||
if (CoreConstants.BUILD.isDevelopment || CoreConstants.BUILD.isTesting) {
|
||||
Http.instance.get(src).subscribe(() => {
|
||||
// Ignore.
|
||||
}, (error) => {
|
||||
if (error.status != 200) {
|
||||
this.logger.error(`Icon ${this.name} not found`);
|
||||
}
|
||||
});
|
||||
try {
|
||||
await Http.instance.get(src, { responseType: 'text' }).toPromise();
|
||||
} catch (error) {
|
||||
this.logger.error(`Icon ${this.name} not found`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.element.removeAttribute('src');
|
||||
|
|
|
@ -101,6 +101,7 @@ export class CoreAppProvider {
|
|||
});
|
||||
});
|
||||
|
||||
// @todo
|
||||
// this.platform.registerBackButtonAction(() => {
|
||||
// this.backButtonAction();
|
||||
// }, 100);
|
||||
|
@ -157,7 +158,7 @@ export class CoreAppProvider {
|
|||
async createTablesFromSchema(schema: CoreAppSchema): Promise<void> {
|
||||
this.logger.debug(`Apply schema to app DB: ${schema.name}`);
|
||||
|
||||
let oldVersion;
|
||||
let oldVersion: number;
|
||||
|
||||
try {
|
||||
// Wait for the schema versions table to be created.
|
||||
|
|
|
@ -753,12 +753,12 @@ export class CoreDomUtilsProvider {
|
|||
* @param findFunction The function used to find the element.
|
||||
* @return Resolved if found, rejected if too many tries.
|
||||
*/
|
||||
waitElementToExist(findFunction: () => HTMLElement): Promise<HTMLElement> {
|
||||
waitElementToExist(findFunction: () => HTMLElement | null): Promise<HTMLElement> {
|
||||
const promiseInterval = CoreUtils.instance.promiseDefer<HTMLElement>();
|
||||
let tries = 100;
|
||||
|
||||
const clear = setInterval(() => {
|
||||
const element: HTMLElement = findFunction();
|
||||
const element: HTMLElement | null = findFunction();
|
||||
|
||||
if (element) {
|
||||
clearInterval(clear);
|
||||
|
@ -834,17 +834,14 @@ export class CoreDomUtilsProvider {
|
|||
* @return Promise resolved with boolean: true if enabled, false otherwise.
|
||||
*/
|
||||
isRichTextEditorEnabled(): Promise<boolean> {
|
||||
if (this.isRichTextEditorSupported()) {
|
||||
return CoreConfig.instance.get(CoreConstants.SETTINGS_RICH_TEXT_EDITOR, true).then((enabled) => !!enabled);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
return CoreConfig.instance.get(CoreConstants.SETTINGS_RICH_TEXT_EDITOR, true).then((enabled) => !!enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rich text editor is supported in the platform.
|
||||
*
|
||||
* @return Whether it's supported.
|
||||
* @deprecated since 3.9.5
|
||||
*/
|
||||
isRichTextEditorSupported(): boolean {
|
||||
return true;
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
// Correctly inherit ion-text-wrap onto labels.
|
||||
ion-item.ion-text-wrap ion-label {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
|
||||
// Ionic toolbar.
|
||||
ion-toolbar ion-back-button,
|
||||
|
@ -99,3 +104,44 @@ ion-list.list-md {
|
|||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// Avatar
|
||||
// -------------------------
|
||||
// Large centered avatar
|
||||
img.large-avatar {
|
||||
display: block;
|
||||
margin: auto;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
max-width: 90px;
|
||||
max-height: 90px;
|
||||
margin-bottom: 10px;
|
||||
border-radius : 50%;
|
||||
padding: 4px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
ion-avatar ion-img, ion-avatar img {
|
||||
text-indent: -99999px;
|
||||
background-color: --var(--gray-light);
|
||||
}
|
||||
|
||||
// Action sheet.
|
||||
.md ion-action-sheet {
|
||||
.action-sheet-group-cancel {
|
||||
-webkit-filter: drop-shadow(0px 3px 3px rgba(var(--action-sheet-shadow-color)));
|
||||
filter: drop-shadow(0px 3px 3px rgba(var(--action-sheet-shadow-color)));
|
||||
}
|
||||
|
||||
.action-sheet-title {
|
||||
border-bottom: 1px solid var(--title-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.ios ion-action-sheet {
|
||||
.action-sheet-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
--yellow: var(--custom-yellow, #fbad1a); // Accent (never text).
|
||||
--purple: var(--custom-purple, #8e24aa); // Accent (never text).
|
||||
|
||||
--core-color: var(--custom-main-color, #f98012);
|
||||
--core-color: var(--custom-main-color, var(--orange));
|
||||
|
||||
--ion-color-primary: var(--core-color);
|
||||
--ion-color-primary-rgb: 249,128,18;
|
||||
|
@ -92,21 +92,49 @@
|
|||
--ion-text-color-rgb: 58,58,58;
|
||||
|
||||
ion-content {
|
||||
--background: #e9e9e9;
|
||||
--background: var(--gray-light);
|
||||
}
|
||||
|
||||
ion-tab-bar {
|
||||
--background: #626262;
|
||||
--color: #ffffff;
|
||||
--background: var(--custom-bottom-tabs-background, var(--gray-darker));
|
||||
--color: var(--custom-bottom-tabs-color, var(--white));
|
||||
}
|
||||
|
||||
ion-toolbar {
|
||||
--color: var(--ion-color-primary-contrast);
|
||||
--background: var(--ion-color-primary);
|
||||
--color: var(--custom-toolbar-color, var(--ion-color-primary-contrast));
|
||||
--background: var(--custom-toolbar-background, var(--ion-color-primary));
|
||||
}
|
||||
|
||||
--core-login-background: var(--custom-login-background, white);
|
||||
--core-login-text-color: var(--custom-login-text-color, #3a3a3a);
|
||||
ion-action-sheet {
|
||||
--action-sheet-shadow-color: var(--custom--action-sheet-shadow-color, 0, 0, 0, 1);
|
||||
--button-color-selected: var(--custom--action-sheet-selected-color, var(--core-color));
|
||||
--title-border-color: var(--custom-title-border-color, var(--gray));
|
||||
|
||||
@media (min-height: 500px) {
|
||||
--max-height: 50%;
|
||||
--height: 100%;
|
||||
}
|
||||
|
||||
.action-sheet-cancel {
|
||||
--button-color: var(--ion-color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
core-tabs {
|
||||
--background: var(--custom-tabs-background, var(--white));
|
||||
ion-slide {
|
||||
--background: var(--custom-tab-background, var(--white));
|
||||
--color: var(--custom-tab-background, var(--gray-dark));
|
||||
--border-color: var(--custom-tab-border-color, var(--gray));
|
||||
--color-active: var(--custom-tab-color-active, var(--core-color));
|
||||
--border-color-active: var(--custom-tab-border-color-active, var(--color-active));
|
||||
}
|
||||
}
|
||||
|
||||
--drop-shadow: var(--custom-drop-shadow, 0, 0, 0, 0.2);
|
||||
|
||||
--core-login-background: var(--custom-login-background, var(--white));
|
||||
--core-login-text-color: var(--custom-login-text-color, var(--black));
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -114,7 +142,7 @@
|
|||
* -------------------------------------------
|
||||
*/
|
||||
:root body.dark {
|
||||
--ion-background-color: #3a3a3a;
|
||||
--ion-background-color: #1e1e1e;
|
||||
--ion-background-color-rgb: 18,18,18;
|
||||
|
||||
--ion-text-color: #ffffff;
|
||||
|
@ -144,7 +172,6 @@
|
|||
|
||||
--ion-tab-bar-background: #1f1f1f;
|
||||
|
||||
|
||||
--ion-item-background: #1e1e1e;
|
||||
|
||||
--ion-card-background: #1c1c1d;
|
||||
|
@ -153,6 +180,15 @@
|
|||
--background: var(--ion-background-color);
|
||||
}
|
||||
|
||||
core-tabs {
|
||||
--background: var(--custom-tabs-background, #3a3a3a);
|
||||
ion-slide {
|
||||
--background: var(--custom-tab-background, #3a3a3a);
|
||||
--color: var(--custom-tab-background, var(--white));
|
||||
--border-color: var(--custom-tab-border-color, var(--gray-light));
|
||||
}
|
||||
}
|
||||
|
||||
--core-login-background: var(--custom-login-background, #3a3a3a);
|
||||
--core-login-text-color: var(--custom-login-text-color, white);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import { CoreColorScheme } from '@core/settings/services/settings.helper';
|
||||
import { CoreSitesDemoSiteData } from '@services/sites';
|
||||
|
||||
declare global {
|
||||
|
@ -59,7 +60,7 @@ declare global {
|
|||
statusbarlighttextremotetheme: boolean;
|
||||
enableanalytics: boolean;
|
||||
enableonboarding: boolean;
|
||||
forceColorScheme: string;
|
||||
forceColorScheme: CoreColorScheme;
|
||||
forceLoginLogo: boolean;
|
||||
ioswebviewscheme: string;
|
||||
appstores: Record<string, string>;
|
||||
|
|
Loading…
Reference in New Issue