+
+
diff --git a/src/components/file/file.scss b/src/components/file/file.scss
new file mode 100644
index 000000000..95127e438
--- /dev/null
+++ b/src/components/file/file.scss
@@ -0,0 +1,2 @@
+core-file {
+}
\ No newline at end of file
diff --git a/src/components/file/file.ts b/src/components/file/file.ts
new file mode 100644
index 000000000..0506bea4f
--- /dev/null
+++ b/src/components/file/file.ts
@@ -0,0 +1,291 @@
+// (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, Input, Output, OnInit, OnDestroy, EventEmitter } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { CoreAppProvider } from '../../providers/app';
+import { CoreEventsProvider } from '../../providers/events';
+import { CoreFileProvider } from '../../providers/file';
+import { CoreFilepoolProvider } from '../../providers/filepool';
+import { CoreSitesProvider } from '../../providers/sites';
+import { CoreDomUtilsProvider } from '../../providers/utils/dom';
+import { CoreMimetypeUtilsProvider } from '../../providers/utils/mimetype';
+import { CoreUtilsProvider } from '../../providers/utils/utils';
+import { CoreConstants } from '../../core/constants';
+
+/**
+ * Component to handle a remote file. Shows the file name, icon (depending on mimetype) and a button
+ * to download/refresh it.
+ */
+@Component({
+ selector: 'core-file',
+ templateUrl: 'file.html'
+})
+export class CoreFileComponent implements OnInit, OnDestroy {
+ @Input() file: any; // The file. Must have a property 'filename' and a 'fileurl' or 'url'
+ @Input() component?: string; // Component the file belongs to.
+ @Input() componentId?: string|number; // Component ID.
+ @Input() timemodified?: number; // If set, the value will be used to check if the file is outdated.
+ @Input() canDelete?: boolean|string; // Whether file can be deleted.
+ @Input() alwaysDownload?: boolean|string; // Whether it should always display the refresh button when the file is downloaded.
+ // Use it for files that you cannot determine if they're outdated or not.
+ @Input() canDownload?: boolean|string = true; // Whether file can be downloaded.
+ @Output() onDelete?: EventEmitter; // Will notify when the delete button is clicked.
+
+ isDownloaded: boolean;
+ isDownloading: boolean;
+ showDownload: boolean;
+ fileIcon: string;
+ fileName: string;
+
+ protected fileUrl: string;
+ protected siteId: string;
+ protected fileSize: number;
+ protected observer;
+
+ constructor(private translate: TranslateService, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider,
+ private domUtils: CoreDomUtilsProvider, private filepoolProvider: CoreFilepoolProvider,
+ private fileProvider: CoreFileProvider, private appProvider: CoreAppProvider,
+ private mimeUtils: CoreMimetypeUtilsProvider, private eventsProvider: CoreEventsProvider) {
+ this.onDelete = new EventEmitter();
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit() {
+ this.canDelete = this.utils.isTrueOrOne(this.canDelete);
+ this.alwaysDownload = this.utils.isTrueOrOne(this.alwaysDownload);
+ this.canDownload = this.utils.isTrueOrOne(this.canDownload);
+ this.timemodified = this.timemodified || 0;
+
+ this.fileUrl = this.file.fileurl || this.file.url;
+ this.siteId = this.sitesProvider.getCurrentSiteId();
+ this.fileSize = this.file.filesize;
+ this.fileName = this.file.filename;
+
+ if (this.file.isexternalfile) {
+ this.alwaysDownload = true; // Always show the download button in external files.
+ }
+
+ this.fileIcon = this.mimeUtils.getFileIcon(this.file.filename);
+
+ if (this.canDownload) {
+ this.calculateState();
+
+ // Update state when receiving events about this file.
+ this.filepoolProvider.getFileEventNameByUrl(this.siteId, this.fileUrl).then((eventName) => {
+ this.observer = this.eventsProvider.on(eventName, () => {
+ this.calculateState();
+ });
+ });
+ }
+ }
+
+ /**
+ * Convenience function to get the file state and set variables based on it.
+ *
+ * @return {Promise} Promise resolved when state has been calculated.
+ */
+ protected calculateState() : Promise {
+ return this.filepoolProvider.getFileStateByUrl(this.siteId, this.fileUrl, this.timemodified).then((state) => {
+ let canDownload = this.sitesProvider.getCurrentSite().canDownloadFiles();
+
+ this.isDownloaded = state === CoreConstants.downloaded || state === CoreConstants.outdated;
+ this.isDownloading = canDownload && state === CoreConstants.downloading;
+ this.showDownload = canDownload && (state === CoreConstants.notDownloaded || state === CoreConstants.outdated ||
+ (this.alwaysDownload && state === CoreConstants.downloaded));
+ });
+ }
+
+ /**
+ * Download the file.
+ *
+ * @return {Promise} Promise resolved when file is downloaded.
+ */
+ protected downloadFile() : Promise {
+ if (!this.sitesProvider.getCurrentSite().canDownloadFiles()) {
+ this.domUtils.showErrorModal('core.cannotdownloadfiles', true);
+ return Promise.reject(null);
+ }
+
+ this.isDownloading = true;
+ return this.filepoolProvider.downloadUrl(this.siteId, this.fileUrl, false, this.component, this.componentId,
+ this.timemodified, undefined, undefined, this.file).catch(() => {
+
+ // Call calculateState to make sure we have the right state.
+ return this.calculateState().then(() => {
+ if (this.isDownloaded) {
+ return this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.fileUrl);
+ } else {
+ return Promise.reject(null);
+ }
+ });
+ });
+ }
+
+ /**
+ * Convenience function to open a file, downloading it if needed.
+ *
+ * @return {Promise} Promise resolved when file is opened.
+ */
+ protected openFile() : Promise {
+ let fixedUrl = this.sitesProvider.getCurrentSite().fixPluginfileURL(this.fileUrl),
+ promise;
+
+ if (this.fileProvider.isAvailable()) {
+ promise = Promise.resolve().then(() => {
+ // The file system is available.
+ let isWifi = !this.appProvider.isNetworkAccessLimited(),
+ isOnline = this.appProvider.isOnline();
+
+ if (this.isDownloaded && !this.showDownload) {
+ // File is downloaded, get the local file URL.
+ return this.filepoolProvider.getUrlByUrl(this.siteId, this.fileUrl,
+ this.component, this.componentId, this.timemodified, false, false, this.file);
+ } else {
+ if (!isOnline && !this.isDownloaded) {
+ // Not downloaded and user is offline, reject.
+ return Promise.reject(this.translate.instant('core.networkerrormsg'));
+ }
+
+ let isDownloading = this.isDownloading;
+ this.isDownloading = true; // This check could take a while, show spinner.
+ return this.filepoolProvider.shouldDownloadBeforeOpen(fixedUrl, this.fileSize).then(() => {
+ if (isDownloading) {
+ // It's already downloading, stop.
+ return;
+ }
+ // Download and then return the local URL.
+ return this.downloadFile();
+ }, () => {
+ // Start the download if in wifi, but return the URL right away so the file is opened.
+ if (isWifi && isOnline) {
+ this.downloadFile();
+ }
+
+ if (isDownloading || !this.isDownloaded || isOnline) {
+ // Not downloaded or outdated and online, return the online URL.
+ return fixedUrl;
+ } else {
+ // Outdated but offline, so we return the local URL.
+ return this.filepoolProvider.getUrlByUrl(this.siteId, this.fileUrl,
+ this.component, this.componentId, this.timemodified, false, false, this.file);
+ }
+ });
+ }
+ });
+ } else {
+ // Use the online URL.
+ promise = Promise.resolve(fixedUrl);
+ }
+
+ return promise.then((url) => {
+ if (!url) {
+ return;
+ }
+
+ if (url.indexOf('http') === 0) {
+ return this.utils.openOnlineFile(url).catch((error) => {
+ // Error opening the file, some apps don't allow opening online files.
+ if (!this.fileProvider.isAvailable()) {
+ return Promise.reject(error);
+ } else if (this.isDownloading) {
+ return Promise.reject(this.translate.instant('core.erroropenfiledownloading'));
+ }
+
+ let subPromise;
+
+ if (status === CoreConstants.notDownloaded) {
+ // File is not downloaded, download and then return the local URL.
+ subPromise = this.downloadFile();
+ } else {
+ // File is outdated and can't be opened in online, return the local URL.
+ subPromise = this.filepoolProvider.getInternalUrlByUrl(this.siteId, this.fileUrl);
+ }
+
+ return subPromise.then((url) => {
+ return this.utils.openFile(url);
+ });
+ });
+ } else {
+ return this.utils.openFile(url);
+ }
+ });
+ }
+
+ /**
+ * Download a file and, optionally, open it afterwards.
+ *
+ * @param {Event} e Click event.
+ * @param {boolean} openAfterDownload Whether the file should be opened after download.
+ */
+ download(e: Event, openAfterDownload: boolean) : void {
+ e.preventDefault();
+ e.stopPropagation();
+
+ let promise;
+
+ if (this.isDownloading && !openAfterDownload) {
+ return;
+ }
+
+ if (!this.appProvider.isOnline() && (!openAfterDownload || (openAfterDownload && !this.isDownloaded))) {
+ this.domUtils.showErrorModal('core.networkerrormsg', true);
+ return;
+ }
+
+ if (openAfterDownload) {
+ // File needs to be opened now.
+ this.openFile().catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
+ });
+ } else {
+ // File doesn't need to be opened (it's a prefetch). Show confirm modal if file size is defined and it's big.
+ promise = this.fileSize ? this.domUtils.confirmDownloadSize({size: this.fileSize, total: true}) : Promise.resolve();
+ promise.then(() => {
+ // User confirmed, add the file to queue.
+ this.filepoolProvider.invalidateFileByUrl(this.siteId, this.fileUrl).finally(() => {
+ this.isDownloading = true;
+ this.filepoolProvider.addToQueueByUrl(this.siteId, this.fileUrl, this.component,
+ this.componentId, this.timemodified, undefined, undefined, 0, this.file).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
+ this.calculateState();
+ });
+ });
+ });
+ }
+ };
+
+ /**
+ * Delete the file.
+ *
+ * @param {Event} e Click event.
+ */
+ deleteFile(e: Event) : void {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this.canDelete) {
+ this.onDelete.emit();
+ }
+ }
+
+ /**
+ * Component destroyed.
+ */
+ ngOnDestroy() {
+ this.observer && this.observer.off();
+ }
+}
diff --git a/src/components/iframe/iframe.html b/src/components/iframe/iframe.html
index 75cea2664..69ace4cf8 100644
--- a/src/components/iframe/iframe.html
+++ b/src/components/iframe/iframe.html
@@ -1,4 +1,4 @@
-
-
+
+
\ No newline at end of file
diff --git a/src/components/loading/loading.html b/src/components/loading/loading.html
index e79daa1ff..d6a224a91 100644
--- a/src/components/loading/loading.html
+++ b/src/components/loading/loading.html
@@ -1,8 +1,9 @@
-
+
+
+
diff --git a/src/core/courses/components/overview-events/overview-events.scss b/src/core/courses/components/overview-events/overview-events.scss
new file mode 100644
index 000000000..4adcffdff
--- /dev/null
+++ b/src/core/courses/components/overview-events/overview-events.scss
@@ -0,0 +1,24 @@
+core-courses-course-progress {
+
+ .core-course-module-handler.item-md.item-block .item-inner {
+ border-bottom: 1px solid $list-md-border-color;
+ }
+
+ .core-course-module-handler.item-ios.item-block .item-inner {
+ border-bottom: $hairlines-width solid $list-ios-border-color;
+ }
+
+ .core-course-module-handler.item-wp.item-block .item-inner {
+ border-bottom: 1px solid $list-wp-border-color;
+ }
+
+ .core-course-module-handler.item:last-child .item-inner {
+ border-bottom: 0;
+ }
+
+ .core-course-module-handler.item .item-heading:first-child {
+ margin-top: 0;
+ }
+}
+
+
diff --git a/src/core/courses/components/overview-events/overview-events.ts b/src/core/courses/components/overview-events/overview-events.ts
new file mode 100644
index 000000000..fd566c4fa
--- /dev/null
+++ b/src/core/courses/components/overview-events/overview-events.ts
@@ -0,0 +1,135 @@
+// (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, Input, Output, OnChanges, EventEmitter, SimpleChange } from '@angular/core';
+import { CoreSitesProvider } from '../../../../providers/sites';
+import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
+import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
+import { CoreUtilsProvider } from '../../../../providers/utils/utils';
+import * as moment from 'moment';
+
+/**
+ * Directive to render a list of events in course overview.
+ */
+@Component({
+ selector: 'core-courses-overview-events',
+ templateUrl: 'overview-events.html'
+})
+export class CoreCoursesOverviewEventsComponent implements OnChanges {
+ @Input() events: any[]; // The events to render.
+ @Input() showCourse?: boolean|string; // Whether to show the course name.
+ @Input() canLoadMore?: boolean; // Whether more events can be loaded.
+ @Output() loadMore: EventEmitter; // Notify that more events should be loaded.
+
+ empty: boolean;
+ loadingMore: boolean;
+ recentlyOverdue: any[] = [];
+ today: any[] = [];
+ next7Days: any[] = [];
+ next30Days: any[] = [];
+ future: any[] = [];
+
+ constructor(private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider,
+ private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider) {
+ this.loadMore = new EventEmitter();
+ }
+
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}) {
+ this.showCourse = this.utils.isTrueOrOne(this.showCourse);
+
+ if (changes.events) {
+ this.updateEvents();
+ }
+ }
+
+ /**
+ * Filter the events by time.
+ *
+ * @param {number} start Number of days to start getting events from today. E.g. -1 will get events from yesterday.
+ * @param {number} [end] Number of days after the start.
+ */
+ protected filterEventsByTime(start: number, end?: number) {
+ start = moment().add(start, 'days').unix();
+ end = typeof end != 'undefined' ? moment().add(end, 'days').unix() : end;
+
+ return this.events.filter((event) => {
+ if (end) {
+ return start <= event.timesort && event.timesort < end;
+ }
+
+ return start <= event.timesort;
+ }).map((event) => {
+ // @todo: event.iconUrl = this.courseProvider.getModuleIconSrc(event.icon.component);
+ return event;
+ });
+ }
+
+ /**
+ * Update the events displayed.
+ */
+ protected updateEvents() {
+ this.empty = !this.events || this.events.length <= 0;
+ if (!this.empty) {
+ this.recentlyOverdue = this.filterEventsByTime(-14, 0);
+ this.today = this.filterEventsByTime(0, 1);
+ this.next7Days = this.filterEventsByTime(1, 7);
+ this.next30Days = this.filterEventsByTime(7, 30);
+ this.future = this.filterEventsByTime(30);
+ }
+ }
+
+ /**
+ * Load more events clicked.
+ */
+ loadMoreEvents() {
+ this.loadingMore = true;
+ this.loadMore.emit();
+ // this.loadMore().finally(function() {
+ // scope.loadingMore = false;
+ // });
+ }
+
+ /**
+ * Action clicked.
+ *
+ * @param {Event} e Click event.
+ * @param {string} url Url of the action.
+ */
+ action(e: Event, url: string) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Fix URL format.
+ url = this.textUtils.decodeHTMLEntities(url);
+
+ let modal = this.domUtils.showModalLoading();
+ this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url).finally(() => {
+ modal.dismiss();
+ });
+
+ // @todo
+ // $mmContentLinksHelper.handleLink(url).then((treated) => {
+ // if (!treated) {
+ // return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url);
+ // }
+ // }).finally(() => {
+ // modal.dismiss();
+ // });
+
+ return false;
+ }
+}
diff --git a/src/core/courses/courses.module.ts b/src/core/courses/courses.module.ts
new file mode 100644
index 000000000..39d01fba1
--- /dev/null
+++ b/src/core/courses/courses.module.ts
@@ -0,0 +1,38 @@
+// (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 { CoreCoursesProvider } from './providers/courses';
+import { CoreCoursesMainMenuHandler } from './providers/handlers';
+import { CoreCoursesMyOverviewProvider } from './providers/my-overview';
+import { CoreCoursesDelegate } from './providers/delegate';
+import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate';
+
+@NgModule({
+ declarations: [],
+ imports: [
+ ],
+ providers: [
+ CoreCoursesProvider,
+ CoreCoursesMainMenuHandler,
+ CoreCoursesMyOverviewProvider,
+ CoreCoursesDelegate
+ ],
+ exports: []
+})
+export class CoreCoursesModule {
+ constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreCoursesMainMenuHandler) {
+ mainMenuDelegate.registerHandler(mainMenuHandler);
+ }
+}
diff --git a/src/core/courses/lang/ar.json b/src/core/courses/lang/ar.json
new file mode 100644
index 000000000..82cbdc293
--- /dev/null
+++ b/src/core/courses/lang/ar.json
@@ -0,0 +1,19 @@
+{
+ "allowguests": "يسمح للمستخدمين الضيوف بالدخول إلى هذا المقرر الدراسي",
+ "availablecourses": "المقررات الدراسية المتاحة",
+ "categories": "تصنيفات المقررات الدراسية",
+ "courses": "المقررات الدراسية",
+ "enrolme": "سجلني",
+ "frontpage": "الصفحة الرئيسية",
+ "mycourses": "مقرراتي الدراسية",
+ "nocourses": "لا يوجد معلومات لمقرر دراسي ليتم اظهرها",
+ "nocoursesyet": "لا توجد مقررات دراسية لهذه الفئة",
+ "nosearchresults": "لا توجد نتائج لهذا البحث",
+ "notenroled": "أنت لست مسجلاً كطالب في هذا المقرر",
+ "password": "كلمة المرور",
+ "paymentrequired": "هذا المقرر الدراسي غير مجانين لذا يجب دفع القيمة للدخول.",
+ "paypalaccepted": "تم قبول التبرع المدفوع",
+ "search": "بحث",
+ "searchcourses": "بحث مقررات دراسية",
+ "sendpaymentbutton": "ارسل القيمة المدفوعة عن طريق التبرع"
+}
\ No newline at end of file
diff --git a/src/core/courses/lang/bg.json b/src/core/courses/lang/bg.json
new file mode 100644
index 000000000..704b295e7
--- /dev/null
+++ b/src/core/courses/lang/bg.json
@@ -0,0 +1,16 @@
+{
+ "allowguests": "В този курс могат да влизат гости",
+ "availablecourses": "Налични курсове",
+ "categories": "Категории курсове",
+ "courses": "Курсове",
+ "enrolme": "Запишете ме",
+ "errorloadcourses": "Грешка при зареждането на курсовете.",
+ "frontpage": "Заглавна страница",
+ "mycourses": "Моите курсове",
+ "nocourses": "Няма информация за курса, която да бъде показана.",
+ "nocoursesyet": "Няма курсове в тази категория",
+ "nosearchresults": "Няма открити резултати за Вашето търсене",
+ "password": "Ключ за записване",
+ "search": "Търсене",
+ "searchcourses": "Търсене на курсове"
+}
\ No newline at end of file
diff --git a/src/core/courses/lang/ca.json b/src/core/courses/lang/ca.json
new file mode 100644
index 000000000..6cc07396f
--- /dev/null
+++ b/src/core/courses/lang/ca.json
@@ -0,0 +1,30 @@
+{
+ "allowguests": "Aquest curs permet entrar als usuaris visitants",
+ "availablecourses": "Cursos disponibles",
+ "cannotretrievemorecategories": "No es poden recuperar categories més enllà del nivell {{$a}}.",
+ "categories": "Categories de cursos",
+ "confirmselfenrol": "Segur que voleu autoinscriure-us en aquest curs?",
+ "courses": "Cursos",
+ "enrolme": "Inscriu-me",
+ "errorloadcategories": "S'ha produït un error en carregar les categories.",
+ "errorloadcourses": "S'ha produït un error carregant els cursos.",
+ "errorsearching": "S'ha produït un error durant la cerca.",
+ "errorselfenrol": "S'ha produït un error durant l'autoinscripció.",
+ "filtermycourses": "Filtrar els meus cursos",
+ "frontpage": "Pàgina principal",
+ "mycourses": "Els meus cursos",
+ "nocourses": "No hi ha informació de cursos per mostrar.",
+ "nocoursesyet": "No hi ha cursos en aquesta categoria",
+ "nosearchresults": "La cerca no ha obtingut resultats",
+ "notenroled": "No us heu inscrit en aquest curs",
+ "notenrollable": "No podeu autoinscriure-us en aquest curs.",
+ "password": "Contrasenya",
+ "paymentrequired": "Aquest curs requereix pagament.",
+ "paypalaccepted": "S'accepten pagaments via PayPal",
+ "search": "Cerca...",
+ "searchcourses": "Cerca cursos",
+ "searchcoursesadvice": "Podeu fer servir el botó de cercar cursos per accedir als cursos com a convidat o autoinscriure-us en cursos que ho permetin.",
+ "selfenrolment": "Autoinscripció",
+ "sendpaymentbutton": "Envia pagament via Paypal",
+ "totalcoursesearchresults": "Total de cursos: {{$a}}"
+}
\ No newline at end of file
diff --git a/src/core/courses/lang/cs.json b/src/core/courses/lang/cs.json
new file mode 100644
index 000000000..4f1701f97
--- /dev/null
+++ b/src/core/courses/lang/cs.json
@@ -0,0 +1,30 @@
+{
+ "allowguests": "Tento kurz je otevřen i pro hosty",
+ "availablecourses": "Dostupné kurzy",
+ "cannotretrievemorecategories": "Kategorie hlubší než úroveň {{$a}} nelze načíst.",
+ "categories": "Kategorie kurzů",
+ "confirmselfenrol": "Jste si jisti, že chcete zapsat se do tohoto kurzu?",
+ "courses": "Kurzy",
+ "enrolme": "Zapsat se do kurzu",
+ "errorloadcategories": "Při načítání kategorií došlo k chybě.",
+ "errorloadcourses": "Při načítání kurzů došlo k chybě.",
+ "errorsearching": "Při vyhledávání došlo k chybě.",
+ "errorselfenrol": "Při zápisu sebe sama došlo k chybě.",
+ "filtermycourses": "Filtrovat mé kurzy",
+ "frontpage": "Titulní stránka",
+ "mycourses": "Moje kurzy",
+ "nocourses": "Žádné dostupné informace o kurzech",
+ "nocoursesyet": "Žádný kurz v této kategorii",
+ "nosearchresults": "Vaše vyhledávání nepřineslo žádný výsledek",
+ "notenroled": "Nejste zapsáni v tomto kurzu",
+ "notenrollable": "Do tohoto kurzu se nemůžete sami zapsat.",
+ "password": "Heslo",
+ "paymentrequired": "Tento kurz je placený",
+ "paypalaccepted": "Platby přes PayPal přijímány",
+ "search": "Hledat",
+ "searchcourses": "Vyhledat kurzy",
+ "searchcoursesadvice": "Můžete použít tlačítko Vyhledat kurzy, pracovat jako host nebo se zapsat do kurzů, které to umožňují.",
+ "selfenrolment": "Zápis sebe sama",
+ "sendpaymentbutton": "Poslat platbu přes službu PayPal",
+ "totalcoursesearchresults": "Celkem kurzů: {{$a}}"
+}
\ No newline at end of file
diff --git a/src/core/courses/lang/da.json b/src/core/courses/lang/da.json
new file mode 100644
index 000000000..7f0aaebd8
--- /dev/null
+++ b/src/core/courses/lang/da.json
@@ -0,0 +1,28 @@
+{
+ "allowguests": "Dette kursus tillader gæster",
+ "availablecourses": "Tilgængelige kurser",
+ "categories": "Kursuskategorier",
+ "confirmselfenrol": "Er du sikker på at du ønsker at tilmelde dig dette kursus?",
+ "courses": "Alle kurser",
+ "enrolme": "Tilmeld mig",
+ "errorloadcourses": "En fejl opstod ved indlæsning af kurset.",
+ "errorsearching": "En fejl opstod under søgning.",
+ "errorselfenrol": "En fejl opstod under selvtilmelding.",
+ "filtermycourses": "Filtrer mit kursus",
+ "frontpage": "Forside",
+ "mycourses": "Mine kurser",
+ "nocourses": "Du er ikke tilmeldt nogen kurser.",
+ "nocoursesyet": "Der er ingen kurser i denne kategori",
+ "nosearchresults": "Der var ingen beskeder der opfyldte søgekriteriet",
+ "notenroled": "Du er ikke tilmeldt dette kursus",
+ "notenrollable": "Du kan ikke selv tilmelde dig dette kursus.",
+ "password": "Adgangskode",
+ "paymentrequired": "Dette kursus kræver betaling for tilmelding.",
+ "paypalaccepted": "PayPal-betalinger er velkomne",
+ "search": "Søg...",
+ "searchcourses": "Søg efter kurser",
+ "searchcoursesadvice": "Du kan bruge knappen kursussøgning for at få adgang som gæst eller tilmelde dig kurser der tillader det.",
+ "selfenrolment": "Selvtilmelding",
+ "sendpaymentbutton": "Send betaling via PayPal",
+ "totalcoursesearchresults": "Kurser i alt: {{$a}}"
+}
\ No newline at end of file
diff --git a/src/core/courses/lang/de-du.json b/src/core/courses/lang/de-du.json
new file mode 100644
index 000000000..13bd07bd7
--- /dev/null
+++ b/src/core/courses/lang/de-du.json
@@ -0,0 +1,30 @@
+{
+ "allowguests": "Dieser Kurs erlaubt einen Gastzugang.",
+ "availablecourses": "Kursliste",
+ "cannotretrievemorecategories": "Kursbereiche tiefer als Level {{$a}} können nicht abgerufen werden.",
+ "categories": "Kursbereiche",
+ "confirmselfenrol": "Möchtest du dich selbst in diesen Kurs einschreiben?",
+ "courses": "Kurse",
+ "enrolme": "Einschreiben",
+ "errorloadcategories": "Fehler beim Laden von Kursbereichen",
+ "errorloadcourses": "Fehler beim Laden von Kursen",
+ "errorsearching": "Fehler beim Suchen",
+ "errorselfenrol": "Fehler bei der Selbsteinschreibung",
+ "filtermycourses": "Meine Kurse filtern",
+ "frontpage": "Startseite",
+ "mycourses": "Meine Kurse",
+ "nocourses": "Keine Kurse",
+ "nocoursesyet": "Keine Kurse in diesem Kursbereich",
+ "nosearchresults": "Keine Ergebnisse",
+ "notenroled": "Sie sind nicht in diesen Kurs eingeschrieben",
+ "notenrollable": "Du kannst dich nicht selbst in diesen Kurs einschreiben.",
+ "password": "Öffentliches Kennwort",
+ "paymentrequired": "Dieser Kurs ist gebührenpflichtig. Bitte bezahle die Teilnahmegebühr, um im Kurs eingeschrieben zu werden.",
+ "paypalaccepted": "PayPal-Zahlungen möglich",
+ "search": "Suchen",
+ "searchcourses": "Kurse suchen",
+ "searchcoursesadvice": "Du kannst Kurse suchen, um als Gast teilzunehmen oder dich selbst einzuschreiben, falls dies erlaubt ist.",
+ "selfenrolment": "Selbsteinschreibung",
+ "sendpaymentbutton": "Zahlung über PayPal",
+ "totalcoursesearchresults": "Alle Kurse: {{$a}}"
+}
\ No newline at end of file
diff --git a/src/core/courses/lang/de.json b/src/core/courses/lang/de.json
new file mode 100644
index 000000000..3f93d4db3
--- /dev/null
+++ b/src/core/courses/lang/de.json
@@ -0,0 +1,30 @@
+{
+ "allowguests": "Dieser Kurs erlaubt einen Gastzugang.",
+ "availablecourses": "Kursliste",
+ "cannotretrievemorecategories": "Kursbereiche tiefer als Level {{$a}} können nicht abgerufen werden.",
+ "categories": "Kursbereiche",
+ "confirmselfenrol": "Möchten Sie sich selbst in diesen Kurs einschreiben?",
+ "courses": "Kurse",
+ "enrolme": "Einschreiben",
+ "errorloadcategories": "Fehler beim Laden von Kursbereichen",
+ "errorloadcourses": "Fehler beim Laden von Kursen",
+ "errorsearching": "Fehler beim Suchen",
+ "errorselfenrol": "Fehler bei der Selbsteinschreibung",
+ "filtermycourses": "Meine Kurse filtern",
+ "frontpage": "Startseite",
+ "mycourses": "Meine Kurse",
+ "nocourses": "Keine Kurse",
+ "nocoursesyet": "Keine Kurse in diesem Kursbereich",
+ "nosearchresults": "Keine Suchergebnisse",
+ "notenroled": "Sie sind nicht in diesen Kurs eingeschrieben",
+ "notenrollable": "Sie können sich nicht selbst in diesen Kurs einschreiben.",
+ "password": "Öffentliches Kennwort",
+ "paymentrequired": "Dieser Kurs ist entgeltpflichtig. Bitte bezahlen Sie das Teilnahmeentgelt, um in den Kurs eingeschrieben zu werden.",
+ "paypalaccepted": "PayPal-Zahlungen möglich",
+ "search": "Suchen",
+ "searchcourses": "Kurse suchen",
+ "searchcoursesadvice": "Sie können Kurse suchen, um als Gast teilzunehmen oder sich selbst einzuschreiben, falls dies erlaubt ist.",
+ "selfenrolment": "Selbsteinschreibung",
+ "sendpaymentbutton": "Zahlung über PayPal",
+ "totalcoursesearchresults": "Alle Kurse: {{$a}}"
+}
\ No newline at end of file
diff --git a/src/core/courses/lang/el.json b/src/core/courses/lang/el.json
new file mode 100644
index 000000000..9b9181e25
--- /dev/null
+++ b/src/core/courses/lang/el.json
@@ -0,0 +1,30 @@
+{
+ "allowguests": "Σε αυτό το μάθημα επιτρέπονται και οι επισκέπτες",
+ "availablecourses": "Διαθέσιμα Μαθήματα",
+ "cannotretrievemorecategories": "Δεν είναι δυνατή η ανάκτηση κατηγοριών μετά από το επίπεδο {{$a}}.",
+ "categories": "Κατηγορίες μαθημάτων",
+ "confirmselfenrol": "Είστε σίγουροι ότι θέλετε να εγγραφείτε σε αυτό το μάθημα;",
+ "courses": "Μαθήματα",
+ "enrolme": "Εγγραφή",
+ "errorloadcategories": "Παρουσιάστηκε σφάλμα κατά την φόρτωση των κατηγοριών.",
+ "errorloadcourses": "Παρουσιάστηκε σφάλμα κατά τη φόρτωση των μαθημάτων.",
+ "errorsearching": "Παρουσιάστηκε σφάλμα κατά τη διάρκεια της αναζήτησης.",
+ "errorselfenrol": "Παρουσιάστηκε σφάλμα κατά τη διάρκεια της αυτο-εγγραφής.",
+ "filtermycourses": "Φιλτράρισμα των μαθημάτων μου",
+ "frontpage": "Αρχική σελίδα",
+ "mycourses": "Τα μαθήματά μου",
+ "nocourses": "Δεν υπάρχει πληροφορία του μαθήματος για προβολή.",
+ "nocoursesyet": "Δεν υπάρχουν μαθήματα σε αυτήν την κατηγορία",
+ "nosearchresults": "Δε βρέθηκαν αποτελέσματα για την αναζήτησή σας",
+ "notenroled": "Δεν είσαι εγγεγραμμένος σε αυτό το μάθημα",
+ "notenrollable": "Δεν μπορείτε να αυτο-εγγραφείτε σε αυτό το μάθημα.",
+ "password": "Κωδικός πρόσβασης",
+ "paymentrequired": "Αυτό το μάθημα απαιτεί πληρωμή για την είσοδο.",
+ "paypalaccepted": "Αποδεκτές οι πληρωμές μέσω PayPal",
+ "search": "Αναζήτηση",
+ "searchcourses": "Αναζήτηση μαθημάτων",
+ "searchcoursesadvice": "Μπορείτε να χρησιμοποιήσετε το κουμπί Αναζήτηση μαθημάτων για πρόσβαση ως επισκέπτης ή για να αυτο-εγγραφείτε σε μαθήματα που το επιτρέπουν.",
+ "selfenrolment": "Αυτο-εγγραφή",
+ "sendpaymentbutton": "Αποστολή πληρωμής με Paypal",
+ "totalcoursesearchresults": "Συνολικά μαθήματα: {{$a}}"
+}
\ No newline at end of file
diff --git a/src/core/courses/lang/en.json b/src/core/courses/lang/en.json
new file mode 100644
index 000000000..09a14d7f8
--- /dev/null
+++ b/src/core/courses/lang/en.json
@@ -0,0 +1,47 @@
+{
+ "allowguests": "This course allows guest users to enter",
+ "availablecourses": "Available courses",
+ "cannotretrievemorecategories": "Categories deeper than level {{$a}} cannot be retrieved.",
+ "categories": "Course categories",
+ "confirmselfenrol": "Are you sure you want to enrol yourself in this course?",
+ "courseoverview": "Course overview",
+ "courses": "Courses",
+ "downloadcourses": "Download courses",
+ "enrolme": "Enrol me",
+ "errorloadcategories": "An error occurred while loading categories.",
+ "errorloadcourses": "An error occurred while loading courses.",
+ "errorsearching": "An error occurred while searching.",
+ "errorselfenrol": "An error occurred while self enrolling.",
+ "filtermycourses": "Filter my courses",
+ "frontpage": "Front page",
+ "future": "Future",
+ "inprogress": "In progress",
+ "morecourses": "More courses",
+ "mycourses": "My courses",
+ "next30days": "Next 30 days",
+ "next7days": "Next 7 days",
+ "nocourses": "No course information to show.",
+ "nocoursesfuture": "No future courses",
+ "nocoursesinprogress": "No in progress courses",
+ "nocoursesoverview": "No courses",
+ "nocoursespast": "No past courses",
+ "nocoursesyet": "No courses in this category",
+ "noevents": "No upcoming activities due",
+ "nosearchresults": "There were no results from your search",
+ "notenroled": "You are not enrolled in this course",
+ "notenrollable": "You cannot enrol yourself in this course.",
+ "password": "Enrolment key",
+ "past": "Past",
+ "paymentrequired": "This course requires a payment for entry.",
+ "paypalaccepted": "PayPal payments accepted",
+ "recentlyoverdue": "Recently overdue",
+ "search": "Search",
+ "searchcourses": "Search courses",
+ "searchcoursesadvice": "You can use the search courses button to find courses to access as a guest or enrol yourself in courses that allow it.",
+ "selfenrolment": "Self enrolment",
+ "sendpaymentbutton": "Send payment via PayPal",
+ "sortbycourses": "Sort by courses",
+ "sortbydates": "Sort by dates",
+ "timeline": "Timeline",
+ "totalcoursesearchresults": "Total courses: {{$a}}"
+}
\ No newline at end of file
diff --git a/src/core/courses/lang/es-mx.json b/src/core/courses/lang/es-mx.json
new file mode 100644
index 000000000..1363fb2cc
--- /dev/null
+++ b/src/core/courses/lang/es-mx.json
@@ -0,0 +1,30 @@
+{
+ "allowguests": "Este curso permite la entrada de invitados",
+ "availablecourses": "Cursos disponibles",
+ "cannotretrievemorecategories": "No se pueden recuperar categorías más profundas que el nivel {{$a}}.",
+ "categories": "Categorías",
+ "confirmselfenrol": "¿Está Usted seguro de querer inscribirse a Usted mismo en este curso?",
+ "courses": "Cursos",
+ "enrolme": "Inscribirme",
+ "errorloadcategories": "Ocurrió un error al cargar categorías.",
+ "errorloadcourses": "Ocurrió un error al cargar los cursos.",
+ "errorsearching": "Ocurrio un error al buscar.",
+ "errorselfenrol": "Ocurrio un error al auto-inscribir.",
+ "filtermycourses": "<<
+
+ {{ 'core.courses.availablecourses' | translate }}
+
+
+
+
+
+
+
+
0">
+
+
+
+
+
diff --git a/src/core/courses/pages/available-courses/available-courses.module.ts b/src/core/courses/pages/available-courses/available-courses.module.ts
new file mode 100644
index 000000000..5fb00ab0a
--- /dev/null
+++ b/src/core/courses/pages/available-courses/available-courses.module.ts
@@ -0,0 +1,33 @@
+// (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 { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreCoursesAvailableCoursesPage } from './available-courses';
+import { CoreComponentsModule } from '../../../../components/components.module';
+import { CoreCoursesComponentsModule } from '../../components/components.module';
+
+@NgModule({
+ declarations: [
+ CoreCoursesAvailableCoursesPage,
+ ],
+ imports: [
+ CoreComponentsModule,
+ CoreCoursesComponentsModule,
+ IonicPageModule.forChild(CoreCoursesAvailableCoursesPage),
+ TranslateModule.forChild()
+ ],
+})
+export class CoreCoursesAvailableCoursesPageModule {}
diff --git a/src/core/courses/pages/available-courses/available-courses.ts b/src/core/courses/pages/available-courses/available-courses.ts
new file mode 100644
index 000000000..47d71babf
--- /dev/null
+++ b/src/core/courses/pages/available-courses/available-courses.ts
@@ -0,0 +1,76 @@
+// (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 } from '@angular/core';
+import { IonicPage } from 'ionic-angular';
+import { CoreSitesProvider } from '../../../../providers/sites';
+import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
+import { CoreCoursesProvider } from '../../providers/courses';
+
+/**
+ * Page that displays available courses in current site.
+ */
+@IonicPage()
+@Component({
+ selector: 'page-core-courses-available-courses',
+ templateUrl: 'available-courses.html',
+})
+export class CoreCoursesAvailableCoursesPage {
+ courses: any[] = [];
+ coursesLoaded: boolean;
+
+ constructor(private coursesProvider: CoreCoursesProvider, private domUtils: CoreDomUtilsProvider,
+ private sitesProvider: CoreSitesProvider) {}
+
+ /**
+ * View loaded.
+ */
+ ionViewDidLoad() {
+ this.loadCourses().finally(() => {
+ this.coursesLoaded = true;
+ });
+ }
+
+ /**
+ * Load the courses.
+ */
+ protected loadCourses() {
+ const frontpageCourseId = this.sitesProvider.getCurrentSite().getSiteHomeId();
+ return this.coursesProvider.getCoursesByField().then((courses) => {
+ this.courses = courses.filter((course) => {
+ return course.id != frontpageCourseId;
+ });
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
+ });
+ }
+
+ /**
+ * Refresh the courses.
+ *
+ * @param {any} refresher Refresher.
+ */
+ refreshCourses(refresher: any) {
+ let promises = [];
+
+ promises.push(this.coursesProvider.invalidateUserCourses());
+ promises.push(this.coursesProvider.invalidateCoursesByField());
+
+ Promise.all(promises).finally(() => {
+ this.loadCourses().finally(() => {
+ refresher.complete();
+ });
+ });
+ };
+}
diff --git a/src/core/courses/pages/categories/categories.html b/src/core/courses/pages/categories/categories.html
new file mode 100644
index 000000000..c22dfe194
--- /dev/null
+++ b/src/core/courses/pages/categories/categories.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+