Merge pull request #1727 from moodlehq/integration

MOBILE-2795 release: Merge integration into master
main
Juan Leyva 2019-01-11 13:20:29 +01:00 committed by GitHub
commit caa3d11700
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2497 changed files with 80902 additions and 55712 deletions

3
.gitignore vendored
View File

@ -22,12 +22,12 @@ dist/
node_modules/ node_modules/
tmp/ tmp/
temp/ temp/
hooks/
platforms/ platforms/
/plugins/ /plugins/
/plugins/android.json /plugins/android.json
/plugins/ios.json /plugins/ios.json
www/ www/
!www/README.md
$RECYCLE.BIN/ $RECYCLE.BIN/
.DS_Store .DS_Store
@ -39,4 +39,3 @@ e2e/build
!/desktop/assets/ !/desktop/assets/
!/desktop/electron.js !/desktop/electron.js
src/configconstants.ts src/configconstants.ts
src/assets/lang

View File

@ -13,4 +13,8 @@ before_script:
- rm -Rf node_modules/electron-builder-squirrel-windows node_modules/electron-windows-notifications #Avoid electron fail - rm -Rf node_modules/electron-builder-squirrel-windows node_modules/electron-windows-notifications #Avoid electron fail
script: script:
- npm run build - npm run build
after_success:
- chmod +x scripts/aot.sh
- scripts/aot.sh

View File

@ -13,3 +13,10 @@ License
------- -------
[Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
Big Thanks
-----------
Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com)
![Sauce Labs Logo](https://user-images.githubusercontent.com/557037/43443976-d88d5a78-94a2-11e8-8915-9f06521423dd.png)

View File

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<widget id="com.moodle.moodlemobile" version="3.5.2" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"> <widget id="com.moodle.moodlemobile" version="3.6.0" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>Moodle</name> <name>Moodle</name>
<description>Moodle official app</description> <description>Moodle official app</description>
<author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author> <author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author>
@ -94,42 +94,39 @@
<icon height="1024" src="resources/ios/icon/icon-1024.png" width="1024" /> <icon height="1024" src="resources/ios/icon/icon-1024.png" width="1024" />
<splash height="2732" src="resources/ios/splash/Default@2x~universal~anyany.png" width="2732" /> <splash height="2732" src="resources/ios/splash/Default@2x~universal~anyany.png" width="2732" />
</platform> </platform>
<plugin name="cordova-plugin-file" spec="^6.0.1" /> <plugin name="com-darryncampbell-cordova-plugin-intent" spec="1.1.1" />
<plugin name="cordova-plugin-file-transfer" spec="^1.7.1" /> <plugin name="cordova-android-support-gradle-release" spec="2.0.1">
<plugin name="cordova-plugin-camera" spec="^4.0.3"> <variable name="ANDROID_SUPPORT_VERSION" value="27.1.0" />
<variable name="CAMERA_USAGE_DESCRIPTION" value="We need camera access to take pictures so you can attach them as part of your submissions." />
<variable name="PHOTOLIBRARY_USAGE_DESCRIPTION" value="We need photo library access to get pictures from there so you can attach them as part of your submissions." />
</plugin> </plugin>
<plugin name="cordova-plugin-media-capture" spec="^3.0.2"> <plugin name="cordova-clipboard" spec="1.2.1" />
<variable name="CAMERA_USAGE_DESCRIPTION" value="We need camera access to take pictures so you can attach them as part of your submissions." /> <plugin name="cordova-plugin-badge" spec="0.8.8" />
<variable name="PHOTOLIBRARY_USAGE_DESCRIPTION" value="We need photo library access to get pictures from there so you can attach them as part of your submissions." /> <plugin name="cordova-plugin-camera" spec="4.0.3" />
<variable name="MICROPHONE_USAGE_DESCRIPTION" value="We need microphone access to record sounds so you can attach them as part of your submissions." /> <plugin name="cordova-plugin-customurlscheme" spec="4.3.0">
<variable name="URL_SCHEME" value="moodlemobile" />
</plugin> </plugin>
<plugin name="cordova-plugin-device" spec="^2.0.2" /> <plugin name="cordova-plugin-device" spec="2.0.2" />
<plugin name="cordova-plugin-globalization" spec="^1.11.0" /> <plugin name="cordova-plugin-file" spec="6.0.1" />
<plugin name="cordova-plugin-inappbrowser" spec="^3.0.0" /> <plugin name="cordova-plugin-file-opener2" spec="2.0.19" />
<plugin name="cordova-plugin-network-information" spec="^2.0.1" /> <plugin name="cordova-plugin-file-transfer" spec="1.7.1" />
<plugin name="cordova-plugin-statusbar" spec="^2.4.2" /> <plugin name="cordova-plugin-globalization" spec="1.11.0" />
<plugin name="cordova-plugin-whitelist" spec="^1.3.3" /> <plugin name="cordova-plugin-inappbrowser" spec="3.0.0" />
<plugin name="cordova-plugin-splashscreen" spec="^5.0.2" /> <plugin name="cordova-plugin-ionic-keyboard" spec="2.1.3" />
<plugin name="cordova-clipboard" spec="^1.2.1" /> <plugin name="cordova-plugin-local-notifications-mm" spec="1.0.13" />
<plugin name="nl.kingsquare.cordova.background-audio" spec="^1.0.1" /> <plugin name="cordova-plugin-media-capture" spec="3.0.2" />
<plugin name="cordova-plugin-network-information" spec="2.0.1" />
<plugin name="cordova-plugin-screen-orientation" spec="3.0.1" />
<plugin name="cordova-plugin-splashscreen" spec="5.0.2" />
<plugin name="cordova-plugin-statusbar" spec="2.4.2" />
<plugin name="cordova-plugin-whitelist" spec="1.3.3" />
<plugin name="cordova-plugin-zip" spec="3.1.0" />
<plugin name="cordova-sqlite-storage" spec="2.6.0" />
<plugin name="nl.kingsquare.cordova.background-audio" spec="1.0.1" />
<plugin name="phonegap-plugin-push" spec="https://github.com/moodlemobile/phonegap-plugin-push.git#moodle"> <plugin name="phonegap-plugin-push" spec="https://github.com/moodlemobile/phonegap-plugin-push.git#moodle">
<variable name="SENDER_ID" value="694767596569" /> <variable name="SENDER_ID" value="694767596569" />
</plugin> </plugin>
<plugin name="cordova-plugin-customurlscheme" spec="^4.3.0"> <edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application/activity[@android:name='MainActivity']">
<variable name="URL_SCHEME" value="moodlemobile" /> <activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|screenLayout|smallestScreenSize" android:debuggable="true" />
</plugin> </edit-config>
<plugin name="ionic-plugin-keyboard" spec="^2.2.1" /> <engine name="android" spec="7.1.2" />
<plugin name="cordova-plugin-zip" spec="^3.1.0" /> <engine name="ios" spec="4.5.5" />
<plugin name="cordova-plugin-local-notifications-mm" spec="^1.0.13" />
<plugin name="cordova-plugin-file-opener2" spec="^2.0.19" />
<plugin name="com-darryncampbell-cordova-plugin-intent" spec="^1.1.0" />
<plugin name="cordova-sqlite-evcore-extbuild-free" spec="^0.9.7" />
<plugin name="cordova-plugin-badge" spec="^0.8.7" />
<plugin name="cordova-android-support-gradle-release" spec="^1.4.4">
<variable name="ANDROID_SUPPORT_VERSION" value="27.1.0" />
</plugin>
<engine name="android" spec="7.0.0" />
<engine name="ios" spec="4.5.4" />
</widget> </widget>

View File

@ -14,5 +14,7 @@
<true/> <true/>
<key>com.apple.security.files.user-selected.read-only</key> <key>com.apple.security.files.user-selected.read-only</key>
<true/> <true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@ -6,7 +6,7 @@
<Identity Name="3312ADB7.MoodleDesktop" <Identity Name="3312ADB7.MoodleDesktop"
ProcessorArchitecture="x64" ProcessorArchitecture="x64"
Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6" Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6"
Version="3.5.1.0" /> Version="3.6.0.0" />
<Properties> <Properties>
<DisplayName>Moodle Desktop</DisplayName> <DisplayName>Moodle Desktop</DisplayName>
<PublisherDisplayName>Moodle Pty Ltd.</PublisherDisplayName> <PublisherDisplayName>Moodle Pty Ltd.</PublisherDisplayName>

View File

@ -5,6 +5,7 @@ const path = require('path');
const url = require('url'); const url = require('url');
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
const userAgent = 'MoodleMobile';
// Keep a global reference of the window object, if you don't, the window will // Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected. // be closed automatically when the JavaScript object is garbage collected.
@ -64,6 +65,9 @@ function createWindow() {
mainWindow.on('focus', () => { mainWindow.on('focus', () => {
mainWindow.webContents.send('coreAppFocused'); // Send an event to the main window. mainWindow.webContents.send('coreAppFocused'); // Send an event to the main window.
}); });
// Append some text to the user agent.
mainWindow.webContents.setUserAgent(mainWindow.webContents.getUserAgent() + ' ' + userAgent);
} }
// This method will be called when Electron has finished initialization and is ready to create browser windows. // This method will be called when Electron has finished initialization and is ready to create browser windows.

View File

@ -9,6 +9,7 @@ var gulp = require('gulp'),
flatten = require('gulp-flatten'), flatten = require('gulp-flatten'),
npmPath = require('path'), npmPath = require('path'),
File = gutil.File, File = gutil.File,
exec = require('child_process').exec,
license = '' + license = '' +
'// (C) Copyright 2015 Martin Dougiamas\n' + '// (C) Copyright 2015 Martin Dougiamas\n' +
'//\n' + '//\n' +
@ -24,17 +25,6 @@ var gulp = require('gulp'),
'// See the License for the specific language governing permissions and\n' + '// See the License for the specific language governing permissions and\n' +
'// limitations under the License.\n\n'; '// limitations under the License.\n\n';
// Get the names of the JSON files inside a directory.
function getFilenames(dir) {
if (fs.existsSync(dir)) {
return fs.readdirSync(dir).filter(function(file) {
return file.indexOf('.json') > -1;
});
} else {
return [];
}
}
/** /**
* Copy a property from one object to another, adding a prefix to the key if needed. * Copy a property from one object to another, adding a prefix to the key if needed.
* @param {Object} target Object to copy the properties to. * @param {Object} target Object to copy the properties to.
@ -75,7 +65,8 @@ function treatMergedData(data) {
var mergedOrdered = {}; var mergedOrdered = {};
for (var filepath in data) { for (var filepath in data) {
var pathSplit = filepath.split('/'); var pathSplit = filepath.split('/'),
prefix;
pathSplit.pop(); pathSplit.pop();
@ -120,71 +111,53 @@ function treatMergedData(data) {
} }
/** /**
* Build lang files. * Build lang file.
* *
* @param {String[]} filenames Names of the language files. * @param {String} language Language to translate.
* @param {String[]} langPaths Paths to the possible language files. * @param {String[]} langPaths Paths to the possible language files.
* @param {String} buildDest Path where to leave the built files. * @param {String} buildDest Path where to leave the built files.
* @param {Function} done Function to call when done. * @param {Function} done Function to call when done.
* @return {Void} * @return {Void}
*/ */
function buildLangs(filenames, langPaths, buildDest, done) { function buildLang(language, langPaths, buildDest, done) {
if (!filenames || !filenames.length) { var filename = language + '.json',
// If no filenames supplied, stop. Maybe it's an empty lang folder. data = {},
done(); firstFile = null;
return;
}
var count = 0; var paths = langPaths.map(function(path) {
if (path.slice(-1) != '/') {
function taskFinished() { path = path + '/';
count++;
if (count == filenames.length) {
done();
} }
} return path + language + '.json';
// Now create the build files for each supported language.
filenames.forEach(function(filename) {
var language = filename.replace('.json', ''),
data = {},
firstFile = null;
var paths = langPaths.map(function(path) {
if (path.slice(-1) != '/') {
path = path + '/';
}
return path + language + '.json';
});
gulp.src(paths)
.pipe(slash())
.pipe(clipEmptyFiles())
.pipe(through(function(file) {
if (!firstFile) {
firstFile = file;
}
return treatFile(file, data);
}, function() {
/* This implementation is based on gulp-jsoncombine module.
* https://github.com/reflog/gulp-jsoncombine */
if (firstFile) {
var joinedPath = path.join(firstFile.base, language+'.json');
var joinedFile = new File({
cwd: firstFile.cwd,
base: firstFile.base,
path: joinedPath,
contents: treatMergedData(data)
});
this.emit('data', joinedFile);
}
this.emit('end');
}))
.pipe(gulp.dest(buildDest))
.on('end', taskFinished);
}); });
gulp.src(paths, { allowEmpty: true })
.pipe(slash())
.pipe(clipEmptyFiles())
.pipe(through(function(file) {
if (!firstFile) {
firstFile = file;
}
return treatFile(file, data);
}, function() {
/* This implementation is based on gulp-jsoncombine module.
* https://github.com/reflog/gulp-jsoncombine */
if (firstFile) {
var joinedPath = path.join(firstFile.base, language+'.json');
var joinedFile = new File({
cwd: firstFile.cwd,
base: firstFile.base,
path: joinedPath,
contents: treatMergedData(data)
});
this.emit('data', joinedFile);
}
this.emit('end');
}))
.pipe(gulp.dest(buildDest))
.on('end', done);
} }
// Delete a folder and all its contents. // Delete a folder and all its contents.
@ -204,10 +177,7 @@ function deleteFolderRecursive(path) {
} }
// List of app lang files. To be used only if cannot get it from filesystem. // List of app lang files. To be used only if cannot get it from filesystem.
var appLangFiles = ['ar.json', 'bg.json', 'ca.json', 'cs.json', 'da.json', 'de.json', 'en.json', 'es-mx.json', 'es.json', 'eu.json', var paths = {
'fa.json', 'fr.json', 'he.json', 'hu.json', 'it.json', 'ja.json', 'nl.json', 'pl.json', 'pt-br.json', 'pt.json', 'ro.json',
'ru.json', 'sv.json', 'tr.json', 'zh-cn.json', 'zh-tw.json'],
paths = {
src: './src', src: './src',
assets: './src/assets', assets: './src/assets',
lang: [ lang: [
@ -220,76 +190,84 @@ var appLangFiles = ['ar.json', 'bg.json', 'ca.json', 'cs.json', 'da.json', 'de.j
config: './src/config.json', config: './src/config.json',
}; };
gulp.task('default', ['lang', 'config']);
gulp.task('watch', function() {
var langsPaths = paths.lang.map(function(path) {
return path + '*.json';
});
gulp.watch(langsPaths, { interval: 500 }, ['lang']);
gulp.watch(paths.config, { interval: 500 }, ['config']);
});
// Build the language files into a single file per language. // Build the language files into a single file per language.
gulp.task('lang', function(done) { gulp.task('lang', function(done) {
// Get filenames to know which languages are available. buildLang('en', paths.lang, path.join(paths.assets, 'lang'), done);
var filenames = getFilenames(paths.lang[0]);
buildLangs(filenames, paths.lang, path.join(paths.assets, 'lang'), done);
}); });
// Convert config.json into a TypeScript class. // Convert config.json into a TypeScript class.
gulp.task('config', function(done) { gulp.task('config', function(done) {
gulp.src(paths.config) // Get the last commit.
.pipe(through(function(file) { exec('git log -1 --pretty=format:"%H"', function (err, commit, stderr) {
// Convert the contents of the file into a TypeScript class. if (err) {
// Disable the rule variable-name in the file. console.error('An error occurred while getting the last commit: ' + err);
var config = JSON.parse(file.contents.toString()), } else if (stderr) {
contents = license + '// tslint:disable: variable-name\n' + 'export class CoreConfigConstants {\n'; console.error('An error occurred while getting the last commit: ' + stderr);
}
for (var key in config) { gulp.src(paths.config)
var value = config[key]; .pipe(through(function(file) {
if (typeof value == 'string') { // Convert the contents of the file into a TypeScript class.
// Wrap the string in ' . // Disable the rule variable-name in the file.
value = "'" + value + "'"; var config = JSON.parse(file.contents.toString()),
} else if (typeof value != 'number' && typeof value != 'boolean') { contents = license + '// tslint:disable: variable-name\n' + 'export class CoreConfigConstants {\n',
// Stringify with 4 spaces of indentation, and then add 4 more spaces in each line. that = this;
value = JSON.stringify(value, null, 4).replace(/^(?: )/gm, ' ').replace(/^(?:})/gm, ' }');
// Replace " by ' in values.
value = value.replace(/: "([^"]*)"/g, ": '$1'");
// Check if the keys have "-" in it. for (var key in config) {
var matches = value.match(/"([^"]*\-[^"]*)":/g); var value = config[key];
if (matches) { if (typeof value == 'string') {
// Replace " by ' in keys. We cannot remove them because keys have chars like '-'. // Wrap the string in ' and scape them.
value = value.replace(/"([^"]*)":/g, "'$1':"); value = "'" + value.replace(/([^\\])'/g, "$1\\'") + "'";
} else { } else if (typeof value != 'number' && typeof value != 'boolean') {
// Remove ' in keys. // Stringify with 4 spaces of indentation, and then add 4 more spaces in each line.
value = value.replace(/"([^"]*)":/g, "$1:"); value = JSON.stringify(value, null, 4).replace(/^(?: )/gm, ' ').replace(/^(?:})/gm, ' }');
// Replace " by ' in values.
value = value.replace(/: "([^"]*)"/g, ": '$1'");
// Check if the keys have "-" in it.
var matches = value.match(/"([^"]*\-[^"]*)":/g);
if (matches) {
// Replace " by ' in keys. We cannot remove them because keys have chars like '-'.
value = value.replace(/"([^"]*)":/g, "'$1':");
} else {
// Remove ' in keys.
value = value.replace(/"([^"]*)":/g, "$1:");
}
// Add type any to the key.
key = key + ': any';
} }
// Add type any to the key. // If key has quotation marks, remove them.
key = key + ': any'; if (key[0] == '"') {
key = key.substr(1, key.length - 2);
}
contents += ' static ' + key + ' = ' + value + ';\n';
} }
// If key has quotation marks, remove them. // Add compilation info.
if (key[0] == '"') { contents += ' static compilationtime = ' + Date.now() + ';\n';
key = key.substr(1, key.length - 2); contents += ' static lastcommit = \'' + commit + '\';\n';
}
contents += ' static ' + key + ' = ' + value + ';\n';
}
// Add compilation time. contents += '}\n';
contents += ' static compilationtime = ' + Date.now() + ';\n';
contents += '}\n'; file.contents = new Buffer(contents);
this.emit('data', file);
}))
.pipe(rename('configconstants.ts'))
.pipe(gulp.dest(paths.src))
.on('end', done);
});
});
file.contents = new Buffer(contents); gulp.task('default', gulp.parallel('lang', 'config'));
this.emit('data', file);
})) gulp.task('watch', function() {
.pipe(rename('configconstants.ts')) var langsPaths = paths.lang.map(function(path) {
.pipe(gulp.dest(paths.src)) return path + 'en.json';
.on('end', done); });
gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang'));
gulp.watch(paths.config, { interval: 500 }, gulp.parallel('config'));
}); });
var templatesSrc = [ var templatesSrc = [
@ -306,7 +284,7 @@ var templatesSrc = [
gulp.task('copy-component-templates', function(done) { gulp.task('copy-component-templates', function(done) {
deleteFolderRecursive(templatesDest); deleteFolderRecursive(templatesDest);
gulp.src(templatesSrc) gulp.src(templatesSrc, { allowEmpty: true })
.pipe(flatten()) .pipe(flatten())
.pipe(gulp.dest(templatesDest)) .pipe(gulp.dest(templatesDest))
.on('end', done); .on('end', done);

View File

@ -0,0 +1,49 @@
#!/usr/bin/env node
// This hook copies Android splash screen files from dev directories into the appropriate platform specific location.
// The code was extracted from here: http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/
var filesToCopy = [{
'resources/android/splash/drawable-land-hdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-hdpi/screen.png'
}, {
'resources/android/splash/drawable-land-ldpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-ldpi/screen.png'
}, {
'resources/android/splash/drawable-land-mdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-mdpi/screen.png'
}, {
'resources/android/splash/drawable-land-xhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xhdpi/screen.png'
}, {
'resources/android/splash/drawable-land-xxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xxhdpi/screen.png'
}, {
'resources/android/splash/drawable-land-xxxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-land-xxxhdpi/screen.png'
}, {
'resources/android/splash/drawable-port-hdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-hdpi/screen.png'
}, {
'resources/android/splash/drawable-port-ldpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-ldpi/screen.png'
}, {
'resources/android/splash/drawable-port-mdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-mdpi/screen.png'
}, {
'resources/android/splash/drawable-port-xhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xhdpi/screen.png'
}, {
'resources/android/splash/drawable-port-xxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xxhdpi/screen.png'
}, {
'resources/android/splash/drawable-port-xxxhdpi-screen.png': 'platforms/android/app/src/main/res/drawable-port-xxxhdpi/screen.png'
}
];
var fs = require('fs');
var path = require('path');
// no need to configure below
var rootDir = process.argv[2];
filesToCopy.forEach(function(obj) {
Object.keys(obj).forEach(function(key) {
var val = obj[key];
var srcFile = path.join(rootDir, key);
var destFile = path.join(rootDir, val);
var destDir = path.dirname(destFile);
if (fs.existsSync(srcFile) && fs.existsSync(destDir)) {
fs.createReadStream(srcFile).pipe(fs.createWriteStream(destFile));
}
});
});

View File

@ -1,10 +1,11 @@
{ {
"name": "moodlemobile", "name": "moodlemobile",
"app_id": "com.moodle.moodlemobile",
"type": "ionic-angular",
"integrations": { "integrations": {
"cordova": {}, "cordova": {},
"gulp": {} "gulp": {}
}, },
"watchPatterns": [] "type": "ionic-angular",
"watchPatterns": [],
"pro_id": "com.moodle.moodlemobile",
"id": "com.moodle.moodlemobile"
} }

5795
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "moodlemobile", "name": "moodlemobile",
"version": "3.5.1", "version": "3.6.0",
"description": "The official app for Moodle.", "description": "The official app for Moodle.",
"author": { "author": {
"name": "Moodle Pty Ltd.", "name": "Moodle Pty Ltd.",
@ -48,64 +48,90 @@
"@angular/http": "5.2.10", "@angular/http": "5.2.10",
"@angular/platform-browser": "5.2.10", "@angular/platform-browser": "5.2.10",
"@angular/platform-browser-dynamic": "5.2.10", "@angular/platform-browser-dynamic": "5.2.10",
"@ionic-native/badge": "^4.5.3", "@ionic-native/badge": "4.17.0",
"@ionic-native/camera": "^4.5.2", "@ionic-native/camera": "4.17.0",
"@ionic-native/clipboard": "^4.3.2", "@ionic-native/clipboard": "4.17.0",
"@ionic-native/core": "4.3.0", "@ionic-native/core": "4.11.0",
"@ionic-native/device": "^4.5.3", "@ionic-native/device": "4.17.0",
"@ionic-native/file": "^4.3.3", "@ionic-native/file": "4.17.0",
"@ionic-native/file-opener": "^4.7.0", "@ionic-native/file-opener": "4.17.0",
"@ionic-native/file-transfer": "^4.3.3", "@ionic-native/file-transfer": "4.17.0",
"@ionic-native/globalization": "^4.3.2", "@ionic-native/globalization": "4.17.0",
"@ionic-native/in-app-browser": "^4.3.3", "@ionic-native/in-app-browser": "4.17.0",
"@ionic-native/keyboard": "^4.3.2", "@ionic-native/keyboard": "4.17.0",
"@ionic-native/local-notifications": "4.5.2", "@ionic-native/local-notifications": "4.5.2",
"@ionic-native/media-capture": "^4.5.2", "@ionic-native/media-capture": "4.17.0",
"@ionic-native/network": "^4.3.2", "@ionic-native/network": "4.17.0",
"@ionic-native/push": "^4.5.3", "@ionic-native/push": "4.17.0",
"@ionic-native/splash-screen": "4.3.0", "@ionic-native/screen-orientation": "4.17.0",
"@ionic-native/sqlite": "^4.3.2", "@ionic-native/splash-screen": "4.17.0",
"@ionic-native/status-bar": "4.3.0", "@ionic-native/sqlite": "4.17.0",
"@ionic-native/web-intent": "^4.7.0", "@ionic-native/status-bar": "4.17.0",
"@ionic-native/zip": "^4.3.3", "@ionic-native/web-intent": "4.17.0",
"@ngx-translate/core": "^8.0.0", "@ionic-native/zip": "4.17.0",
"@ngx-translate/http-loader": "^2.0.0", "@ngx-translate/core": "8.0.0",
"@ngx-translate/http-loader": "2.0.1",
"@types/cordova": "0.0.34", "@types/cordova": "0.0.34",
"@types/cordova-plugin-file-transfer": "0.0.3", "@types/cordova-plugin-file-transfer": "0.0.3",
"@types/cordova-plugin-globalization": "0.0.3", "@types/cordova-plugin-globalization": "0.0.3",
"@types/cordova-plugin-network-information": "0.0.3", "@types/cordova-plugin-network-information": "0.0.3",
"@types/node": "^8.0.47", "@types/node": "8.10.19",
"@types/promise.prototype.finally": "^2.0.2", "@types/promise.prototype.finally": "2.0.2",
"chart.js": "^2.7.2", "chart.js": "2.7.2",
"cordova-android": "7.0.0", "com-darryncampbell-cordova-plugin-intent": "1.1.1",
"cordova-ios": "4.5.4", "cordova-android": "7.1.2",
"cordova-android-support-gradle-release": "2.0.1",
"cordova-clipboard": "1.2.1",
"cordova-ios": "4.5.5",
"cordova-plugin-app-event": "1.2.1",
"cordova-plugin-badge": "0.8.8",
"cordova-plugin-camera": "4.0.3",
"cordova-plugin-customurlscheme": "4.3.0",
"cordova-plugin-device": "2.0.2",
"cordova-plugin-file": "6.0.1",
"cordova-plugin-file-opener2": "2.0.19",
"cordova-plugin-file-transfer": "1.7.1",
"cordova-plugin-globalization": "1.11.0",
"cordova-plugin-inappbrowser": "3.0.0",
"cordova-plugin-ionic-keyboard": "2.1.3",
"cordova-plugin-local-notifications-mm": "1.0.13",
"cordova-plugin-media-capture": "3.0.2",
"cordova-plugin-network-information": "2.0.1",
"cordova-plugin-screen-orientation": "3.0.1",
"cordova-plugin-splashscreen": "5.0.2",
"cordova-plugin-statusbar": "2.4.2",
"cordova-plugin-whitelist": "1.3.3",
"cordova-plugin-zip": "3.1.0",
"cordova-sqlite-storage": "2.6.0",
"es6-promise-plugin": "4.2.2",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"ionic-angular": "^3.9.2", "ionic-angular": "3.9.2",
"ionicons": "3.0.0", "ionicons": "3.0.0",
"jszip": "^3.1.4", "jszip": "3.1.5",
"moment": "^2.19.1", "moment": "2.22.2",
"promise.prototype.finally": "^3.0.1", "nl.kingsquare.cordova.background-audio": "1.0.1",
"phonegap-plugin-push": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle",
"promise.prototype.finally": "3.1.0",
"rxjs": "5.5.11", "rxjs": "5.5.11",
"sw-toolbox": "3.6.0", "sw-toolbox": "3.6.0",
"ts-md5": "^1.2.2", "ts-md5": "1.2.4",
"web-animations-js": "2.3.1",
"zone.js": "0.8.26" "zone.js": "0.8.26"
}, },
"devDependencies": { "devDependencies": {
"@ionic/app-scripts": "3.1.9", "@ionic/app-scripts": "3.1.9",
"electron-rebuild": "^1.8.1", "electron-rebuild": "1.8.1",
"gulp": "^3.9.1", "electron-builder-lib": "20.23.1",
"gulp-clip-empty-files": "^0.1.2", "gulp": "4.0.0",
"gulp-flatten": "^0.4.0", "gulp-clip-empty-files": "0.1.2",
"gulp-rename": "^1.2.2", "gulp-flatten": "0.4.0",
"gulp-slash": "^1.1.3", "gulp-rename": "1.3.0",
"node-loader": "^0.6.0", "gulp-slash": "1.1.3",
"through": "^2.3.8", "gulp-util": "3.0.8",
"typescript": "~2.6.2", "node-loader": "0.6.0",
"webpack-merge": "^4.1.2" "through": "2.3.8",
}, "typescript": "2.6.2",
"optionalDependencies": { "webpack-merge": "4.1.2"
"electron-windows-notifications": "^2.1.1",
"electron-builder-squirrel-windows": "^20.19.0"
}, },
"browser": { "browser": {
"electron": false "electron": false
@ -114,7 +140,39 @@
"platforms": [ "platforms": [
"android", "android",
"ios" "ios"
] ],
"plugins": {
"com-darryncampbell-cordova-plugin-intent": {},
"cordova-android-support-gradle-release": {
"ANDROID_SUPPORT_VERSION": "27.1.0"
},
"cordova-clipboard": {},
"cordova-plugin-badge": {},
"cordova-plugin-camera": {},
"cordova-plugin-customurlscheme": {
"URL_SCHEME": "moodlemobile"
},
"cordova-plugin-device": {},
"cordova-plugin-file": {},
"cordova-plugin-file-opener2": {},
"cordova-plugin-file-transfer": {},
"cordova-plugin-globalization": {},
"cordova-plugin-inappbrowser": {},
"cordova-plugin-ionic-keyboard": {},
"cordova-plugin-local-notifications-mm": {},
"cordova-plugin-media-capture": {},
"cordova-plugin-network-information": {},
"cordova-plugin-screen-orientation": {},
"cordova-plugin-splashscreen": {},
"cordova-plugin-statusbar": {},
"cordova-plugin-whitelist": {},
"cordova-plugin-zip": {},
"cordova-sqlite-storage": {},
"nl.kingsquare.cordova.background-audio": {},
"phonegap-plugin-push": {
"SENDER_ID": "694767596569"
}
}
}, },
"main": "desktop/electron.js", "main": "desktop/electron.js",
"build": { "build": {
@ -162,7 +220,8 @@
"icon": "resources/desktop/icon.ico" "icon": "resources/desktop/icon.ico"
}, },
"linux": { "linux": {
"category": "Education" "category": "Education",
"target": "AppImage"
}, },
"snap": { "snap": {
"confinement": "classic" "confinement": "classic"

51
scripts/aot.sh 100755
View File

@ -0,0 +1,51 @@
#!/bin/bash
# Compile AOT.
if [ $TRAVIS_BRANCH == 'integration' ] || [ $TRAVIS_BRANCH == 'master' ] || [ -z $TRAVIS_BRANCH ] ; then
cd scripts
./build_lang.sh
cd ..
if [ $TRAVIS_BRANCH == 'master' ] && [ ! -z $GIT_TOKEN ] ; then
git remote set-url origin https://$GIT_TOKEN@github.com/$TRAVIS_REPO_SLUG.git
git fetch -q origin
git add src/assets/lang
git add */en.json
git commit -m 'Update lang files [ci skip]'
git push origin HEAD:$TRAVIS_BRANCH
version=`grep versionname src/config.json| cut -d: -f2|cut -d'"' -f2`
date=`date +%Y%m%d`'00'
pushd ../../moodle-local_moodlemobileapp
sed -ie "s/release[ ]*=[ ]*'[^']*';/release = '$version';/1" version.php
sed -ie "s/version[ ]*=[ ]*[0-9]*;/version = $date;/1" version.php
rm version.phpe
git remote set-url origin https://$GIT_TOKEN@github.com/moodlehq/moodle-local_moodlemobileapp.git
git fetch -q origin
git add .
git commit -m "Update lang files from $version"
git push
popd
fi
sed -ie $'s~throw new Error("No ResourceLoader.*~url = "templates/" + url;\\\nvar resolve;\\\nvar reject;\\\nvar promise = new Promise(function (res, rej) {\\\nresolve = res;\\\nreject = rej;\\\n});\\\nvar xhr = new XMLHttpRequest();\\\nxhr.open("GET", url, true);\\\nxhr.responseType = "text";\\\nxhr.onload = function () {\\\nvar response = xhr.response || xhr.responseText;\\\nvar status = xhr.status === 1223 ? 204 : xhr.status;\\\nif (status === 0) {\\\nstatus = response ? 200 : 0;\\\n}\\\nif (200 <= status \&\& status <= 300) {\\\nresolve(response);\\\n}\\\nelse {\\\nreject("Failed to load " + url);\\\n}\\\n};\\\nxhr.onerror = function () { reject("Failed to load " + url); };\\\nxhr.send();\\\nreturn promise;\\\n~g' node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js
sed -ie "s/context\.isProd || hasArg('--minifyJs')/hasArg('--minifyJs')/g" node_modules/@ionic/app-scripts/dist/util/config.js
sed -ie "s/context\.isProd || hasArg('--optimizeJs')/hasArg('--optimizeJs')/g" node_modules/@ionic/app-scripts/dist/util/config.js
npm run ionic:build -- --prod
fi
# Copy to PGB git (only on a configured travis build).
if [ ! -z $GIT_ORG ] && [ ! -z $GIT_TOKEN ] ; then
gitfolder=${PWD##*/}
cd ..
git clone --depth 1 --no-single-branch https://github.com/$GIT_ORG/moodlemobile-phonegapbuild.git pgb
cd pgb
git checkout $TRAVIS_BRANCH
rm -Rf assets build index.html templates
cp -Rf ../$gitfolder/www/* ./
rm -Rf assets/countries assets/mimetypes
git add .
git commit -m "Travis build: $TRAVIS_BUILD_NUMBER"
git push https://$GIT_TOKEN@github.com/$GIT_ORG/moodlemobile-phonegapbuild.git
fi

View File

@ -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!'

View File

@ -0,0 +1,266 @@
#!/bin/bash
source "functions.sh"
#Saves or updates a key on langindex_old.json
function save_key {
key=$1
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 {
key=$1
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
}
#Check if and i exists in php file
function exists_in_file {
file=$1
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 {
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 {
key=$1
value=$2
type=`echo $key | cut -d'.' -f1`
component=`echo $key | cut -d'.' -f2`
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
}
#Finds if there's a better file where to get the id from.
function find_better_file {
key=$1
value=$2
current=$3
type=`echo $key | cut -d'.' -f1`
component=`echo $key | cut -d'.' -f2`
plainid=`echo $key | cut -d'.' -f3-`
if [ -z "$plainid" ]; then
plainid=$component
component='moodle'
fi
currentFile=`echo $current | cut -d'/' -f1`
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`
exec="jq -r .\"$key\" $1"
value=`$exec`
if [ -z "$found" ] || [ "$found" == 'null' ]; then
guess_file $key "$value"
elif [ ! -z "$findbetter" ]; then
find_better_file "$key" "$value" "$found"
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!'

View File

@ -0,0 +1,41 @@
#!/bin/bash
LANGPACKSFOLDER='../../moodle-langpacks'
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"
}
function print_ok {
tput setaf 2; echo " OK: $1"
echo
}
function print_message {
tput setaf 3; echo "-------- $1"
echo
}
function print_title {
stepnumber=$(($stepnumber + 1))
echo
tput setaf 5; echo "$stepnumber $1"
tput setaf 5; echo '=================='
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,388 @@
<?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', '../src/config.json');
$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;
$languages = explode(',', $argv[1]);
} else {
$forcedetect = true;
$languages = $config_langs;
}
// 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;
} 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\n";
$add_langs = array();
// Process the languages.
foreach ($languages as $lang) {
$ok = build_lang($lang, $keys, $total);
if ($ok) {
$add_langs[$lang] = $lang;
}
}
if ($forcedetect) {
echo "\n\n\n";
$all_languages = glob(LANGPACKSFOLDER.'/*' , GLOB_ONLYDIR);
function get_lang_from_dir($dir) {
return str_replace('_', '-', explode('/', $dir)[3]);
}
$all_languages = array_map('get_lang_from_dir', $all_languages);
$detect_lang = array_diff($all_languages, $languages);
$new_langs = array();
foreach ($detect_lang as $lang) {
$new = detect_lang($lang, $keys, $total);
if ($new) {
$new_langs[$lang] = $lang;
}
}
if (!empty($new_langs)) {
echo "\n\n\nThe following languages are going to be added\n\n\n";
foreach ($new_langs as $lang) {
$ok = build_lang($lang, $keys, $total);
if ($ok) {
$add_langs[$lang] = $lang;
}
}
add_langs_to_config($add_langs, $config);
}
} else {
add_langs_to_config($add_langs, $config);
}
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 build_lang($lang, $keys, $total) {
$local = 0;
$langFile = false;
$translations = [];
$langfoldername = str_replace('-', '_', $lang);
if (!is_dir(LANGPACKSFOLDER.'/'.$langfoldername) || !is_file(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php')) {
echo "Cannot translate $langfoldername, folder not found";
return false;
}
$string = [];
include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php');
$parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : "";
echo "Processing $lang";
if ($parent != "" && $parent != $lang) {
echo "($parent)";
}
// Add the translation to the array.
foreach ($keys as $key => $value) {
$file = LANGPACKSFOLDER.'/'.$langfoldername.'/'.$value->file.'.php';
// Apply translations.
if (!file_exists($file)) {
continue;
}
$string = [];
include($file);
if (!isset($string[$value->string])) {
// Not yet translated. Do not override.
if (!$langFile) {
// Load lang fils just once.
$langFile = file_get_contents(ASSETSPATH.$lang.'.json');
$langFile = (array) json_decode($langFile);
}
if (is_array($langFile) && isset($langFile[$key])) {
$translations[$key] = $langFile[$key];
$local++;
}
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 {
$local++;
}
$translations[$key] = html_entity_decode($text);
}
// 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);
echo "\t\t$success of $total -> $percentage% ($local local)\n";
if ($lang == 'en') {
generate_local_moodlemobileapp($keys, $translations);
override_component_lang_files($keys, $translations);
}
return true;
}
function detect_lang($lang, $keys, $total) {
$success = 0;
$local = 0;
$langfoldername = str_replace('-', '_', $lang);
if (!is_dir(LANGPACKSFOLDER.'/'.$langfoldername) || !is_file(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php')) {
echo "Cannot translate $langfoldername, folder not found";
return false;
}
$string = [];
include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php');
$parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : "";
if (!isset($string['thislanguage'])) {
echo "Cannot translate $langfoldername, name not found";
return false;
}
echo "Checking $lang";
if ($parent != "" && $parent != $lang) {
echo "($parent)";
}
$langname = $string['thislanguage'];
echo " ".$langname." -D";
// Add the translation to the array.
foreach ($keys as $key => $value) {
$file = LANGPACKSFOLDER.'/'.$langfoldername.'/'.$value->file.'.php';
// Apply translations.
if (!file_exists($file)) {
continue;
}
$string = [];
include($file);
if (!isset($string[$value->string])) {
continue;
} else {
$text = $string[$value->string];
}
if ($value->file == 'local_moodlemobileapp') {
$local++;
}
$success++;
}
$percentage = floor($success/$total *100);
echo "\t\t$success of $total -> $percentage% ($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 ($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.
Wed 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");
}

View File

@ -1,4 +0,0 @@
{
"badges": "شارات",
"expired": "عذراً، تم إغلاق هذا النشاط في {{$a}} وهو غير متوفر الآن."
}

View File

@ -1,11 +0,0 @@
{
"badgedetails": "Елементи на значката",
"badges": "Значки",
"contact": "Контакт",
"expired": "За съжаление тази дейност е затворена от {{$a}} и вече не е достъпна",
"expirydate": "Дата на изтичане",
"issuancedetails": "Срок на значката",
"issuerdetails": "Данни за връчващия",
"issuername": "Име на връчващия",
"nobadges": "Няма налични значки."
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Detalls de la insígnia",
"badges": "Insígnies",
"contact": "Contacte",
"dateawarded": "Data publicada",
"expired": "Aquesta activitat es va tancar el dia {{$a}} i ja no està disponible.",
"expirydate": "Data d'expiració",
"issuancedetails": "Expiració de la insígnia",
"issuerdetails": "Detalls de l'atorgador",
"issuername": "Nom de l'atorgador",
"nobadges": "No hi ha insígnies disponibles.",
"recipientdetails": "Detalls del receptor"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Detaily odznaku",
"badges": "Odznaky",
"contact": "Kontakt",
"dateawarded": "Datum udělení",
"expired": "Je nám líto, tato činnost byla uzavřena {{$a}} a není nadále dostupná",
"expirydate": "Datum vypršení platnosti",
"issuancedetails": "Vypršení platnosti odznaku",
"issuerdetails": "Podrobnosti o vydavateli",
"issuername": "Jméno vydavatele",
"nobadges": "Žádné odznaky nejsou k dispozici.",
"recipientdetails": "Podrobnosti o příjemci"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Badgedetaljer",
"badges": "Badges",
"contact": "Kontakt",
"dateawarded": "Udstedelsesdato",
"expired": "Beklager, denne aktivitet er lukket d. {{$a}} og er ikke længere tilgængelig",
"expirydate": "Udløbsdato",
"issuancedetails": "Badge-udløb",
"issuerdetails": "Udstederdata",
"issuername": "Udsteders navn",
"nobadges": "Der er ingen tilgængelige badges.",
"recipientdetails": "Modtagerdata"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Grundeinstellungen",
"badges": "Auszeichnungen",
"contact": "Kontakt",
"dateawarded": "Verleihdatum",
"expired": "Diese Abstimmung ist seit {{$a}} beendet. Eine Auswahl ist nicht mehr möglich.",
"expirydate": "Ablaufdatum",
"issuancedetails": "Ablauf festlegen",
"issuerdetails": "Verleiher",
"issuername": "Verleiher",
"nobadges": "Keine Auszeichnungen verfügbar",
"recipientdetails": "Empfängerdetails"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Grundeinstellungen",
"badges": "Auszeichnungen",
"contact": "Kontakt",
"dateawarded": "Verleihdatum",
"expired": "Diese Abstimmung ist seit {{$a}} beendet. Eine Auswahl ist nicht mehr möglich.",
"expirydate": "Ablaufdatum",
"issuancedetails": "Ablauf festlegen",
"issuerdetails": "Verleiher",
"issuername": "Verleiher",
"nobadges": "Keine Auszeichnungen verfügbar",
"recipientdetails": "Empfängerdetails"
}

View File

@ -1,4 +0,0 @@
{
"badges": "Βραβεία",
"expired": "Η δραστηριότητα αυτή έκλεισε στις {{$a}} και δεν είναι πλέον διαθέσιμη"
}

View File

@ -1,13 +1,29 @@
{ {
"alignment": "Competency",
"badgedetails": "Badge details", "badgedetails": "Badge details",
"badges": "Badges", "badges": "Badges",
"bendorsement": "Endorsement",
"claimcomment": "Endorsement comment",
"claimid": "Claim URL",
"contact": "Contact", "contact": "Contact",
"dateawarded": "Date issued", "dateawarded": "Date issued",
"expired": "Expired", "expired": "Expired",
"expirydate": "Expiry date", "expirydate": "Expiry date",
"imageauthoremail": "Image author's email",
"imageauthorname": "Image author's name",
"imageauthorurl": "Image author's URL",
"imagecaption": "Image caption",
"issuancedetails": "Badge expiry", "issuancedetails": "Badge expiry",
"issuerdetails": "Issuer details", "issuerdetails": "Issuer details",
"issueremail": "Email",
"issuername": "Issuer name", "issuername": "Issuer name",
"issuerurl": "Issuer URL",
"language": "Language",
"noalignment": "This badge does not have any competencies specified.",
"nobadges": "There are no badges available.", "nobadges": "There are no badges available.",
"recipientdetails": "Recipient details" "norelated": "This badge does not have any related badges.",
"recipientdetails": "Recipient details",
"relatedbages": "Related badges",
"version": "Version",
"warnexpired": "(This badge has expired!)"
} }

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Detalles de insignia",
"badges": "Insignias",
"contact": "Contacto",
"dateawarded": "Fecha de emisión",
"expired": "Lo sentimos, esta actividad se cerró el {{$a}} y ya no está disponible",
"expirydate": "Fecha de caducidad",
"issuancedetails": "Caducidad de insignia",
"issuerdetails": "Detalles del emisor",
"issuername": "Nombre del emisor",
"nobadges": "No hay insignias disponibles.",
"recipientdetails": "Detalles de receptores"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Detalles de la insignia",
"badges": "Insignias",
"contact": "Contacto",
"dateawarded": "Fecha de la emisión",
"expired": "Lo sentimos, esta actividad se cerró el {{$a}} y ya no está disponible",
"expirydate": "Fecha de expiración",
"issuancedetails": "Caducidad de la insignia",
"issuerdetails": "Detalles del emisor",
"issuername": "Nombre del emisor",
"nobadges": "No hay insignias disponibles",
"recipientdetails": "Detalles del destinatario"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Dominaren xehetasunak",
"badges": "Dominak",
"contact": "Kontaktua",
"dateawarded": "Emate-data",
"expired": "Sentitzen dugu, jarduera hau {{$a}}(e)an itxi zen eta dagoeneko ez dago eskuragarri.",
"expirydate": "Epemugaren data",
"issuancedetails": "Dominaren iraungitzea",
"issuerdetails": "Emailearen xehetasunak",
"issuername": "Emailearen izena",
"nobadges": "Ez dago dominarik eskura.",
"recipientdetails": "Jasotzailearen zehaztasunak"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "مشخصات مدال",
"badges": "مدال‌ها",
"contact": "تماس",
"dateawarded": "تاریخ صدور",
"expired": "با عرض پوزش، این فعالیت در {{$a}} بسته شد و دیگر در دسترس نیست",
"expirydate": "تاریخ انقضا",
"issuancedetails": "انقضای مدال",
"issuerdetails": "مشخصات صادرکننده",
"issuername": "نام صادرکننده",
"nobadges": "مدالی موجود نیست.",
"recipientdetails": "مشخصات دریافت‌کننده"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Osaamismerkin tiedot",
"badges": "Osaamismerkit",
"contact": "Yhteystieto",
"dateawarded": "Myöntämispäivä",
"expired": "Tämä aktiviteeti on suljettu {{$a}} eikä ole enää käytettävissä.",
"expirydate": "Vanhenemispäivä",
"issuancedetails": "Osaamismerkin vanhentuminen",
"issuerdetails": "Osaamismerkin myöntäjän tiedot",
"issuername": "Osaamismerkin myöntäjän nimi",
"nobadges": "Yhtään osaamismerkkiä ei ole tarjolla",
"recipientdetails": "Vastaanottajan tiedot"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Description du badge",
"badges": "Badges",
"contact": "Contact",
"dateawarded": "Date de remise",
"expired": "Désolé, cette activité s'est terminée le {{$a}} et n'est plus disponible",
"expirydate": "Date d'échéance",
"issuancedetails": "Échéance du badge",
"issuerdetails": "Détail de l'émetteur",
"issuername": "Nom de l'émetteur",
"nobadges": "Il n'y a pas de badge disponible.",
"recipientdetails": "Infos détenteur"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "פרטי ההישג",
"badges": "הישגים",
"contact": "ליצירת קשר",
"dateawarded": "תאריך הקבלה",
"expired": "מצטערים, פעילות זו נסגרה על {{$a}} והיא איננה זמינה יותר",
"expirydate": "תאריך תפוגה",
"issuancedetails": "מועד תפוגת ההישג",
"issuerdetails": "פרטי הגורם אשר העניק את ההישג",
"issuername": "שם מעניק ההישג",
"nobadges": "אין הישגים זמינים.",
"recipientdetails": "פרטי המכותב"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Detalji značke",
"badges": "Značke",
"contact": "Kontakt",
"dateawarded": "Datum izdavanja",
"expired": "Nažalost, ova aktivnost je zatvorena od {{$a}} i nije više dostupna",
"expirydate": "Datum isteka",
"issuancedetails": "Istek značke",
"issuerdetails": "Detalji o izdavaču",
"issuername": "Ime izdavača",
"nobadges": "Nema dostupnih značaka.",
"recipientdetails": "Podaci o dobitniku"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Részletek",
"badges": "Kitűzők",
"contact": "Kapcsolat",
"dateawarded": "Kiadás dátuma",
"expired": "Ez a tevékenység {{$a}} időpontban lezárult és már nem érhető el",
"expirydate": "Lejárat időpontja",
"issuancedetails": "A kitűző lejárata",
"issuerdetails": "Az adományozó adatai",
"issuername": "Az adományozó neve",
"nobadges": "Nincs elérhető kitűző.",
"recipientdetails": "A megjutalmazott adatai"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Dettagli badge",
"badges": "Badge",
"contact": "Contatto",
"dateawarded": "Data di rilascio",
"expired": "Spiacente, questa attività è stata chiusa il {{$a}} e non è più disponibile",
"expirydate": "Data di scadenza",
"issuancedetails": "Scadenza badge",
"issuerdetails": "Dettagli di chi rilascia il badge",
"issuername": "Nome di chi rilascia il badge",
"nobadges": "Non sono presenti badge.",
"recipientdetails": "Dettagli destinatario"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "バッジ詳細",
"badges": "バッジ",
"contact": "連絡先",
"dateawarded": "発効日",
"expired": "申し訳ございません、この活動は {{$a}} に終了しているため、これ以上利用することはできません。",
"expirydate": "有効期限",
"issuancedetails": "バッジ有効期限",
"issuerdetails": "発行者詳細",
"issuername": "発行者名",
"nobadges": "利用できるバッジはありません。",
"recipientdetails": "取得者詳細"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "뱃지 세부사항",
"badges": "뱃지",
"contact": "연락처",
"dateawarded": "발행일",
"expired": "죄송합니다. 이 활동은 {{$a}} 에 종료되어서 더 이상 사용할 수 없습니다.",
"expirydate": "만료일",
"issuancedetails": "뱃지 만료기한",
"issuerdetails": "발행자 세부정보",
"issuername": "발행자 이름",
"nobadges": "사용가능한 뱃지가 없습니다.",
"recipientdetails": "수신자 세부사항"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Pasiekimo detalės",
"badges": "Pasiekimai",
"contact": "Kontaktas",
"dateawarded": "Suteikimo data",
"expired": "Atsiprašome, veikla uždaryta {{$a}} ir nebegalima",
"expirydate": "Galiojimo laikas",
"issuancedetails": "Pasiekimo galiojimas",
"issuerdetails": "Suteikėjo detalesnė informacija",
"issuername": "Suteikėjo vardas",
"nobadges": "Nėra sukurtų pasiekimų.",
"recipientdetails": "Informacija apie gavėją"
}

View File

@ -1,3 +0,0 @@
{
"expired": "संपलेला"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Badgedetails",
"badges": "Badges",
"contact": "Contact",
"dateawarded": "Uitgavedatum",
"expired": "Sorry, deze activiteit is afgesloten op {{$a}} en is niet meer beschikbaar",
"expirydate": "Vervaldatum",
"issuancedetails": "Badge verloopt",
"issuerdetails": "Details uitgever",
"issuername": "Naam uitgever",
"nobadges": "Er zijn geen badges beschikbaar.",
"recipientdetails": "Details ontvanger"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Utmerkelsesdetaljer",
"badges": "Utmerkelser",
"contact": "Kontakt",
"dateawarded": "Dato tildelt",
"expired": "Beklager, denne aktiviteten ble stengt {{$a}} og er ikke tilgjengelig lenger.",
"expirydate": "Utløpsdato",
"issuancedetails": "Utløpsdato på utmerkelse",
"issuerdetails": "Utstederdetaljer",
"issuername": "Navn på utsteder",
"nobadges": "Det er ingen tilgjengelige utmerkelser.",
"recipientdetails": "Mottakerdetaljer"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Szczegóły odznaki",
"badges": "Odznaki",
"contact": "Kontakt",
"dateawarded": "Data wydania",
"expired": "Niestety ta aktywność została zamknięta {{$a}} i nie jest już dostępna.",
"expirydate": "Data ważności",
"issuancedetails": "Wygaśnięcie odznaki",
"issuerdetails": "Dane wystawcy",
"issuername": "Nazwa wydawcy",
"nobadges": "Brak dostępnych odznak",
"recipientdetails": "Dane odbiorcy"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Detalhes do emblema",
"badges": "Emblemas",
"contact": "Contato",
"dateawarded": "Data de emissão",
"expired": "Esta atividade está encerrada desde {{$a}}",
"expirydate": "Data de validade",
"issuancedetails": "Expiração do emblema",
"issuerdetails": "Detalhes do emissor",
"issuername": "Nome do emissor",
"nobadges": "Não há emblemas disponíveis.",
"recipientdetails": "Detalhes do usuário a receber o emblema"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Detalhes da Medalha",
"badges": "Medalhas",
"contact": "Contacto",
"dateawarded": "Data de emissão",
"expired": "Esta atividade terminou em {{$a}} e já não está disponível",
"expirydate": "Data de validade",
"issuancedetails": "Data de validade da Medalha",
"issuerdetails": "Detalhes do emissor",
"issuername": "Nome do emissor",
"nobadges": "Não existem Medalhas disponíveis.",
"recipientdetails": "Detalhes do condecorado"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Detalii ecuson",
"badges": "Ecusoane",
"contact": "Contact",
"dateawarded": "Data emiterii",
"expired": "Ne pare rău, această activitate s-a închis la {{$a}} şi nu mai este disponibilă",
"expirydate": "Dată de expirare",
"issuancedetails": "Expirare ecuson",
"issuerdetails": "Detalii emitent",
"issuername": "Nume emitent",
"nobadges": "Nu există ecusoane disponibile",
"recipientdetails": "Detalii recipient"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Подробнее о значке",
"badges": "Значки",
"contact": "Контакты",
"dateawarded": "Дата выдачи",
"expired": "Извините, этот элемент курса закрыт {{$a}} и более недоступен",
"expirydate": "Дата окончания срока действия",
"issuancedetails": "Срок действия значка",
"issuerdetails": "Сведения об эмитенте",
"issuername": "Наименование эмитента",
"nobadges": "Нет доступных значков.",
"recipientdetails": "Сведения о получателе"
}

View File

@ -1,12 +0,0 @@
{
"badgedetails": "Подаци о беџу",
"badges": "Беџеви",
"contact": "Контакт",
"dateawarded": "Датум издавања",
"expirydate": "Датум истека",
"issuancedetails": "Беџ истиче",
"issuerdetails": "Подаци о издавачу",
"issuername": "Име/назив издавача беџа",
"nobadges": "Нема доступних беџева",
"recipientdetails": "Детаљи о примаоцу"
}

View File

@ -1,12 +0,0 @@
{
"badgedetails": "Podaci o bedžu",
"badges": "Bedževi",
"contact": "Kontakt",
"dateawarded": "Datum izdavanja",
"expirydate": "Datum isteka",
"issuancedetails": "Bedž ističe",
"issuerdetails": "Podaci o izdavaču",
"issuername": "Ime/naziv izdavača bedža",
"nobadges": "Nema dostupnih bedževa",
"recipientdetails": "Detalji o primaocu"
}

View File

@ -1,12 +0,0 @@
{
"badgedetails": "Detaljer för märke",
"badges": "Märken",
"contact": "Kontakt",
"dateawarded": "Utfärdandedatum",
"expired": "Den här aktiviteten är stängd på {{$a}} och den är inte längre tillgänglig.",
"expirydate": "Förfallodatum",
"issuancedetails": "Förfallande av märke",
"issuerdetails": "Utfärdarens detaljer",
"issuername": "Utfärdarens namn",
"nobadges": "Det finns inga märken tillgängliga."
}

View File

@ -1,4 +0,0 @@
{
"badges": "Бейҷҳо",
"expired": "Бубахшед,ин фаъолият маҳкам карда шудааст {{$a}} ва акнун дастрас нест"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Nişan ayrıntıları",
"badges": "Nişanlar",
"contact": "İletişim",
"dateawarded": "Verilen tarih",
"expired": "Üzgünüz, bu etkinlik {{$a}} tarihinde kapandı ve bu etkinliğe artık ulaşılamaz",
"expirydate": "Bitiş Tarihi",
"issuancedetails": "Rozet sona erme",
"issuerdetails": ıkaran ayrıntıları",
"issuername": ıkaranın adı",
"nobadges": "Uygun nişan bulunmuyor.",
"recipientdetails": "Alıcı bilgileri"
}

View File

@ -1,13 +0,0 @@
{
"badgedetails": "Детальніше про відзнаку",
"badges": "Відзнаки",
"contact": "Контакт",
"dateawarded": "Дата отримання",
"expired": "На жаль, ця діяльність закрита для {{$a}} та більше недоступна",
"expirydate": "Дата завершення",
"issuancedetails": "Відзнака не актуальна",
"issuerdetails": "Деталі присудження",
"issuername": "Ім’я видавця",
"nobadges": "Немає доступних відзнак.",
"recipientdetails": "Деталі отримувача"
}

View File

@ -1,12 +0,0 @@
{
"badgedetails": "勋章详情",
"badges": "勋章",
"contact": "联系",
"dateawarded": "授予日期",
"expirydate": "过期时间",
"issuancedetails": "有效期",
"issuerdetails": "授勋机构详情",
"issuername": "授勋机构名称",
"nobadges": "没有可用的勋章",
"recipientdetails": "获得者详情"
}

View File

@ -1,12 +0,0 @@
{
"badgedetails": "獎章細節",
"badges": "獎章",
"contact": "聯絡",
"dateawarded": "頒發的日期",
"expirydate": "失效日期",
"issuancedetails": "獎章到期",
"issuerdetails": "頒授者細節",
"issuername": "頒授者的姓名",
"nobadges": "這裡沒有可用的獎章",
"recipientdetails": "收件者細節"
}

View File

@ -19,7 +19,7 @@
</ion-item-group> </ion-item-group>
<ion-item-group *ngIf="user.fullname"> <ion-item-group *ngIf="user.fullname">
<ion-item-divider color="light"> <ion-item-divider>
<h2>{{ 'addon.badges.recipientdetails' | translate}}</h2> <h2>{{ 'addon.badges.recipientdetails' | translate}}</h2>
</ion-item-divider> </ion-item-divider>
<ion-item text-wrap> <ion-item text-wrap>
@ -31,7 +31,7 @@
</ion-item-group> </ion-item-group>
<ion-item-group> <ion-item-group>
<ion-item-divider color="light"> <ion-item-divider>
<h2>{{ 'addon.badges.issuerdetails' | translate}}</h2> <h2>{{ 'addon.badges.issuerdetails' | translate}}</h2>
</ion-item-divider> </ion-item-divider>
<ion-item text-wrap *ngIf="badge.issuername"> <ion-item text-wrap *ngIf="badge.issuername">
@ -42,19 +42,27 @@
</ion-item> </ion-item>
<ion-item text-wrap *ngIf="badge.issuercontact"> <ion-item text-wrap *ngIf="badge.issuercontact">
<h2>{{ 'addon.badges.contact' | translate}}</h2> <h2>{{ 'addon.badges.contact' | translate}}</h2>
<p> <p><a href="mailto:{{badge.issuercontact}}" core-link auto-login="no">
<core-format-text clean="true" [text]="badge.issuercontact"></core-format-text> <core-format-text [text]="badge.issuercontact"></core-format-text>
</p> </a></p>
</ion-item> </ion-item>
</ion-item-group> </ion-item-group>
<ion-item-group> <ion-item-group>
<ion-item-divider color="light"> <ion-item-divider>
<h2>{{ 'addon.badges.badgedetails' | translate}}</h2> <h2>{{ 'addon.badges.badgedetails' | translate}}</h2>
</ion-item-divider> </ion-item-divider>
<ion-item text-wrap *ngIf="badge.name"> <ion-item text-wrap *ngIf="badge.name">
<h2>{{ 'core.name' | translate}}</h2> <h2>{{ 'core.name' | translate}}</h2>
<p>{{badge.name}}</p> <p>{{ badge.name }}</p>
</ion-item>
<ion-item text-wrap *ngIf="badge.version">
<h2>{{ 'addon.badges.version' | translate}}</h2>
<p>{{ badge.version }}</p>
</ion-item>
<ion-item text-wrap *ngIf="badge.language">
<h2>{{ 'addon.badges.language' | translate}}</h2>
<p>{{ badge.language }}</p>
</ion-item> </ion-item>
<ion-item text-wrap *ngIf="badge.description"> <ion-item text-wrap *ngIf="badge.description">
<h2>{{ 'core.description' | translate}}</h2> <h2>{{ 'core.description' | translate}}</h2>
@ -62,25 +70,117 @@
<core-format-text clean="true" [text]="badge.description"></core-format-text> <core-format-text clean="true" [text]="badge.description"></core-format-text>
</p> </p>
</ion-item> </ion-item>
<ion-item text-wrap *ngIf="badge.imageauthorname">
<h2>{{ 'addon.badges.imageauthorname' | translate}}</h2>
<p>{{ badge.imageauthorname }}</p>
</ion-item>
<ion-item text-wrap *ngIf="badge.imageauthoremail">
<h2>{{ 'addon.badges.imageauthoremail' | translate}}</h2>
<p><a href="mailto:{{badge.imageauthoremail}}" core-link auto-login="no">
<core-format-text [text]="badge.imageauthoremail"></core-format-text>
</a></p>
</ion-item>
<ion-item text-wrap *ngIf="badge.imageauthorurl">
<h2>{{ 'addon.badges.imageauthorurl' | translate}}</h2>
<p><a [href]="badge.imageauthorurl" core-link auto-login="no">
<core-format-text [text]="badge.imageauthorurl"></core-format-text>
</a></p>
</ion-item>
<ion-item text-wrap *ngIf="badge.imagecaption">
<h2>{{ 'addon.badges.imagecaption' | translate}}</h2>
<p><core-format-text [text]="badge.imagecaption"></core-format-text></p>
</ion-item>
<ion-item text-wrap *ngIf="course.fullname"> <ion-item text-wrap *ngIf="course.fullname">
<h2>{{ 'core.course' | translate}}</h2> <h2>{{ 'core.course' | translate}}</h2>
<p> <p>
<core-format-text [text]="course.fullname"></core-format-text> <core-format-text [text]="course.fullname"></core-format-text>
</p> </p>
</ion-item> </ion-item>
<!-- Criteria (not yet avalaible) -->
</ion-item-group> </ion-item-group>
<ion-item-group> <ion-item-group>
<ion-item-divider color="light"> <ion-item-divider>
<h2>{{ 'addon.badges.issuancedetails' | translate}}</h2> <h2>{{ 'addon.badges.issuancedetails' | translate}}</h2>
</ion-item-divider> </ion-item-divider>
<ion-item text-wrap *ngIf="badge.dateissued"> <ion-item text-wrap *ngIf="badge.dateissued">
<h2>{{ 'addon.badges.dateawarded' | translate}}</h2> <h2>{{ 'addon.badges.dateawarded' | translate}}</h2>
<p>{{badge.dateissued | coreToLocaleString }}</p> <p>{{badge.dateissued * 1000 | coreFormatDate }}</p>
</ion-item> </ion-item>
<ion-item text-wrap *ngIf="badge.dateexpire"> <ion-item text-wrap *ngIf="badge.dateexpire">
<h2>{{ 'addon.badges.expirydate' | translate}}</h2> <h2>{{ 'addon.badges.expirydate' | translate}}</h2>
<p>{{badge.dateexpire | coreToLocaleString }}</p> <p>
{{ badge.dateexpire * 1000 | coreFormatDate }}
<span class="text-danger" *ngIf="currentTime >= badge.dateexpire">
{{ 'addon.badges.warnexpired' | translate }}
</span>
</p>
</ion-item>
<!-- Evidence (not yet avalaible) -->
</ion-item-group>
<!-- Endorsement -->
<ion-item-group *ngIf="badge.endorsement">
<ion-item-divider>
<h2>{{ 'addon.badges.bendorsement' | translate}}</h2>
</ion-item-divider>
<ion-item text-wrap *ngIf="badge.endorsement.issuername">
<h2>{{ 'addon.badges.issuername' | translate}}</h2>
<p>{{ badge.endorsement.issuername }}</p>
</ion-item>
<ion-item text-wrap *ngIf="badge.endorsement.issueremail">
<h2>{{ 'addon.badges.issueremail' | translate}}</h2>
<p><a href="mailto:{{badge.endorsement.issueremail}}" core-link auto-login="no">
<core-format-text [text]="badge.endorsement.issueremail"></core-format-text>
</a></p>
</ion-item>
<ion-item text-wrap *ngIf="badge.endorsement.issuerurl">
<h2>{{ 'addon.badges.issuerurl' | translate}}</h2>
<p><a [href]="badge.endorsement.issuerurl" core-link auto-login="no">
<core-format-text [text]="badge.endorsement.issuerurl"></core-format-text>
</a></p>
</ion-item>
<ion-item text-wrap *ngIf="badge.endorsement.dateissued">
<h2>{{ 'addon.badges.dateawarded' | translate}}</h2>
<p>{{ badge.endorsement.dateissued * 1000 | coreFormatDate }}</p>
</ion-item>
<ion-item text-wrap *ngIf="badge.endorsement.claimid">
<h2>{{ 'addon.badges.claimid' | translate}}</h2>
<p><a [href]="badge.endorsement.claimid" core-link auto-login="no">
<core-format-text [text]="badge.endorsement.claimid"></core-format-text>
</a></p>
</ion-item>
<ion-item text-wrap *ngIf="badge.endorsement.claimcomment">
<h2>{{ 'addon.badges.claimcomment' | translate}}</h2>
<p>
<core-format-text [text]="badge.endorsement.claimcomment"></core-format-text>
</p>
</ion-item>
</ion-item-group>
<!-- Related badges -->
<ion-item-group *ngIf="badge.relatedbadges">
<ion-item-divider>
<h2>{{ 'addon.badges.relatedbages' | translate}}</h2>
</ion-item-divider>
<ion-item text-wrap *ngFor="let relatedBadge of badge.relatedbadges">
<h2><core-format-text [text]="relatedBadge.name"></core-format-text></h2>
</ion-item>
<ion-item text-wrap *ngIf="badge.relatedbadges.length == 0">
<h2>{{ 'addon.badges.norelated' | translate}}</h2>
</ion-item>
</ion-item-group>
<!-- Competencies alignment -->
<ion-item-group *ngIf="badge.competencies">
<ion-item-divider>
<h2>{{ 'addon.badges.alignment' | translate}}</h2>
</ion-item-divider>
<a ion-item text-wrap *ngFor="let competency of badge.competencies" [href]="competency.targeturl" core-link auto-login="no">
<h2><core-format-text [text]="competency.targetname"></core-format-text></h2>
</a>
<ion-item text-wrap *ngIf="badge.competencies.length == 0">
<h2>{{ 'addon.badges.noalignment' | translate}}</h2>
</ion-item> </ion-item>
</ion-item-group> </ion-item-group>
</core-loading> </core-loading>

View File

@ -14,12 +14,12 @@
import { Component, ViewChild } from '@angular/core'; import { Component, ViewChild } from '@angular/core';
import { IonicPage, Content, NavParams } from 'ionic-angular'; import { IonicPage, Content, NavParams } from 'ionic-angular';
import { AddonBadgesProvider } from '../../providers/badges';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreUserProvider } from '@core/user/providers/user'; import { CoreUserProvider } from '@core/user/providers/user';
import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { AddonBadgesProvider } from '../../providers/badges';
/** /**
* Page that displays the list of calendar events. * Page that displays the list of calendar events.
@ -32,9 +32,10 @@ import { CoreCoursesProvider } from '@core/courses/providers/courses';
export class AddonBadgesIssuedBadgePage { export class AddonBadgesIssuedBadgePage {
@ViewChild(Content) content: Content; @ViewChild(Content) content: Content;
courseId: number; protected badgeHash: string;
userId: number; protected userId: number;
badgeHash: string; protected courseId: number;
user: any = {}; user: any = {};
course: any = {}; course: any = {};
badge: any = {}; badge: any = {};
@ -70,29 +71,29 @@ export class AddonBadgesIssuedBadgePage {
const promises = []; const promises = [];
this.currentTime = this.timeUtils.timestamp(); this.currentTime = this.timeUtils.timestamp();
let promise = this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => { promises.push(this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => {
this.user = user; this.user = user;
}); }));
promises.push(promise);
promise = this.badgesProvider.getUserBadges(this.courseId, this.userId).then((badges) => { promises.push(this.badgesProvider.getUserBadges(this.courseId, this.userId).then((badges) => {
badges.forEach((badge) => { const badge = badges.find((badge) => {
if (this.badgeHash == badge.uniquehash) { return this.badgeHash == badge.uniquehash;
this.badge = badge;
if (badge.courseid) {
return this.coursesProvider.getUserCourse(badge.courseid, true).then((course) => {
this.course = course;
}).catch(() => {
// Maybe an old deleted course.
this.course = null;
});
}
}
}); });
if (badge) {
this.badge = badge;
if (badge.courseid) {
return this.coursesProvider.getUserCourse(badge.courseid, true).then((course) => {
this.course = course;
}).catch(() => {
// Maybe an old deleted course.
this.course = null;
});
}
}
}).catch((message) => { }).catch((message) => {
this.domUtils.showErrorModalDefault(message, 'Error getting badge data.'); this.domUtils.showErrorModalDefault(message, 'Error getting badge data.');
}); }));
promises.push(promise);
return Promise.all(promises); return Promise.all(promises);
} }

View File

@ -18,7 +18,7 @@
<img [src]="badge.badgeurl" [alt]="badge.name" item-start core-external-content> <img [src]="badge.badgeurl" [alt]="badge.name" item-start core-external-content>
</ion-avatar> </ion-avatar>
<h2><core-format-text [text]="badge.name"></core-format-text></h2> <h2><core-format-text [text]="badge.name"></core-format-text></h2>
<p>{{ badge.dateissued | coreToLocaleString }}</p> <p>{{ badge.dateissued * 1000 | coreFormatDate :'strftimedatetimeshort' }}</p>
<ion-badge item-end color="danger" *ngIf="badge.dateexpire && currentTime >= badge.dateexpire"> <ion-badge item-end color="danger" *ngIf="badge.dateexpire && currentTime >= badge.dateexpire">
{{ 'addon.badges.expired' | translate }} {{ 'addon.badges.expired' | translate }}
</ion-badge> </ion-badge>

View File

@ -0,0 +1,44 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonBlockActivityModulesComponentsModule } from './components/components.module';
import { CoreBlockDelegate } from '@core/block/providers/delegate';
import { AddonBlockActivityModulesHandler } from './providers/block-handler';
@NgModule({
declarations: [
],
imports: [
IonicModule,
CoreComponentsModule,
CoreDirectivesModule,
AddonBlockActivityModulesComponentsModule,
TranslateModule.forChild()
],
exports: [
],
providers: [
AddonBlockActivityModulesHandler
]
})
export class AddonBlockActivityModulesModule {
constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockActivityModulesHandler) {
blockDelegate.registerHandler(blockHandler);
}
}

View File

@ -0,0 +1,26 @@
ion-app.app-root.md addon-block-activitymodules {
.core-module-icon {
margin-top: $label-md-margin-top;
margin-bottom: $label-md-margin-bottom;
width: 24px;
height: 24px;
}
}
ion-app.app-root.ios addon-block-activitymodules {
.core-module-icon {
margin-top: $label-ios-margin-top;
margin-bottom: $label-ios-margin-bottom;
width: 24px;
height: 24px;
}
}
ion-app.app-root.wp addon-block-activitymodules {
.core-module-icon {
margin-top: $item-wp-padding-top;
margin-bottom: $item-wp-padding-bottom;
width: 24px;
height: 24px;
}
}

View File

@ -0,0 +1,126 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, Injector, Input } from '@angular/core';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component';
import { CoreConstants } from '@core/constants';
import { TranslateService } from '@ngx-translate/core';
/**
* Component to render an "activity modules" block.
*/
@Component({
selector: 'addon-block-activitymodules',
templateUrl: 'addon-block-activitymodules.html'
})
export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent implements OnInit {
@Input() block: any; // The block to render.
@Input() contextLevel: string; // The context where the block will be used.
@Input() instanceId: number; // The instance ID associated with the context level.
entries: any[] = [];
protected fetchContentDefaultError = 'Error getting activity modules data.';
constructor(injector: Injector, protected utils: CoreUtilsProvider, protected courseProvider: CoreCourseProvider,
protected translate: TranslateService, protected moduleDelegate: CoreCourseModuleDelegate) {
super(injector, 'AddonBlockActivityModulesComponent');
}
/**
* Component being initialized.
*/
ngOnInit(): void {
super.ngOnInit();
}
/**
* Perform the invalidate content function.
*
* @return {Promise<any>} Resolved when done.
*/
protected invalidateContent(): Promise<any> {
return this.courseProvider.invalidateSections(this.instanceId);
}
/**
* Fetch the data to render the block.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchContent(): Promise<any> {
return this.courseProvider.getSections(this.instanceId, false, true).then((sections) => {
this.entries = [];
const archetypes = {},
modIcons = {};
let modFullNames = {};
sections.forEach((section) => {
if (!section.modules) {
return;
}
section.modules.forEach((mod) => {
if (mod.uservisible === false || !this.courseProvider.moduleHasView(mod) ||
typeof modFullNames[mod.modname] != 'undefined') {
// Ignore this module.
return;
}
// Get the archetype of the module type.
if (typeof archetypes[mod.modname] == 'undefined') {
archetypes[mod.modname] = this.moduleDelegate.supportsFeature(mod.modname,
CoreConstants.FEATURE_MOD_ARCHETYPE, CoreConstants.MOD_ARCHETYPE_OTHER);
}
// Get the full name of the module type.
if (archetypes[mod.modname] == CoreConstants.MOD_ARCHETYPE_RESOURCE) {
// All resources are gathered in a single "Resources" option.
if (!modFullNames['resources']) {
modFullNames['resources'] = this.translate.instant('core.resources');
}
} else {
modFullNames[mod.modname] = mod.modplural;
}
modIcons[mod.modname] = mod.modicon;
});
});
// Sort the modnames alphabetically.
modFullNames = this.utils.sortValues(modFullNames);
for (const modName in modFullNames) {
let icon;
if (modName === 'resources') {
icon = this.courseProvider.getModuleIconSrc('page', modIcons['page']);
} else {
icon = this.moduleDelegate.getModuleIconSrc(modName, modIcons[modName]);
}
this.entries.push({
icon: icon,
name: modFullNames[modName],
modName: modName
});
}
});
}
}

View File

@ -0,0 +1,9 @@
<ion-item-divider>
<h2>{{ 'addon.block_activitymodules.pluginname' | translate }}</h2>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<a ion-item text-wrap *ngFor="let entry of entries" class="item-media" detail-none [navPush]="'CoreCourseListModTypePage'" [navParams]="{title: entry.name, courseId: instanceId, modName: entry.modName}">
<img item-start [src]="entry.icon" alt="" role="presentation" class="core-module-icon">
<core-format-text [text]="entry.name"></core-format-text>
</a>
</core-loading>

View File

@ -0,0 +1,45 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonBlockActivityModulesComponent } from './activitymodules/activitymodules';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
@NgModule({
declarations: [
AddonBlockActivityModulesComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCourseComponentsModule
],
providers: [
],
exports: [
AddonBlockActivityModulesComponent
],
entryComponents: [
AddonBlockActivityModulesComponent
]
})
export class AddonBlockActivityModulesComponentsModule {}

View File

@ -0,0 +1,3 @@
{
"pluginname": "Activities"
}

View File

@ -0,0 +1,50 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Injector } from '@angular/core';
import { CoreBlockHandlerData } from '@core/block/providers/delegate';
import { AddonBlockActivityModulesComponent } from '../components/activitymodules/activitymodules';
import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
/**
* Block handler.
*/
@Injectable()
export class AddonBlockActivityModulesHandler extends CoreBlockBaseHandler {
name = 'AddonBlockActivityModules';
blockName = 'activity_modules';
constructor() {
super();
}
/**
* Returns the data needed to render the block.
*
* @param {Injector} injector Injector.
* @param {any} block The block to render.
* @param {string} contextLevel The context where the block will be used.
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise<CoreBlockHandlerData>} Data or promise resolved with the data.
*/
getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise<CoreBlockHandlerData> {
return {
title: 'addon.block_activitymodules.pluginname',
class: 'addon-block-activitymodules',
component: AddonBlockActivityModulesComponent
};
}
}

View File

@ -0,0 +1,47 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreCoursesComponentsModule } from '@core/courses/components/components.module';
import { AddonBlockMyOverviewComponent } from './myoverview/myoverview';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
@NgModule({
declarations: [
AddonBlockMyOverviewComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCoursesComponentsModule,
CoreCourseComponentsModule
],
providers: [
],
exports: [
AddonBlockMyOverviewComponent
],
entryComponents: [
AddonBlockMyOverviewComponent
]
})
export class AddonBlockMyOverviewComponentsModule {}

View File

@ -0,0 +1,44 @@
<ion-item-divider>
<h2>{{ 'addon.block_myoverview.pluginname' | translate }}</h2>
<!-- Download all courses. -->
<div *ngIf="downloadEnabled && courses[selectedFilter] && courses[selectedFilter].length > 1 && !showFilter" class="core-button-spinner" item-end>
<button *ngIf="prefetchCoursesData[selectedFilter].icon && prefetchCoursesData[selectedFilter].icon != 'spinner'" ion-button icon-only clear color="dark" (click)="prefetchCourses()">
<core-icon [name]="prefetchCoursesData[selectedFilter].icon"></core-icon>
</button>
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData[selectedFilter].badge">{{prefetchCoursesData[selectedFilter].badge}}</ion-badge>
<ion-spinner *ngIf="!prefetchCoursesData[selectedFilter].icon || prefetchCoursesData[selectedFilter].icon == 'spinner'"></ion-spinner>
</div>
<core-context-menu item-end>
<core-context-menu-item *ngIf="loaded && showFilterSwitchButton()" [priority]="1000" [content]="'core.courses.filtermycourses' | translate" (action)="switchFilter()" iconAction="funnel"></core-context-menu-item>
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="900" content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.title' | translate)}}" (action)="switchSort('fullname')" [iconAction]="sort == 'fullname' ? 'radio-button-on' : 'radio-button-off'"></core-context-menu-item>
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="800" content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.lastaccessed' | translate)}}" (action)="switchSort('lastaccess')" [iconAction]="sort == 'lastaccess' ? 'radio-button-on' : 'radio-button-off'"></core-context-menu-item>
</core-context-menu>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<div padding [hidden]="showFilter || !showSelectorFilter" class="safe-padding-horizontal">
<!-- "Time" selector. -->
<ion-select text-start [title]="'core.show' | translate" [(ngModel)]="selectedFilter" (ngModelChange)="selectedChanged()" interface="popover" class="core-button-select">
<ion-option value="all">{{ 'addon.block_myoverview.all' | translate }}</ion-option>
<ion-option value="inprogress">{{ 'addon.block_myoverview.inprogress' | translate }}</ion-option>
<ion-option value="future">{{ 'addon.block_myoverview.future' | translate }}</ion-option>
<ion-option value="past">{{ 'addon.block_myoverview.past' | translate }}</ion-option>
<ion-option value="favourite" *ngIf="showFavourite">{{ 'addon.block_myoverview.favourites' | translate }}</ion-option>
<ion-option value="hidden" *ngIf="showHidden">{{ 'addon.block_myoverview.hiddencourses' | translate }}</ion-option>
</ion-select>
</div>
<core-empty-box *ngIf="courses[selectedFilter].length == 0" image="assets/img/icons/courses.svg" [message]="'addon.block_myoverview.nocourses' | translate"></core-empty-box>
<!-- Filter courses. -->
<ion-searchbar #searchbar *ngIf="showFilter" [(ngModel)]="courses.filter" (ionInput)="filterChanged($event)" (ionCancel)="filterChanged()" [placeholder]="'core.courses.filtermycourses' | translate">
</ion-searchbar>
<!-- List of courses. -->
<div class="safe-area-page">
<ion-grid no-padding>
<ion-row no-padding>
<ion-col *ngFor="let course of filteredCourses" no-padding col-12 col-sm-6 col-md-6 col-lg-4 col-xl-4 align-self-stretch>
<core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true" [showDownload]="downloadEnabled"></core-courses-course-progress>
</ion-col>
</ion-row>
</ion-grid>
</div>
</core-loading>

View File

@ -0,0 +1,341 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, Input, OnDestroy, ViewChild, Injector } from '@angular/core';
import { Searchbar } from 'ionic-angular';
import { CoreEventsProvider } from '@providers/events';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreSitesProvider } from '@providers/sites';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreCoursesHelperProvider } from '@core/courses/providers/helper';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion';
import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component';
/**
* Component to render a my overview block.
*/
@Component({
selector: 'addon-block-myoverview',
templateUrl: 'addon-block-myoverview.html'
})
export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy {
@ViewChild('searchbar') searchbar: Searchbar;
@Input() downloadEnabled: boolean;
courses = {
filter: '',
all: [],
past: [],
inprogress: [],
future: [],
favourite: [],
hidden: []
};
selectedFilter = 'inprogress';
sort = 'fullname';
currentSite: any;
filteredCourses: any[];
prefetchCoursesData = {
all: {},
inprogress: {},
past: {},
future: {},
favourite: {},
hidden: {}
};
showFilter = false;
showFavourite = false;
showHidden = false;
showSelectorFilter = false;
showSortFilter = false;
protected prefetchIconsInitialized = false;
protected isDestroyed;
protected downloadButtonObserver;
protected coursesObserver;
protected courseIds = [];
protected fetchContentDefaultError = 'Error getting my overview data.';
constructor(injector: Injector, private coursesProvider: CoreCoursesProvider,
private courseCompletionProvider: AddonCourseCompletionProvider, private eventsProvider: CoreEventsProvider,
private courseHelper: CoreCourseHelperProvider, private utils: CoreUtilsProvider,
private courseOptionsDelegate: CoreCourseOptionsDelegate, private coursesHelper: CoreCoursesHelperProvider,
private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider) {
super(injector, 'AddonBlockMyOverviewComponent');
}
/**
* Component being initialized.
*/
ngOnInit(): void {
// Refresh the enabled flags if enabled.
this.downloadButtonObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED,
(data) => {
const wasEnabled = this.downloadEnabled;
this.downloadEnabled = data.enabled;
if (!wasEnabled && this.downloadEnabled && this.loaded) {
// Download all courses is enabled now, initialize it.
this.initPrefetchCoursesIcons();
}
});
this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => {
this.refreshContent();
}, this.sitesProvider.getCurrentSiteId());
this.currentSite = this.sitesProvider.getCurrentSite();
const promises = [];
promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewSort', this.sort).then((value) => {
this.sort = value;
}));
promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewFilter', this.selectedFilter).then((value) => {
this.selectedFilter = typeof this.courses[value] == 'undefined' ? 'inprogress' : value;
}));
Promise.all(promises).finally(() => {
super.ngOnInit();
});
}
/**
* Perform the invalidate content function.
*
* @return {Promise<any>} Resolved when done.
*/
protected invalidateContent(): Promise<any> {
const promises = [];
promises.push(this.coursesProvider.invalidateUserCourses().finally(() => {
// Invalidate course completion data.
promises.push(this.coursesProvider.invalidateUserCourses().finally(() => {
// Invalidate course completion data.
return this.utils.allPromises(this.courseIds.map((courseId) => {
return this.courseCompletionProvider.invalidateCourseCompletion(courseId);
}));
}));
}));
promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions());
if (this.courseIds.length > 0) {
promises.push(this.coursesProvider.invalidateCoursesByField('ids', this.courseIds.join(',')));
}
return this.utils.allPromises(promises).finally(() => {
this.prefetchIconsInitialized = false;
});
}
/**
* Fetch the courses for my overview.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchContent(): Promise<any> {
return this.coursesHelper.getUserCoursesWithOptions(this.sort).then((courses) => {
this.courseIds = courses.map((course) => {
return course.id;
});
this.showSortFilter = courses.length > 0 && typeof courses[0].lastaccess != 'undefined';
this.initCourseFilters(courses);
this.courses.filter = '';
this.showFilter = false;
this.showSelectorFilter = courses.length > 0 && (this.courses.past.length > 0 || this.courses.future.length > 0 ||
typeof courses[0].enddate != 'undefined');
this.showHidden = this.showSelectorFilter && typeof courses[0].hidden != 'undefined';
this.showFavourite = this.showSelectorFilter && typeof courses[0].isfavourite != 'undefined';
if (!this.showSelectorFilter) {
// No selector, show all.
this.selectedFilter = 'all';
}
this.filteredCourses = this.courses[this.selectedFilter];
this.initPrefetchCoursesIcons();
});
}
/**
* The filter has changed.
*
* @param {any} Received Event.
*/
filterChanged(event: any): void {
const newValue = event.target.value && event.target.value.trim().toLowerCase();
if (!newValue || !this.courses['all']) {
this.filteredCourses = this.courses['all'];
} else {
// Use displayname if avalaible, or fullname if not.
if (this.courses['all'].length > 0 &&
typeof this.courses['all'][0].displayname != 'undefined') {
this.filteredCourses = this.courses['all'].filter((course) => {
return course.displayname.toLowerCase().indexOf(newValue) > -1;
});
} else {
this.filteredCourses = this.courses['all'].filter((course) => {
return course.fullname.toLowerCase().indexOf(newValue) > -1;
});
}
}
}
/**
* Initialize the prefetch icon for selected courses.
*/
protected initPrefetchCoursesIcons(): void {
if (this.prefetchIconsInitialized || !this.downloadEnabled) {
// Already initialized.
return;
}
this.prefetchIconsInitialized = true;
Object.keys(this.prefetchCoursesData).forEach((filter) => {
this.courseHelper.initPrefetchCoursesIcons(this.courses[filter], this.prefetchCoursesData[filter]).then((prefetch) => {
this.prefetchCoursesData[filter] = prefetch;
});
});
}
/**
* Prefetch all the shown courses.
*
* @return {Promise<any>} Promise resolved when done.
*/
prefetchCourses(): Promise<any> {
const selected = this.selectedFilter,
initialIcon = this.prefetchCoursesData[selected].icon;
return this.courseHelper.prefetchCourses(this.courses[selected], this.prefetchCoursesData[selected]).catch((error) => {
if (!this.isDestroyed) {
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
this.prefetchCoursesData[selected].icon = initialIcon;
}
});
}
/**
* The selected courses filter have changed.
*/
selectedChanged(): void {
this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewFilter', this.selectedFilter);
this.filteredCourses = this.courses[this.selectedFilter];
}
/**
* Init courses filters.
*
* @param {any[]} courses Courses to filter.
*/
initCourseFilters(courses: any[]): void {
if (this.showSortFilter) {
if (this.sort == 'lastaccess') {
courses.sort((a, b) => {
return b.lastaccess - a.lastaccess;
});
} else if (this.sort == 'fullname') {
courses.sort((a, b) => {
const compareA = a.fullname.toLowerCase(),
compareB = b.fullname.toLowerCase();
return compareA.localeCompare(compareB);
});
}
}
this.courses.all = [];
this.courses.past = [];
this.courses.inprogress = [];
this.courses.future = [];
this.courses.favourite = [];
this.courses.hidden = [];
const today = this.timeUtils.timestamp();
courses.forEach((course) => {
if (course.hidden) {
this.courses.hidden.push(course);
} else {
this.courses.all.push(course);
if ((course.enddate && course.enddate < today) || course.completed) {
// Courses that have already ended.
this.courses.past.push(course);
} else if (course.startdate > today) {
// Courses that have not started yet.
this.courses.future.push(course);
} else {
// Courses still in progress.
this.courses.inprogress.push(course);
}
if (course.isfavourite) {
this.courses.favourite.push(course);
}
}
});
this.filteredCourses = this.courses[this.selectedFilter];
}
/**
* The selected courses sort filter have changed.
*
* @param {string} sort New sorting.
*/
switchSort(sort: string): void {
this.sort = sort;
this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewSort', this.sort);
this.initCourseFilters(this.courses.all.concat(this.courses.hidden));
}
/**
* Show or hide the filter.
*/
switchFilter(): void {
this.showFilter = !this.showFilter;
this.courses.filter = '';
this.filteredCourses = this.courses[this.showFilter ? 'all' : this.selectedFilter];
if (this.showFilter) {
setTimeout(() => {
this.searchbar.setFocus();
}, 500);
}
}
/**
* If switch button that enables the filter input is shown or not.
*
* @return {boolean} If switch button that enables the filter input is shown or not.
*/
showFilterSwitchButton(): boolean {
return this.loaded && this.courses['all'] && this.courses['all'].length > 5;
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
this.coursesObserver && this.coursesObserver.off();
this.downloadButtonObserver && this.downloadButtonObserver.off();
}
}

View File

@ -0,0 +1,13 @@
{
"all": "All",
"future": "Future",
"inprogress": "In progress",
"favourites" : "Starred",
"hiddencourses": "Hidden",
"lastaccessed": "Last accessed",
"morecourses": "More courses",
"nocourses": "No courses",
"past": "Past",
"pluginname": "Course overview",
"title": "Title"
}

View File

@ -0,0 +1,40 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreBlockDelegate } from '@core/block/providers/delegate';
import { AddonBlockMyOverviewComponentsModule } from './components/components.module';
import { AddonBlockMyOverviewHandler } from './providers/block-handler';
@NgModule({
declarations: [
],
imports: [
IonicModule,
AddonBlockMyOverviewComponentsModule,
TranslateModule.forChild()
],
exports: [
],
providers: [
AddonBlockMyOverviewHandler
]
})
export class AddonBlockMyOverviewModule {
constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockMyOverviewHandler) {
blockDelegate.registerHandler(blockHandler);
}
}

View File

@ -0,0 +1,62 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Injector } from '@angular/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreBlockHandlerData } from '@core/block/providers/delegate';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { AddonBlockMyOverviewComponent } from '../components/myoverview/myoverview';
import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
/**
* Block handler.
*/
@Injectable()
export class AddonBlockMyOverviewHandler extends CoreBlockBaseHandler {
name = 'AddonBlockMyOverview';
blockName = 'myoverview';
constructor(private coursesProvider: CoreCoursesProvider, private sitesProvider: CoreSitesProvider) {
super();
}
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan('3.6') ||
!this.coursesProvider.isMyCoursesDisabledInSite();
}
/**
* Returns the data needed to render the block.
*
* @param {Injector} injector Injector.
* @param {any} block The block to render.
* @param {string} contextLevel The context where the block will be used.
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise<CoreBlockHandlerData>} Data or promise resolved with the data.
*/
getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise<CoreBlockHandlerData> {
return {
title: 'addon.block_myoverview.pluginname',
class: 'addon-block-myoverview',
component: AddonBlockMyOverviewComponent
};
}
}

View File

@ -0,0 +1,45 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonBlockRecentlyAccessedCoursesComponent } from './recentlyaccessedcourses/recentlyaccessedcourses';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCoursesComponentsModule } from '@core/courses/components/components.module';
@NgModule({
declarations: [
AddonBlockRecentlyAccessedCoursesComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCoursesComponentsModule
],
providers: [
],
exports: [
AddonBlockRecentlyAccessedCoursesComponent
],
entryComponents: [
AddonBlockRecentlyAccessedCoursesComponent
]
})
export class AddonBlockRecentlyAccessedCoursesComponentsModule {}

View File

@ -0,0 +1,21 @@
<ion-item-divider>
<h2>{{ 'addon.block_recentlyaccessedcourses.pluginname' | translate }}</h2>
<div *ngIf="downloadEnabled && courses && courses.length > 1" class="core-button-spinner" item-end>
<button *ngIf="prefetchCoursesData.icon && prefetchCoursesData.icon != 'spinner'" ion-button icon-only clear color="dark" (click)="prefetchCourses()">
<core-icon [name]="prefetchCoursesData.icon"></core-icon>
</button>
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge">{{prefetchCoursesData.badge}}</ion-badge>
<ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.icon == 'spinner'"></ion-spinner>
</div>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" [message]="'addon.block_recentlyaccessedcourses.nocourses' | translate"></core-empty-box>
<!-- List of courses. -->
<div class="safe-area-page">
<div class="core-horizontal-scroll">
<ng-container *ngFor="let course of courses">
<core-courses-course-progress [course]="course" class="core-recentlyaccessedcourses" [showDownload]="downloadEnabled"></core-courses-course-progress>
</ng-container>
</div>
</div>
</core-loading>

View File

@ -0,0 +1,160 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, Injector, Input } from '@angular/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSitesProvider } from '@providers/sites';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreCoursesHelperProvider } from '@core/courses/providers/helper';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion';
import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component';
/**
* Component to render a recent courses block.
*/
@Component({
selector: 'addon-block-recentlyaccessedcourses',
templateUrl: 'addon-block-recentlyaccessedcourses.html'
})
export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy {
@Input() downloadEnabled: boolean;
courses = [];
prefetchCoursesData = {
icon: '',
badge: ''
};
protected prefetchIconsInitialized = false;
protected isDestroyed;
protected downloadButtonObserver;
protected coursesObserver;
protected courseIds = [];
protected fetchContentDefaultError = 'Error getting recent courses data.';
constructor(injector: Injector, private coursesProvider: CoreCoursesProvider,
private courseCompletionProvider: AddonCourseCompletionProvider, private eventsProvider: CoreEventsProvider,
private courseHelper: CoreCourseHelperProvider, private utils: CoreUtilsProvider,
private courseOptionsDelegate: CoreCourseOptionsDelegate, private coursesHelper: CoreCoursesHelperProvider,
private sitesProvider: CoreSitesProvider) {
super(injector, 'AddonBlockRecentlyAccessedCoursesComponent');
}
/**
* Component being initialized.
*/
ngOnInit(): void {
// Refresh the enabled flags if enabled.
this.downloadButtonObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED,
(data) => {
const wasEnabled = this.downloadEnabled;
this.downloadEnabled = data.enabled;
if (!wasEnabled && this.downloadEnabled && this.loaded) {
// Download all courses is enabled now, initialize it.
this.initPrefetchCoursesIcons();
}
});
this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => {
this.refreshContent();
}, this.sitesProvider.getCurrentSiteId());
super.ngOnInit();
}
/**
* Perform the invalidate content function.
*
* @return {Promise<any>} Resolved when done.
*/
protected invalidateContent(): Promise<any> {
const promises = [];
promises.push(this.coursesProvider.invalidateUserCourses().finally(() => {
// Invalidate course completion data.
return this.utils.allPromises(this.courseIds.map((courseId) => {
return this.courseCompletionProvider.invalidateCourseCompletion(courseId);
}));
}));
promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions());
if (this.courseIds.length > 0) {
promises.push(this.coursesProvider.invalidateCoursesByField('ids', this.courseIds.join(',')));
}
return this.utils.allPromises(promises).finally(() => {
this.prefetchIconsInitialized = false;
});
}
/**
* Fetch the courses for recent courses.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchContent(): Promise<any> {
return this.coursesHelper.getUserCoursesWithOptions('lastaccess', 10).then((courses) => {
this.courses = courses;
this.initPrefetchCoursesIcons();
});
}
/**
* Initialize the prefetch icon for selected courses.
*/
protected initPrefetchCoursesIcons(): void {
if (this.prefetchIconsInitialized || !this.downloadEnabled) {
// Already initialized.
return;
}
this.prefetchIconsInitialized = true;
this.courseHelper.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData).then((prefetch) => {
this.prefetchCoursesData = prefetch;
});
}
/**
* Prefetch all the shown courses.
*
* @return {Promise<any>} Promise resolved when done.
*/
prefetchCourses(): Promise<any> {
const initialIcon = this.prefetchCoursesData.icon;
return this.courseHelper.prefetchCourses(this.courses, this.prefetchCoursesData).catch((error) => {
if (!this.isDestroyed) {
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
this.prefetchCoursesData.icon = initialIcon;
}
});
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
this.coursesObserver && this.coursesObserver.off();
this.downloadButtonObserver && this.downloadButtonObserver.off();
}
}

View File

@ -0,0 +1,4 @@
{
"nocourses": "No recent courses",
"pluginname": "Recently accessed courses"
}

View File

@ -0,0 +1,50 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Injector } from '@angular/core';
import { CoreBlockHandlerData } from '@core/block/providers/delegate';
import { AddonBlockRecentlyAccessedCoursesComponent } from '../components/recentlyaccessedcourses/recentlyaccessedcourses';
import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
/**
* Block handler.
*/
@Injectable()
export class AddonBlockRecentlyAccessedCoursesHandler extends CoreBlockBaseHandler {
name = 'AddonBlockRecentlyAccessedCourses';
blockName = 'recentlyaccessedcourses';
constructor() {
super();
}
/**
* Returns the data needed to render the block.
*
* @param {Injector} injector Injector.
* @param {any} block The block to render.
* @param {string} contextLevel The context where the block will be used.
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise<CoreBlockHandlerData>} Data or promise resolved with the data.
*/
getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise<CoreBlockHandlerData> {
return {
title: 'addon.block_recentlyaccessedcourses.pluginname',
class: 'addon-block-recentlyaccessedcourses',
component: AddonBlockRecentlyAccessedCoursesComponent
};
}
}

View File

@ -0,0 +1,40 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreBlockDelegate } from '@core/block/providers/delegate';
import { AddonBlockRecentlyAccessedCoursesComponentsModule } from './components/components.module';
import { AddonBlockRecentlyAccessedCoursesHandler } from './providers/block-handler';
@NgModule({
declarations: [
],
imports: [
IonicModule,
CoreComponentsModule,
AddonBlockRecentlyAccessedCoursesComponentsModule,
TranslateModule.forChild()
],
providers: [
AddonBlockRecentlyAccessedCoursesHandler
]
})
export class AddonBlockRecentlyAccessedCoursesModule {
constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockRecentlyAccessedCoursesHandler) {
blockDelegate.registerHandler(blockHandler);
}
}

View File

@ -0,0 +1,45 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonBlockRecentlyAccessedItemsComponent } from './recentlyaccesseditems/recentlyaccesseditems';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
@NgModule({
declarations: [
AddonBlockRecentlyAccessedItemsComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCourseComponentsModule
],
providers: [
],
exports: [
AddonBlockRecentlyAccessedItemsComponent
],
entryComponents: [
AddonBlockRecentlyAccessedItemsComponent
]
})
export class AddonBlockRecentlyAccessedItemsComponentsModule {}

View File

@ -0,0 +1,19 @@
<ion-item-divider>
<h2>{{ 'addon.block_recentlyaccesseditems.pluginname' | translate }}</h2>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<div class="core-horizontal-scroll" *ngIf="items && items.length > 0">
<div *ngFor="let item of items">
<ion-card>
<a ion-item text-wrap detail-none class="core-course-module-handler item-media" (click)="action($event, item)" [title]="item.name">
<img item-start [src]="item.iconUrl" alt="" role="presentation" *ngIf="item.iconUrl" class="core-module-icon">
<h2><core-format-text [text]="item.name"></core-format-text></h2>
<p><core-format-text [text]="item.coursename"></core-format-text></p>
</a>
</ion-card>
</div>
</div>
<core-empty-box *ngIf="items.length <= 0" image="assets/img/icons/activities.svg" [message]="'addon.block_recentlyaccesseditems.noitems' | translate"></core-empty-box>
</core-loading>

View File

@ -0,0 +1,3 @@
ion-app.app-root addon-block-recentlyaccesseditems .core-horizontal-scroll > div {
@include horizontal_scroll_item(80%, 250px, 300px);
}

View File

@ -0,0 +1,90 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, Injector, Optional } from '@angular/core';
import { NavController } from 'ionic-angular';
import { CoreSitesProvider } from '@providers/sites';
import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component';
import { AddonBlockRecentlyAccessedItemsProvider } from '../../providers/recentlyaccesseditems';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
/**
* Component to render a recently accessed items block.
*/
@Component({
selector: 'addon-block-recentlyaccesseditems',
templateUrl: 'addon-block-recentlyaccesseditems.html'
})
export class AddonBlockRecentlyAccessedItemsComponent extends CoreBlockBaseComponent implements OnInit {
items = [];
protected fetchContentDefaultError = 'Error getting recently accessed items data.';
constructor(injector: Injector, @Optional() private navCtrl: NavController,
private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
private recentItemsProvider: AddonBlockRecentlyAccessedItemsProvider,
private contentLinksHelper: CoreContentLinksHelperProvider) {
super(injector, 'AddonBlockRecentlyAccessedItemsComponent');
}
/**
* Component being initialized.
*/
ngOnInit(): void {
super.ngOnInit();
}
/**
* Perform the invalidate content function.
*
* @return {Promise<any>} Resolved when done.
*/
protected invalidateContent(): Promise<any> {
return this.recentItemsProvider.invalidateRecentItems();
}
/**
* Fetch the data to render the block.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchContent(): Promise<any> {
return this.recentItemsProvider.getRecentItems().then((items) => {
this.items = items;
});
}
/**
* Event clicked.
*
* @param {Event} e Click event.
* @param {any} item Activity item info.
*/
action(e: Event, item: any): void {
e.preventDefault();
e.stopPropagation();
const url = this.textUtils.decodeHTMLEntities(item.viewurl);
const modal = this.domUtils.showModalLoading();
this.contentLinksHelper.handleLink(url, undefined, this.navCtrl).then((treated) => {
if (!treated) {
return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url);
}
}).finally(() => {
modal.dismiss();
});
}
}

View File

@ -0,0 +1,4 @@
{
"noitems": "No recent items",
"pluginname": "Recently accessed items"
}

View File

@ -0,0 +1,50 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Injector } from '@angular/core';
import { CoreBlockHandlerData } from '@core/block/providers/delegate';
import { AddonBlockRecentlyAccessedItemsComponent } from '../components/recentlyaccesseditems/recentlyaccesseditems';
import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
/**
* Block handler.
*/
@Injectable()
export class AddonBlockRecentlyAccessedItemsHandler extends CoreBlockBaseHandler {
name = 'AddonBlockRecentlyAccessedItems';
blockName = 'recentlyaccesseditems';
constructor() {
super();
}
/**
* Returns the data needed to render the block.
*
* @param {Injector} injector Injector.
* @param {any} block The block to render.
* @param {string} contextLevel The context where the block will be used.
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise<CoreBlockHandlerData>} Data or promise resolved with the data.
*/
getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise<CoreBlockHandlerData> {
return {
title: 'addon.block_recentlyaccesseditems.pluginname',
class: 'addon-block-recentlyaccesseditems',
component: AddonBlockRecentlyAccessedItemsComponent
};
}
}

View File

@ -0,0 +1,74 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
* Service that provides some features regarding recently accessed items.
*/
@Injectable()
export class AddonBlockRecentlyAccessedItemsProvider {
protected ROOT_CACHE_KEY = 'AddonBlockRecentlyAccessedItems:';
constructor(private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider,
private domUtils: CoreDomUtilsProvider) { }
/**
* Get cache key for get last accessed items value WS call.
*
* @return {string} Cache key.
*/
protected getRecentItemsCacheKey(): string {
return this.ROOT_CACHE_KEY + ':recentitems';
}
/**
* Get last accessed items.
*
* @param {string} [siteId] Site ID. If not defined, use current site.
* @return {Promise<any[]>} Promise resolved when the info is retrieved.
*/
getRecentItems(siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
const preSets = {
cacheKey: this.getRecentItemsCacheKey()
};
return site.read('block_recentlyaccesseditems_get_recent_items', undefined, preSets).then((items) => {
return items.map((item) => {
const modicon = item.icon && this.domUtils.getHTMLElementAttribute(item.icon, 'src');
item.iconUrl = this.courseProvider.getModuleIconSrc(item.modname, modicon);
return item;
});
});
});
}
/**
* Invalidates get last accessed items WS call.
*
* @param {string} [siteId] Site ID to invalidate. If not defined, use current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateRecentItems(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getRecentItemsCacheKey());
});
}
}

View File

@ -0,0 +1,42 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonBlockRecentlyAccessedItemsComponentsModule } from './components/components.module';
import { CoreBlockDelegate } from '@core/block/providers/delegate';
import { AddonBlockRecentlyAccessedItemsHandler } from './providers/block-handler';
import { AddonBlockRecentlyAccessedItemsProvider } from './providers/recentlyaccesseditems';
@NgModule({
declarations: [
],
imports: [
IonicModule,
AddonBlockRecentlyAccessedItemsComponentsModule,
TranslateModule.forChild()
],
exports: [
],
providers: [
AddonBlockRecentlyAccessedItemsHandler,
AddonBlockRecentlyAccessedItemsProvider
]
})
export class AddonBlockRecentlyAccessedItemsModule {
constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockRecentlyAccessedItemsHandler) {
blockDelegate.registerHandler(blockHandler);
}
}

View File

@ -0,0 +1,45 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonBlockSiteMainMenuComponent } from './sitemainmenu/sitemainmenu';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
@NgModule({
declarations: [
AddonBlockSiteMainMenuComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCourseComponentsModule
],
providers: [
],
exports: [
AddonBlockSiteMainMenuComponent
],
entryComponents: [
AddonBlockSiteMainMenuComponent
]
})
export class AddonBlockSiteMainMenuComponentsModule {}

View File

@ -0,0 +1,10 @@
<ion-item-divider>
<h2>{{ 'addon.block_sitemainmenu.pluginname' | translate }}</h2>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<ion-item text-wrap *ngIf="block.summary">
<core-format-text [text]="block.summary"></core-format-text>
</ion-item>
<core-course-module *ngFor="let module of block.modules" [module]="module" [courseId]="siteHomeId" [downloadEnabled]="true" [section]="block"></core-course-module>
</core-loading>

View File

@ -0,0 +1,114 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, Injector } from '@angular/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { CoreSiteHomeProvider } from '@core/sitehome/providers/sitehome';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component';
/**
* Component to render a site main menu block.
*/
@Component({
selector: 'addon-block-sitemainmenu',
templateUrl: 'addon-block-sitemainmenu.html'
})
export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent implements OnInit {
block: any;
siteHomeId: number;
protected fetchContentDefaultError = 'Error getting main menu data.';
constructor(injector: Injector, protected sitesProvider: CoreSitesProvider, protected courseProvider: CoreCourseProvider,
protected courseHelper: CoreCourseHelperProvider, protected siteHomeProvider: CoreSiteHomeProvider,
protected prefetchDelegate: CoreCourseModulePrefetchDelegate) {
super(injector, 'AddonBlockSiteMainMenuComponent');
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
}
/**
* Component being initialized.
*/
ngOnInit(): void {
super.ngOnInit();
}
/**
* Perform the invalidate content function.
*
* @return {Promise<any>} Resolved when done.
*/
protected invalidateContent(): Promise<any> {
const promises = [];
promises.push(this.courseProvider.invalidateSections(this.siteHomeId));
promises.push(this.siteHomeProvider.invalidateNewsForum(this.siteHomeId));
if (this.block && this.block.modules) {
// Invalidate modules prefetch data.
promises.push(this.prefetchDelegate.invalidateModules(this.block.modules, this.siteHomeId));
}
return Promise.all(promises);
}
/**
* Fetch the data to render the block.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchContent(): Promise<any> {
return this.courseProvider.getSections(this.siteHomeId, false, true).then((sections) => {
this.block = sections[0];
if (this.block) {
this.block.hasContent = this.courseHelper.sectionHasContent(this.block);
this.courseHelper.addHandlerDataForModules([this.block], this.siteHomeId);
// Check if Site Home displays announcements. If so, remove it from the main menu block.
const currentSite = this.sitesProvider.getCurrentSite(),
config = currentSite ? currentSite.getStoredConfig() || {} : {};
let hasNewsItem = false;
if (config.frontpageloggedin) {
const items = config.frontpageloggedin.split(',');
hasNewsItem = items.find((item) => { return item == '0'; });
}
if (hasNewsItem && this.block.modules) {
// Remove forum activity (news one only) from the main menu block to prevent duplicates.
return this.siteHomeProvider.getNewsForum(this.siteHomeId).then((forum) => {
// Search the module that belongs to site news.
for (let i = 0; i < this.block.modules.length; i++) {
const module = this.block.modules[i];
if (module.modname == 'forum' && module.instance == forum.id) {
this.block.modules.splice(i, 1);
break;
}
}
}).catch(() => {
// Ignore errors.
});
}
}
});
}
}

View File

@ -0,0 +1,3 @@
{
"pluginname": "Main menu"
}

View File

@ -0,0 +1,50 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Injector } from '@angular/core';
import { CoreBlockHandlerData } from '@core/block/providers/delegate';
import { AddonBlockSiteMainMenuComponent } from '../components/sitemainmenu/sitemainmenu';
import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
/**
* Block handler.
*/
@Injectable()
export class AddonBlockSiteMainMenuHandler extends CoreBlockBaseHandler {
name = 'AddonBlockSiteMainMenu';
blockName = 'site_main_menu';
constructor() {
super();
}
/**
* Returns the data needed to render the block.
*
* @param {Injector} injector Injector.
* @param {any} block The block to render.
* @param {string} contextLevel The context where the block will be used.
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise<CoreBlockHandlerData>} Data or promise resolved with the data.
*/
getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise<CoreBlockHandlerData> {
return {
title: 'addon.block_sitemainmenu.pluginname',
class: 'addon-block-sitemainmenu',
component: AddonBlockSiteMainMenuComponent
};
}
}

View File

@ -0,0 +1,44 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonBlockSiteMainMenuComponentsModule } from './components/components.module';
import { CoreBlockDelegate } from '@core/block/providers/delegate';
import { AddonBlockSiteMainMenuHandler } from './providers/block-handler';
@NgModule({
declarations: [
],
imports: [
IonicModule,
CoreComponentsModule,
CoreDirectivesModule,
AddonBlockSiteMainMenuComponentsModule,
TranslateModule.forChild()
],
exports: [
],
providers: [
AddonBlockSiteMainMenuHandler
]
})
export class AddonBlockSiteMainMenuModule {
constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockSiteMainMenuHandler) {
blockDelegate.registerHandler(blockHandler);
}
}

View File

@ -0,0 +1,45 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonBlockStarredCoursesComponent } from './starredcourses/starredcourses';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCoursesComponentsModule } from '@core/courses/components/components.module';
@NgModule({
declarations: [
AddonBlockStarredCoursesComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCoursesComponentsModule
],
providers: [
],
exports: [
AddonBlockStarredCoursesComponent
],
entryComponents: [
AddonBlockStarredCoursesComponent
]
})
export class AddonBlockStarredCoursesComponentsModule {}

View File

@ -0,0 +1,21 @@
<ion-item-divider>
<h2>{{ 'addon.block_starredcourses.pluginname' | translate }}</h2>
<div *ngIf="downloadEnabled && courses && courses.length > 1" class="core-button-spinner" item-end>
<button *ngIf="prefetchCoursesData.icon && prefetchCoursesData.icon != 'spinner'" ion-button icon-only clear color="dark" (click)="prefetchCourses()">
<core-icon [name]="prefetchCoursesData.icon"></core-icon>
</button>
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge">{{prefetchCoursesData.badge}}</ion-badge>
<ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.icon == 'spinner'"></ion-spinner>
</div>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" [message]="'addon.block_starredcourses.nocourses' | translate"></core-empty-box>
<!-- List of courses. -->
<div class="safe-area-page">
<div class="core-horizontal-scroll">
<ng-container *ngFor="let course of courses">
<core-courses-course-progress [course]="course" class="core-block_starredcourses" [showDownload]="downloadEnabled"></core-courses-course-progress>
</ng-container>
</div>
</div>
</core-loading>

View File

@ -0,0 +1,160 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, Injector, Input } from '@angular/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSitesProvider } from '@providers/sites';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreCoursesHelperProvider } from '@core/courses/providers/helper';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion';
import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component';
/**
* Component to render a starred courses block.
*/
@Component({
selector: 'addon-block-starredcourses',
templateUrl: 'addon-block-starredcourses.html'
})
export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy {
@Input() downloadEnabled: boolean;
courses = [];
prefetchCoursesData = {
icon: '',
badge: ''
};
protected prefetchIconsInitialized = false;
protected isDestroyed;
protected downloadButtonObserver;
protected coursesObserver;
protected courseIds = [];
protected fetchContentDefaultError = 'Error getting starred courses data.';
constructor(injector: Injector, private coursesProvider: CoreCoursesProvider,
private courseCompletionProvider: AddonCourseCompletionProvider, private eventsProvider: CoreEventsProvider,
private courseHelper: CoreCourseHelperProvider, private utils: CoreUtilsProvider,
private courseOptionsDelegate: CoreCourseOptionsDelegate, private coursesHelper: CoreCoursesHelperProvider,
private sitesProvider: CoreSitesProvider) {
super(injector, 'AddonBlockStarredCoursesComponent');
}
/**
* Component being initialized.
*/
ngOnInit(): void {
// Refresh the enabled flags if enabled.
this.downloadButtonObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED,
(data) => {
const wasEnabled = this.downloadEnabled;
this.downloadEnabled = data.enabled;
if (!wasEnabled && this.downloadEnabled && this.loaded) {
// Download all courses is enabled now, initialize it.
this.initPrefetchCoursesIcons();
}
});
this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => {
this.refreshContent();
}, this.sitesProvider.getCurrentSiteId());
super.ngOnInit();
}
/**
* Perform the invalidate content function.
*
* @return {Promise<any>} Resolved when done.
*/
protected invalidateContent(): Promise<any> {
const promises = [];
promises.push(this.coursesProvider.invalidateUserCourses().finally(() => {
// Invalidate course completion data.
return this.utils.allPromises(this.courseIds.map((courseId) => {
return this.courseCompletionProvider.invalidateCourseCompletion(courseId);
}));
}));
promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions());
if (this.courseIds.length > 0) {
promises.push(this.coursesProvider.invalidateCoursesByField('ids', this.courseIds.join(',')));
}
return this.utils.allPromises(promises).finally(() => {
this.prefetchIconsInitialized = false;
});
}
/**
* Fetch the courses.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchContent(): Promise<any> {
return this.coursesHelper.getUserCoursesWithOptions('timemodified', 0, 'isfavourite').then((courses) => {
this.courses = courses;
this.initPrefetchCoursesIcons();
});
}
/**
* Initialize the prefetch icon for selected courses.
*/
protected initPrefetchCoursesIcons(): void {
if (this.prefetchIconsInitialized || !this.downloadEnabled) {
// Already initialized.
return;
}
this.prefetchIconsInitialized = true;
this.courseHelper.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData).then((prefetch) => {
this.prefetchCoursesData = prefetch;
});
}
/**
* Prefetch all the shown courses.
*
* @return {Promise<any>} Promise resolved when done.
*/
prefetchCourses(): Promise<any> {
const initialIcon = this.prefetchCoursesData.icon;
return this.courseHelper.prefetchCourses(this.courses, this.prefetchCoursesData).catch((error) => {
if (!this.isDestroyed) {
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
this.prefetchCoursesData.icon = initialIcon;
}
});
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
this.coursesObserver && this.coursesObserver.off();
this.downloadButtonObserver && this.downloadButtonObserver.off();
}
}

View File

@ -0,0 +1,4 @@
{
"nocourses": "No starred courses",
"pluginname": "Starred courses"
}

View File

@ -0,0 +1,50 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Injector } from '@angular/core';
import { CoreBlockHandlerData } from '@core/block/providers/delegate';
import { AddonBlockStarredCoursesComponent } from '../components/starredcourses/starredcourses';
import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
/**
* Block handler.
*/
@Injectable()
export class AddonBlockStarredCoursesHandler extends CoreBlockBaseHandler {
name = 'AddonBlockStarredCourses';
blockName = 'starredcourses';
constructor() {
super();
}
/**
* Returns the data needed to render the block.
*
* @param {Injector} injector Injector.
* @param {any} block The block to render.
* @param {string} contextLevel The context where the block will be used.
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise<CoreBlockHandlerData>} Data or promise resolved with the data.
*/
getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise<CoreBlockHandlerData> {
return {
title: 'addon.starredcourses.pluginname',
class: 'addon-block-starredcourses',
component: AddonBlockStarredCoursesComponent
};
}
}

View File

@ -0,0 +1,40 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreBlockDelegate } from '@core/block/providers/delegate';
import { AddonBlockStarredCoursesComponentsModule } from './components/components.module';
import { AddonBlockStarredCoursesHandler } from './providers/block-handler';
@NgModule({
declarations: [
],
imports: [
IonicModule,
CoreComponentsModule,
AddonBlockStarredCoursesComponentsModule,
TranslateModule.forChild()
],
providers: [
AddonBlockStarredCoursesHandler
]
})
export class AddonBlockStarredCoursesModule {
constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockStarredCoursesHandler) {
blockDelegate.registerHandler(blockHandler);
}
}

View File

@ -0,0 +1,53 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreCoursesComponentsModule } from '@core/courses/components/components.module';
import { AddonBlockTimelineComponent } from './timeline/timeline';
import { AddonBlockTimelineEventsComponent } from './events/events';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
@NgModule({
declarations: [
AddonBlockTimelineComponent,
AddonBlockTimelineEventsComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
CoreCoursesComponentsModule,
CoreCourseComponentsModule
],
providers: [
],
exports: [
AddonBlockTimelineComponent,
AddonBlockTimelineEventsComponent
],
entryComponents: [
AddonBlockTimelineComponent,
AddonBlockTimelineEventsComponent
]
})
export class AddonBlockTimelineComponentsModule {}

View File

@ -0,0 +1,36 @@
<ion-item-group *ngFor="let dayEvents of filteredEvents">
<ion-item-divider [color]="dayEvents.color">
<h2>{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h2>
</ion-item-divider>
<ng-container *ngFor="let event of dayEvents.events">
<a ion-item text-wrap detail-none class="core-course-module-handler item-media" (click)="action($event, event.url)" [title]="event.name">
<img item-start [src]="event.iconUrl" core-external-content alt="" role="presentation" *ngIf="event.iconUrl" class="core-module-icon">
<h2><core-format-text [text]="event.name"></core-format-text></h2>
<p *ngIf="showCourse">
<core-format-text [text]="event.course.fullnamedisplay"></core-format-text>
</p>
<button ion-button clear class="hidden-tablet" (click)="action($event, event.action.url)" [title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action">
{{event.action.name}}
<ion-badge item-end margin-start *ngIf="event.action.showitemcount">{{event.action.itemcount}}</ion-badge>
</button>
<ion-badge color="light" item-end>{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge>
<button ion-button clear item-end class="hidden-phone" (click)="action($event, event.action.url)" [title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action">
{{event.action.name}}
<ion-badge item-end margin-start *ngIf="event.action.showitemcount">{{event.action.itemcount}}</ion-badge>
</button>
</a>
</ng-container>
</ion-item-group>
<div padding text-center *ngIf="canLoadMore && !empty">
<!-- Button and spinner to show more attempts. -->
<button ion-button block (click)="loadMoreEvents()" color="light" *ngIf="!loadingMore">
{{ 'core.loadmore' | translate }}
</button>
<ion-spinner *ngIf="loadingMore"></ion-spinner>
</div>
<core-empty-box *ngIf="empty" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate" [inline]="!showCourse"></core-empty-box>

Some files were not shown because too many files have changed in this diff Show More